diff --git a/src/server/board.ts b/src/server/board.ts
new file mode 100644
index 0000000..9ae1e07
--- /dev/null
+++ b/src/server/board.ts
@@ -0,0 +1,280 @@
+/**
+ * Self-contained Trello-like task board, served at `GET /board`.
+ *
+ * Design constraints (MVP — maximum simplicity):
+ * - One static HTML page: inline CSS + JS, no build step, no framework, no npm deps.
+ * - Reads only the existing same-origin endpoints (`/tasks`, `/handoffs`,
+ * `/decisions`). It never mutates state — purely an observability surface.
+ * - Auto-refreshes on a small interval via `setInterval` + `fetch`.
+ *
+ * The column skeleton is rendered server-side so the markup (and its
+ * `data-column` markers) exist even before the client JS runs; JS only fills
+ * the cards.
+ */
+
+export interface BoardColumn {
+ key: string;
+ label: string;
+}
+
+/** Task statuses, in board order. Mirrors `TaskStatus` in core/schema.ts. */
+export const BOARD_COLUMNS: BoardColumn[] = [
+ { key: 'open', label: 'Open' },
+ { key: 'in_progress', label: 'In Progress' },
+ { key: 'review', label: 'Review' },
+ { key: 'done', label: 'Done' },
+ { key: 'cancelled', label: 'Cancelled' },
+];
+
+function columnSkeleton(): string {
+ return BOARD_COLUMNS.map(
+ (c) => ` `,
+ ).join('\n');
+}
+
+export function renderBoardHtml(): string {
+ return `
+
+
+
+
+
+ AgentHub Board
+
+
+
+
+
+
AgentHub Board
+
+ loading…
+
+
+
+${columnSkeleton()}
+
+
+
+
+
+
+
+`;
+}
diff --git a/src/server/routes.ts b/src/server/routes.ts
index 8fad2fa..b8b98dc 100644
--- a/src/server/routes.ts
+++ b/src/server/routes.ts
@@ -6,6 +6,7 @@ import { addMemory, searchMemory, listMemory } from '../core/services/memoryServ
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) {
@@ -17,6 +18,12 @@ function badRequest(reply: FastifyReply, message: string) {
}
export async function registerRoutes(app: FastifyInstance, cwd: string): Promise {
+ // 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) }));
diff --git a/tests/server.test.ts b/tests/server.test.ts
index 6a71c35..634294e 100644
--- a/tests/server.test.ts
+++ b/tests/server.test.ts
@@ -60,4 +60,22 @@ describe('server routes', () => {
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload)).toHaveLength(1);
});
+
+ it('serves the task board as HTML via GET /board', async () => {
+ const res = await app.inject({ method: 'GET', url: '/board' });
+ expect(res.statusCode).toBe(200);
+ expect(res.headers['content-type']).toContain('text/html');
+
+ const html = res.payload;
+ expect(html).toContain('AgentHub Board');
+ // All five status columns are present in the static markup.
+ for (const col of ['open', 'in_progress', 'review', 'done', 'cancelled']) {
+ expect(html).toContain(`data-column="${col}"`);
+ }
+ // Handoffs + decisions panels and the polling logic are wired in.
+ expect(html).toContain('Handoffs');
+ expect(html).toContain('Decisions');
+ expect(html).toContain("getJSON('/tasks')");
+ expect(html).toContain('setInterval(refresh');
+ });
});