diff options
Diffstat (limited to 'admin-panel/src/server/routes/tasks.ts')
| -rw-r--r-- | admin-panel/src/server/routes/tasks.ts | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/admin-panel/src/server/routes/tasks.ts b/admin-panel/src/server/routes/tasks.ts new file mode 100644 index 0000000..1ab1e8d --- /dev/null +++ b/admin-panel/src/server/routes/tasks.ts @@ -0,0 +1,248 @@ +import { Router, Request, Response } from 'express'; +import { getAdminDb, getProjectDb } from '../db.js'; +import { broadcast } from '../ws.js'; + +const router = Router({ mergeParams: true }); + +type Params = { project: string; id?: string }; + +interface Task { + id: number; + title: string; + status: string; + priority: string; + level: number; + description: string | null; + plan: string | null; + decision_log: string | null; + done_when: string | null; + implementation_notes: string | null; + tags: string; + plan_review_comments: string; + review_comments: string; + test_results: string; + agent_log: string; + current_agent: string | null; + plan_review_count: number; + impl_review_count: number; + branch_name: string | null; + gerrit_change_id: string | null; + gerrit_change_number: number | null; + diff: string | null; + rank: number; + created_at: string; + started_at: string | null; + planned_at: string | null; + reviewed_at: string | null; + tested_at: string | null; + completed_at: string | null; +} + +const VALID_STATUSES = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done']; +const VALID_PRIORITIES = ['high', 'medium', 'low']; + +function getProject(projectName: string): boolean { + const db = getAdminDb(); + return !!db.prepare('SELECT id FROM projects WHERE name = ?').get(projectName); +} + +// GET /api/projects/:project/tasks +router.get('/', (req: Request<Params>, res: Response) => { + const { project } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const tasks = db.prepare('SELECT * FROM tasks ORDER BY rank ASC, id ASC').all(); + res.json(tasks); +}); + +// POST /api/projects/:project/tasks +router.post('/', (req: Request<Params>, res: Response) => { + const { project } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const { title, description, priority, level, tags, status } = req.body; + if (!title) { + res.status(400).json({ error: 'title is required' }); + return; + } + + const db = getProjectDb(project); + + // Get max rank for ordering + const maxRank = (db.prepare('SELECT MAX(rank) as max FROM tasks').get() as { max: number | null })?.max || 0; + + const result = db.prepare( + `INSERT INTO tasks (title, description, priority, level, tags, status, rank) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).run( + title, + description || null, + VALID_PRIORITIES.includes(priority) ? priority : 'medium', + [1, 2, 3].includes(level) ? level : 3, + JSON.stringify(tags || []), + VALID_STATUSES.includes(status) ? status : 'todo', + maxRank + 1 + ); + + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid); + broadcast(project, { type: 'task:created', project, task }); + res.status(201).json(task); +}); + +// GET /api/projects/:project/tasks/:id +router.get('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + res.json(task); +}); + +// PATCH /api/projects/:project/tasks/:id +router.patch('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id) as Task | undefined; + if (!existing) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + const allowedFields = [ + 'title', 'status', 'priority', 'level', 'description', 'plan', + 'decision_log', 'done_when', 'implementation_notes', 'tags', + 'plan_review_comments', 'review_comments', 'test_results', + 'agent_log', 'current_agent', 'plan_review_count', 'impl_review_count', + 'branch_name', 'gerrit_change_id', 'gerrit_change_number', 'diff', 'rank' + ]; + + const updates: string[] = []; + const params: unknown[] = []; + + for (const field of allowedFields) { + if (req.body[field] !== undefined) { + let value = req.body[field]; + + // Validate specific fields + if (field === 'status' && !VALID_STATUSES.includes(value)) continue; + if (field === 'priority' && !VALID_PRIORITIES.includes(value)) continue; + if (field === 'level' && ![1, 2, 3].includes(value)) continue; + + // Stringify JSON fields + if (['tags', 'plan_review_comments', 'review_comments', 'test_results', 'agent_log'].includes(field)) { + value = typeof value === 'string' ? value : JSON.stringify(value); + } + + updates.push(`${field} = ?`); + params.push(value); + } + } + + // Set timestamp fields based on status changes + if (req.body.status) { + const now = new Date().toISOString(); + switch (req.body.status) { + case 'plan': + if (!existing.started_at) { + updates.push('started_at = ?'); + params.push(now); + } + break; + case 'impl': + updates.push('planned_at = ?'); + params.push(now); + break; + case 'test': + updates.push('reviewed_at = ?'); + params.push(now); + break; + case 'done': + updates.push('completed_at = ?'); + params.push(now); + break; + } + } + + if (updates.length === 0) { + res.json(existing); + return; + } + + params.push(id); + db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + broadcast(project, { type: 'task:updated', project, task }); + res.json(task); +}); + +// DELETE /api/projects/:project/tasks/:id +router.delete('/:id', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const db = getProjectDb(project); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + if (!task) { + res.status(404).json({ error: 'Task not found' }); + return; + } + + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); + broadcast(project, { type: 'task:deleted', project, taskId: Number(id) }); + res.status(204).send(); +}); + +// PATCH /api/projects/:project/tasks/:id/reorder +router.patch('/:id/reorder', (req: Request<Params>, res: Response) => { + const { project, id } = req.params; + if (!getProject(project)) { + res.status(404).json({ error: 'Project not found' }); + return; + } + + const { rank, status } = req.body; + if (rank === undefined) { + res.status(400).json({ error: 'rank is required' }); + return; + } + + const db = getProjectDb(project); + const updates: string[] = ['rank = ?']; + const params: unknown[] = [rank]; + + if (status && VALID_STATUSES.includes(status)) { + updates.push('status = ?'); + params.push(status); + } + + params.push(id); + db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); + broadcast(project, { type: 'task:updated', project, task }); + res.json(task); +}); + +export default router; |
