summaryrefslogtreecommitdiff
path: root/admin-panel/src/server/routes/tasks.ts
diff options
context:
space:
mode:
Diffstat (limited to 'admin-panel/src/server/routes/tasks.ts')
-rw-r--r--admin-panel/src/server/routes/tasks.ts248
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;