Extract dashboard simple route table (#70)

* review: harden readonly git checks and lint verification

* test: satisfy readonly reviewer sandbox typing

* test: fix readonly reviewer sandbox assertions

* test: remove impossible readonly sandbox guards

* test: relax readonly sandbox assertions

* test: cast readonly sandbox expectation shape

* test: cast readonly sandbox expectation through unknown

* Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke

* Add STEP_DONE guards, verdict storage, and stale delivery suppression

* style: format paired stepdone telemetry files

* Route STEP_DONE through reviewer

* Add structured Discord attachments

* style: format structured attachment test

* Fix room thread bot output parity

* Remove duplicate work item attachment migration from owner branch

* Remove legacy dashboard rooms renderer

* Extract dashboard parsed body renderer

* Extract dashboard redaction helpers

* Extract dashboard RoomCardV2 component

* Include RoomCardV2 test in vitest suite

* Extract dashboard RoomBoardV2 component

* Extract dashboard EmptyState component

* Extract dashboard InboxPanel component

* Split dashboard InboxPanel card renderer

* Extract dashboard TaskPanel component

* Extract dashboard UsagePanel component

* Fix dashboard room thread chunk rendering

* Extract dashboard ServicePanel component

* Render dashboard live progress markdown

* Extract dashboard SettingsPanel component

* Fix structured attachment rendering

* Extract dashboard simple route table
This commit is contained in:
Eyejoker
2026-04-28 17:51:15 +09:00
committed by GitHub
parent 6197e90c8c
commit fb79aa307b
3 changed files with 144 additions and 10 deletions

View File

@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';
import type { StatusSnapshot } from './status-dashboard.js';
import type { ScheduledTask } from './types.js';
import { handleSimpleGetRoute } from './web-dashboard-routes.js';
function jsonResponse(value: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(value), {
...init,
headers: {
'content-type': 'application/json; charset=utf-8',
...(init?.headers as Record<string, string> | undefined),
},
});
}
function makeTask(overrides: Partial<ScheduledTask> = {}): ScheduledTask {
return {
id: 'task-1',
group_folder: 'general',
chat_jid: 'dc:general',
agent_type: null,
status_message_id: null,
status_started_at: null,
prompt: 'regular scheduled task',
schedule_type: 'cron',
schedule_value: '* * * * *',
context_mode: 'group',
next_run: '2026-04-26T05:00:00.000Z',
last_run: null,
last_result: null,
status: 'active',
created_at: '2026-04-26T04:00:00.000Z',
...overrides,
};
}
describe('web dashboard simple routes', () => {
const snapshots: StatusSnapshot[] = [
{
serviceId: 'codex-main',
agentType: 'codex',
assistantName: 'Codex',
updatedAt: '2026-04-26T05:00:00.000Z',
entries: [],
},
];
it('serves health, snapshots, and task JSON routes', async () => {
const routeContext = {
statusMaxAgeMs: 1234,
readSnapshots: (maxAgeMs: number) => {
expect(maxAgeMs).toBe(1234);
return snapshots;
},
loadTasks: () => [makeTask()],
jsonResponse,
};
const health = handleSimpleGetRoute({
...routeContext,
url: new URL('http://localhost/api/health'),
});
expect(health?.status).toBe(200);
await expect(health?.json()).resolves.toEqual({ ok: true });
const status = handleSimpleGetRoute({
...routeContext,
url: new URL('http://localhost/api/status-snapshots'),
});
expect(status?.status).toBe(200);
await expect(status?.json()).resolves.toEqual(snapshots);
const tasks = handleSimpleGetRoute({
...routeContext,
url: new URL('http://localhost/api/tasks'),
});
expect(tasks?.status).toBe(200);
await expect(tasks?.json()).resolves.toMatchObject([
{
id: 'task-1',
groupFolder: 'general',
promptPreview: 'regular scheduled task',
status: 'active',
},
]);
});
it('returns null for routes outside the simple table', () => {
const result = handleSimpleGetRoute({
url: new URL('http://localhost/api/overview'),
statusMaxAgeMs: 1234,
readSnapshots: () => [],
loadTasks: () => [],
jsonResponse,
});
expect(result).toBeNull();
});
});

View File

@@ -0,0 +1,35 @@
import type { StatusSnapshot } from './status-dashboard.js';
import type { ScheduledTask } from './types.js';
import { sanitizeScheduledTask } from './web-dashboard-data.js';
import { serveValidatedAttachment } from './web-dashboard-attachments.js';
type JsonResponse = (
value: unknown,
init?: ResponseInit,
request?: Request,
) => Response;
interface SimpleGetRouteContext {
url: URL;
statusMaxAgeMs: number;
readSnapshots: (maxAgeMs: number) => StatusSnapshot[];
loadTasks: () => ScheduledTask[];
jsonResponse: JsonResponse;
}
export function handleSimpleGetRoute({
url,
statusMaxAgeMs,
readSnapshots,
loadTasks,
jsonResponse,
}: SimpleGetRouteContext): Response | null {
const simpleGetRoutes: Record<string, () => Response> = {
'/api/health': () => jsonResponse({ ok: true }),
'/api/status-snapshots': () => jsonResponse(readSnapshots(statusMaxAgeMs)),
'/api/tasks': () => jsonResponse(loadTasks().map(sanitizeScheduledTask)),
'/api/attachments': () => serveValidatedAttachment(url),
};
const route = simpleGetRoutes[url.pathname];
return route ? route() : null;
}

View File

@@ -46,7 +46,7 @@ import {
buildWebDashboardOverview, buildWebDashboardOverview,
sanitizeScheduledTask, sanitizeScheduledTask,
} from './web-dashboard-data.js'; } from './web-dashboard-data.js';
import { serveValidatedAttachment } from './web-dashboard-attachments.js'; import { handleSimpleGetRoute } from './web-dashboard-routes.js';
import { import {
addClaudeAccountFromToken, addClaudeAccountFromToken,
getActiveCodexSettingsIndex, getActiveCodexSettingsIndex,
@@ -1571,15 +1571,14 @@ export function createWebDashboardHandler(
return jsonResponse({ error: 'Method not allowed' }, { status: 405 }); return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
} }
const simpleGetRoutes: Record<string, () => Response> = { const simpleGetRoute = handleSimpleGetRoute({
'/api/health': () => jsonResponse({ ok: true }), url,
'/api/status-snapshots': () => statusMaxAgeMs,
jsonResponse(readSnapshots(statusMaxAgeMs)), readSnapshots,
'/api/tasks': () => jsonResponse(loadTasks().map(sanitizeScheduledTask)), loadTasks,
'/api/attachments': () => serveValidatedAttachment(url), jsonResponse,
}; });
const simpleGetRoute = simpleGetRoutes[url.pathname]; if (simpleGetRoute) return simpleGetRoute;
if (simpleGetRoute) return simpleGetRoute();
if (url.pathname === '/api/overview') { if (url.pathname === '/api/overview') {
const snapshots = readSnapshots(statusMaxAgeMs); const snapshots = readSnapshots(statusMaxAgeMs);