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:
beabigegg
2026-01-10 22:13:43 +08:00
parent 96210c7ad4
commit 3bdc6ff1c9
106 changed files with 9704 additions and 429 deletions

View File

@@ -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