feat: implement 8 OpenSpec proposals for security, reliability, and UX improvements
## Security Enhancements (P0) - Add input validation with max_length and numeric range constraints - Implement WebSocket token authentication via first message - Add path traversal prevention in file storage service ## Permission Enhancements (P0) - Add project member management for cross-department access - Implement is_department_manager flag for workload visibility ## Cycle Detection (P0) - Add DFS-based cycle detection for task dependencies - Add formula field circular reference detection - Display user-friendly cycle path visualization ## Concurrency & Reliability (P1) - Implement optimistic locking with version field (409 Conflict on mismatch) - Add trigger retry mechanism with exponential backoff (1s, 2s, 4s) - Implement cascade restore for soft-deleted tasks ## Rate Limiting (P1) - Add tiered rate limits: standard (60/min), sensitive (20/min), heavy (5/min) - Apply rate limits to tasks, reports, attachments, and comments ## Frontend Improvements (P1) - Add responsive sidebar with hamburger menu for mobile - Improve touch-friendly UI with proper tap target sizes - Complete i18n translations for all components ## Backend Reliability (P2) - Configure database connection pool (size=10, overflow=20) - Add Redis fallback mechanism with message queue - Add blocker check before task deletion ## API Enhancements (P3) - Add standardized response wrapper utility - Add /health/ready and /health/live endpoints - Implement project templates with status/field copying ## Tests Added - test_input_validation.py - Schema and path traversal tests - test_concurrency_reliability.py - Optimistic locking and retry tests - test_backend_reliability.py - Connection pool and Redis tests - test_api_enhancements.py - Health check and template tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ interface Task {
|
||||
subtask_count: number
|
||||
parent_task_id: string | null
|
||||
custom_values?: CustomValueResponse[]
|
||||
version?: number
|
||||
}
|
||||
|
||||
interface TaskStatus {
|
||||
@@ -52,9 +53,14 @@ export function TaskDetailModal({
|
||||
onUpdate,
|
||||
onSubtaskClick,
|
||||
}: TaskDetailModalProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const { t, i18n } = useTranslation('tasks')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [conflictError, setConflictError] = useState<{
|
||||
message: string
|
||||
currentVersion: number
|
||||
yourVersion: number
|
||||
} | null>(null)
|
||||
const [editForm, setEditForm] = useState({
|
||||
title: task.title,
|
||||
description: task.description || '',
|
||||
@@ -153,6 +159,7 @@ export function TaskDetailModal({
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setConflictError(null)
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
title: editForm.title,
|
||||
@@ -160,6 +167,11 @@ export function TaskDetailModal({
|
||||
priority: editForm.priority,
|
||||
}
|
||||
|
||||
// Include version for optimistic locking
|
||||
if (task.version) {
|
||||
payload.version = task.version
|
||||
}
|
||||
|
||||
// Always send status_id (null to clear, or the value)
|
||||
payload.status_id = editForm.status_id || null
|
||||
// Always send assignee_id (null to clear, or the value)
|
||||
@@ -194,13 +206,34 @@ export function TaskDetailModal({
|
||||
await api.patch(`/tasks/${task.id}`, payload)
|
||||
setIsEditing(false)
|
||||
onUpdate()
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
// Handle 409 Conflict - version mismatch
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const axiosError = err as { response?: { status?: number; data?: { detail?: { error?: string; message?: string; current_version?: number; your_version?: number } } } }
|
||||
if (axiosError.response?.status === 409) {
|
||||
const detail = axiosError.response?.data?.detail
|
||||
if (detail?.error === 'conflict') {
|
||||
setConflictError({
|
||||
message: detail.message || t('conflict.message'),
|
||||
currentVersion: detail.current_version || 0,
|
||||
yourVersion: detail.your_version || 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error('Failed to update task:', err)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshTask = () => {
|
||||
setConflictError(null)
|
||||
setIsEditing(false)
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
const handleCustomFieldChange = (fieldId: string, value: string | number | null) => {
|
||||
setEditCustomValues((prev) => ({
|
||||
...prev,
|
||||
@@ -289,6 +322,22 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conflict Error Banner */}
|
||||
{conflictError && (
|
||||
<div style={styles.conflictBanner}>
|
||||
<div style={styles.conflictContent}>
|
||||
<div style={styles.conflictIcon}>!</div>
|
||||
<div style={styles.conflictText}>
|
||||
<div style={styles.conflictTitle}>{t('conflict.title')}</div>
|
||||
<div style={styles.conflictMessage}>{t('conflict.message')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleRefreshTask} style={styles.conflictRefreshButton}>
|
||||
{t('common:buttons.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={styles.content}>
|
||||
<div style={styles.mainSection}>
|
||||
{/* Description */}
|
||||
@@ -424,7 +473,11 @@ export function TaskDetailModal({
|
||||
) : (
|
||||
<div style={styles.dueDateDisplay}>
|
||||
{task.due_date
|
||||
? new Date(task.due_date).toLocaleDateString()
|
||||
? new Date(task.due_date).toLocaleDateString(i18n.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: t('status.noDueDate')}
|
||||
</div>
|
||||
)}
|
||||
@@ -742,6 +795,54 @@ const styles: Record<string, React.CSSProperties> = {
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
conflictBanner: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderBottom: '1px solid #ffc107',
|
||||
},
|
||||
conflictContent: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
},
|
||||
conflictIcon: {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ff9800',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
},
|
||||
conflictText: {
|
||||
flex: 1,
|
||||
},
|
||||
conflictTitle: {
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
color: '#856404',
|
||||
},
|
||||
conflictMessage: {
|
||||
fontSize: '13px',
|
||||
color: '#856404',
|
||||
marginTop: '2px',
|
||||
},
|
||||
conflictRefreshButton: {
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#ff9800',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
},
|
||||
}
|
||||
|
||||
export default TaskDetailModal
|
||||
|
||||
Reference in New Issue
Block a user