Serves a single self-contained HTML page (inline CSS+JS, no build step, no framework, no npm deps) that polls /tasks, /handoffs and /decisions on the same origin and renders a column board by task status (open/in_progress/review/done/cancelled). Each card shows id, title, role and assignedTo; small handoffs + decisions panels below; auto-refresh every 4s with a connection indicator. Purely additive and read-only — the CLI core path is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { FastifyInstance, FastifyReply } from 'fastify';
|
|
import { createTask, listTasks, getTask, claimTask, doneTask } from '../core/services/taskService.js';
|
|
import { createHandoff, listHandoffs, getHandoff } from '../core/services/handoffService.js';
|
|
import { createDecision, listDecisions } from '../core/services/decisionService.js';
|
|
import { addMemory, searchMemory, listMemory } from '../core/services/memoryService.js';
|
|
import { getStatus, updateStatus } from '../core/services/statusService.js';
|
|
import { autoDelegate, suggestDelegation } from '../core/services/delegateService.js';
|
|
import { loadConfig } from '../core/config.js';
|
|
import { renderBoardHtml } from './board.js';
|
|
import type { Task, Handoff, Decision, Memory } from '../core/schema.js';
|
|
|
|
function notFound(reply: FastifyReply, resource: string) {
|
|
return reply.status(404).send({ error: `${resource} not found` });
|
|
}
|
|
|
|
function badRequest(reply: FastifyReply, message: string) {
|
|
return reply.status(400).send({ error: message });
|
|
}
|
|
|
|
export async function registerRoutes(app: FastifyInstance, cwd: string): Promise<void> {
|
|
// Static, self-contained Trello-like board. Polls /tasks, /handoffs and
|
|
// /decisions on the same origin; no build step, no deps. Cached once — the
|
|
// markup is constant, only the data it fetches changes.
|
|
const boardHtml = renderBoardHtml();
|
|
app.get('/board', async (_request, reply) => reply.type('text/html; charset=utf-8').send(boardHtml));
|
|
|
|
app.get('/status', async () => ({ body: getStatus(cwd) }));
|
|
app.post('/status/update', async () => ({ body: updateStatus(cwd) }));
|
|
|
|
app.get('/tasks', async (request) => {
|
|
const { status, role } = request.query as { status?: string; role?: string };
|
|
return listTasks(cwd, { status, role });
|
|
});
|
|
|
|
app.post('/tasks', async (request, reply) => {
|
|
try {
|
|
return createTask(cwd, request.body as Partial<Task>);
|
|
} catch (err) {
|
|
return badRequest(reply, err instanceof Error ? err.message : 'Invalid task');
|
|
}
|
|
});
|
|
|
|
app.get('/tasks/:id', async (request, reply) => {
|
|
const { id } = request.params as { id: string };
|
|
try {
|
|
const { task, body } = getTask(cwd, id);
|
|
return { task, body };
|
|
} catch {
|
|
return notFound(reply, 'Task');
|
|
}
|
|
});
|
|
|
|
app.patch('/tasks/:id', async (request, reply) => {
|
|
const { id } = request.params as { id: string };
|
|
const patch = request.body as Partial<Task>;
|
|
if (patch.status === 'in_progress' && patch.assignedTo) {
|
|
return claimTask(cwd, id, patch.assignedTo);
|
|
}
|
|
if (patch.status === 'done') {
|
|
return doneTask(cwd, id);
|
|
}
|
|
return badRequest(reply, 'Unsupported patch');
|
|
});
|
|
|
|
app.get('/handoffs', async () => listHandoffs(cwd));
|
|
app.post('/handoffs', async (request, reply) => {
|
|
try {
|
|
return createHandoff(cwd, request.body as Partial<Handoff>);
|
|
} catch (err) {
|
|
return badRequest(reply, err instanceof Error ? err.message : 'Invalid handoff');
|
|
}
|
|
});
|
|
app.get('/handoffs/:id', async (request, reply) => {
|
|
const { id } = request.params as { id: string };
|
|
try {
|
|
const { handoff, body } = getHandoff(cwd, id);
|
|
return { handoff, body };
|
|
} catch {
|
|
return notFound(reply, 'Handoff');
|
|
}
|
|
});
|
|
|
|
app.get('/decisions', async () => listDecisions(cwd));
|
|
app.post('/decisions', async (request, reply) => {
|
|
try {
|
|
return createDecision(cwd, request.body as Partial<Decision>);
|
|
} catch (err) {
|
|
return badRequest(reply, err instanceof Error ? err.message : 'Invalid decision');
|
|
}
|
|
});
|
|
|
|
app.get('/memory', async () => listMemory(cwd));
|
|
app.post('/memory', async (request, reply) => {
|
|
try {
|
|
return addMemory(cwd, request.body as Partial<Memory>);
|
|
} catch (err) {
|
|
return badRequest(reply, err instanceof Error ? err.message : 'Invalid memory');
|
|
}
|
|
});
|
|
app.get('/memory/search', async (request) => {
|
|
const { q } = request.query as { q: string };
|
|
return searchMemory(cwd, q ?? '');
|
|
});
|
|
|
|
app.post('/delegate', async (request) => {
|
|
const { auto } = request.query as { auto?: string };
|
|
const config = loadConfig(cwd);
|
|
const suggestion = suggestDelegation(cwd);
|
|
if (!suggestion) return { suggestion: null };
|
|
const shouldAuto = auto === 'true' || config.delegationMode === 'auto';
|
|
if (shouldAuto) {
|
|
const handoff = autoDelegate(cwd);
|
|
return { suggestion, handoff };
|
|
}
|
|
return { suggestion };
|
|
});
|
|
}
|