Extract dashboard room timeline routes

This commit is contained in:
ejclaw
2026-04-28 18:39:46 +09:00
parent 4a2a2fafbb
commit 46e6ab7a51
3 changed files with 625 additions and 242 deletions

View File

@@ -0,0 +1,256 @@
import { gunzipSync } from 'node:zlib';
import { describe, expect, it } from 'vitest';
import type { PairedTurnAttemptRecord, PairedTurnRecord } from './db.js';
import type { StatusSnapshot } from './status-dashboard.js';
import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js';
import {
handleRoomTimelineRoute,
type RoomsTimelineRouteDependencies,
} from './web-dashboard-room-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 makePairedTask(overrides: Partial<PairedTask> = {}): PairedTask {
return {
id: 'paired-room-1',
chat_jid: 'dc:ops',
group_folder: 'ops-room',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude-reviewer',
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
arbiter_agent_type: 'codex',
title: 'Dashboard PR',
source_ref: null,
plan_notes: null,
review_requested_at: null,
round_trip_count: 2,
owner_failure_count: 0,
owner_step_done_streak: 0,
finalize_step_done_count: 0,
task_done_then_user_reopen_count: 0,
empty_step_done_streak: 0,
status: 'in_review',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-04-26T04:00:00.000Z',
updated_at: '2026-04-26T05:20:00.000Z',
...overrides,
};
}
function snapshot(updatedAt = '2026-04-26T05:22:00.000Z'): StatusSnapshot {
return {
serviceId: 'codex-main',
agentType: 'codex',
assistantName: 'Codex',
updatedAt,
entries: [
{
jid: 'dc:ops',
name: '#ops',
folder: 'ops-room',
agentType: 'codex',
status: 'processing',
elapsedMs: 120_000,
pendingMessages: true,
pendingTasks: 1,
},
],
};
}
function roomDeps(
overrides: Partial<RoomsTimelineRouteDependencies> = {},
): RoomsTimelineRouteDependencies {
return {
statusMaxAgeMs: 1234,
readSnapshots: () => [snapshot()],
loadLatestPairedTaskForChat: () => null,
loadPairedTurnsForTask: () => [],
loadLatestPairedTurnForTask: () => null,
loadPairedTurnAttempts: () => [],
loadPairedTurnOutputs: () => [],
loadRecentPairedTurnOutputsForChat: () => [],
loadRecentChatMessages: () => [],
...overrides,
};
}
describe('web dashboard room routes', () => {
it('serves room timeline with paired progress and recent messages', async () => {
const pairedTask = makePairedTask();
const turns: PairedTurnRecord[] = [
{
turn_id: 'paired-room-1:reviewer-turn',
task_id: pairedTask.id,
task_updated_at: pairedTask.updated_at,
role: 'reviewer',
intent_kind: 'reviewer-turn',
state: 'queued',
executor_service_id: null,
executor_agent_type: null,
attempt_no: 0,
created_at: '2026-04-26T05:18:30.000Z',
updated_at: '2026-04-26T05:21:00.000Z',
completed_at: null,
last_error: null,
},
];
const attempts: PairedTurnAttemptRecord[] = [
{
attempt_id: 'paired-room-1:reviewer-turn:attempt:2',
parent_attempt_id: null,
parent_handoff_id: null,
continuation_handoff_id: null,
turn_id: 'paired-room-1:reviewer-turn',
task_id: pairedTask.id,
task_updated_at: pairedTask.updated_at,
role: 'reviewer',
intent_kind: 'reviewer-turn',
state: 'running',
executor_service_id: 'claude-reviewer',
executor_agent_type: 'claude-code',
active_run_id: 'run-reviewer-1',
attempt_no: 2,
created_at: '2026-04-26T05:19:00.000Z',
updated_at: '2026-04-26T05:21:00.000Z',
completed_at: null,
last_error: 'OPENAI_API_KEY=plain-secret-value',
},
];
const outputs: PairedTurnOutput[] = [
{
id: 1,
task_id: pairedTask.id,
turn_number: 1,
role: 'owner',
output_text: 'owner final output',
verdict: 'step_done',
created_at: '2026-04-26T05:18:00.000Z',
},
];
const messages: NewMessage[] = [
{
id: 'msg-1',
chat_jid: 'dc:ops',
sender: 'u1',
sender_name: '눈쟁이',
content: '진행 어디까지야? BOT_TOKEN=plain-secret-value',
timestamp: '2026-04-26T05:17:00.000Z',
is_from_me: false,
is_bot_message: false,
message_source_kind: 'human',
},
];
const progressMessages: NewMessage[] = [
...messages,
{
id: 'msg-progress',
chat_jid: 'dc:ops',
sender: 'reviewer-bot',
sender_name: '리뷰어',
content: 'checking current output\n\n9s',
timestamp: '2026-04-26T05:20:00.000Z',
is_from_me: true,
is_bot_message: true,
message_source_kind: 'bot',
},
];
const requestedMessageLimits: number[] = [];
const response = handleRoomTimelineRoute({
url: new URL(
`http://localhost/api/rooms/${encodeURIComponent('dc:ops')}/timeline`,
),
request: new Request('http://localhost'),
jsonResponse,
...roomDeps({
loadLatestPairedTaskForChat: () => pairedTask,
loadPairedTurnsForTask: () => turns,
loadPairedTurnAttempts: () => attempts,
loadPairedTurnOutputs: () => outputs,
loadRecentChatMessages: (_jid, limit) => {
requestedMessageLimits.push(limit ?? 20);
return limit && limit > 8 ? progressMessages : messages;
},
}),
});
expect(response?.status).toBe(200);
const body = (await response?.json()) as {
pairedTask: {
currentTurn: { lastError: string; progressText: string };
outputs: Array<{ outputText: string }>;
};
messages: Array<{ content: string; senderName: string }>;
};
expect(body.pairedTask.currentTurn).toMatchObject({
lastError: 'OPENAI_API_KEY=<redacted>',
progressText: 'checking current output',
});
expect(body.pairedTask.outputs).toMatchObject([
{ outputText: 'owner final output' },
]);
expect(requestedMessageLimits).toEqual([8, 40]);
expect(body.messages[0]).toMatchObject({
content: expect.stringContaining('BOT_TOKEN=<redacted>'),
senderName: '눈쟁이',
});
});
it('serves cached room timelines and falls through outside room routes', async () => {
const pairedTask = makePairedTask({ id: 'paired-cache-1' });
const response = handleRoomTimelineRoute({
url: new URL('http://localhost/api/rooms-timeline'),
request: new Request('http://localhost/api/rooms-timeline', {
headers: { 'accept-encoding': 'gzip' },
}),
jsonResponse,
...roomDeps({
readSnapshots: () => [snapshot('2026-04-26T06:22:00.000Z')],
loadLatestPairedTaskForChat: () => pairedTask,
loadLatestPairedTurnForTask: () => null,
loadRecentChatMessages: () => [
{
id: 'msg-cache',
chat_jid: 'dc:ops',
sender: 'u1',
sender_name: '눈쟁이',
content: 'cache message',
timestamp: '2026-04-26T06:17:00.000Z',
is_from_me: false,
is_bot_message: false,
message_source_kind: 'human',
},
],
}),
});
expect(response?.status).toBe(200);
expect(response?.headers.get('content-encoding')).toBe('gzip');
const bytes = new Uint8Array(await response!.arrayBuffer());
const body = JSON.parse(
new TextDecoder().decode(gunzipSync(bytes)),
) as Record<string, { pairedTask: { id: string } }>;
expect(body['dc:ops']?.pairedTask.id).toBe('paired-cache-1');
expect(
handleRoomTimelineRoute({
url: new URL('http://localhost/api/overview'),
request: new Request('http://localhost/api/overview'),
jsonResponse,
...roomDeps(),
}),
).toBeNull();
});
});

View File

@@ -0,0 +1,345 @@
import { gzipSync } from 'node:zlib';
import type { PairedTurnAttemptRecord, PairedTurnRecord } from './db.js';
import type { StatusSnapshot } from './status-dashboard.js';
import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js';
import {
buildWebDashboardRoomActivity,
type WebDashboardRoomActivity,
} from './web-dashboard-data.js';
type JsonResponse = (
value: unknown,
init?: ResponseInit,
request?: Request,
) => Response;
export interface RoomsTimelineRouteDependencies {
statusMaxAgeMs: number;
readSnapshots: (maxAgeMs: number) => StatusSnapshot[];
loadLatestPairedTaskForChat: (
chatJid: string,
) => PairedTask | null | undefined;
loadPairedTurnsForTask: (taskId: string) => PairedTurnRecord[];
loadLatestPairedTurnForTask: (
taskId: string,
) => PairedTurnRecord | null | undefined;
loadPairedTurnAttempts: (turnId: string) => PairedTurnAttemptRecord[];
loadPairedTurnOutputs: (taskId: string) => PairedTurnOutput[];
loadRecentPairedTurnOutputsForChat: (
chatJid: string,
limit: number,
) => PairedTurnOutput[];
loadRecentChatMessages: (chatJid: string, limit?: number) => NewMessage[];
}
interface RoomsTimelineRouteContext extends RoomsTimelineRouteDependencies {
url: URL;
request: Request;
jsonResponse: JsonResponse;
}
interface RoomsTimelineCache {
key: string;
builtAt: number;
rawJson: string;
gzipBuffer: Uint8Array;
}
const ROOMS_TIMELINE_BG_INTERVAL_MS = 2000;
let roomsTimelineCache: RoomsTimelineCache | null = null;
function parseRoomTimelinePath(pathname: string): string | null {
const match = pathname.match(/^\/api\/rooms\/([^/]+)\/timeline$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
function isReadMethod(method: string): boolean {
return method === 'GET' || method === 'HEAD';
}
function latestSnapshotEntry(
snapshots: StatusSnapshot[],
jid: string,
):
| { snapshot: StatusSnapshot; entry: StatusSnapshot['entries'][number] }
| undefined {
return snapshots
.flatMap((snapshot) =>
snapshot.entries
.filter((entry) => entry.jid === jid)
.map((entry) => ({ snapshot, entry })),
)
.sort((a, b) => b.snapshot.updatedAt.localeCompare(a.snapshot.updatedAt))
.at(0);
}
function buildRoomActivity(
deps: RoomsTimelineRouteDependencies,
snapshot: StatusSnapshot,
entry: StatusSnapshot['entries'][number],
): WebDashboardRoomActivity {
const pairedTask = deps.loadLatestPairedTaskForChat(entry.jid) ?? null;
const turns = pairedTask ? deps.loadPairedTurnsForTask(pairedTask.id) : [];
const attempts = turns.flatMap((turn) =>
deps.loadPairedTurnAttempts(turn.turn_id),
);
return buildWebDashboardRoomActivity({
serviceId: snapshot.serviceId,
entry,
pairedTask,
turns,
attempts,
outputs: pairedTask ? deps.loadPairedTurnOutputs(pairedTask.id) : [],
messages: deps.loadRecentChatMessages(entry.jid, 8),
progressMessages: deps.loadRecentChatMessages(entry.jid, 40),
});
}
export function buildRoomsTimelineResult(
deps: RoomsTimelineRouteDependencies,
): Record<string, WebDashboardRoomActivity> {
const snapshots = deps.readSnapshots(deps.statusMaxAgeMs);
const uniqueByJid = new Map<
string,
{
snapshot: StatusSnapshot;
entry: StatusSnapshot['entries'][number];
}
>();
for (const snapshot of snapshots) {
for (const entry of snapshot.entries) {
const existing = uniqueByJid.get(entry.jid);
if (
!existing ||
existing.snapshot.updatedAt.localeCompare(snapshot.updatedAt) < 0
) {
uniqueByJid.set(entry.jid, { snapshot, entry });
}
}
}
const result: Record<string, WebDashboardRoomActivity> = {};
for (const [jid, { snapshot, entry }] of uniqueByJid) {
const pairedTask = deps.loadLatestPairedTaskForChat(jid) ?? null;
const messages = deps.loadRecentChatMessages(jid, 8);
const progressMessages = pairedTask
? deps.loadRecentChatMessages(jid, 40)
: messages;
const outputs = deps.loadRecentPairedTurnOutputsForChat(jid, 8);
if (!pairedTask && messages.length === 0 && outputs.length === 0) continue;
const latestTurn = pairedTask
? deps.loadLatestPairedTurnForTask(pairedTask.id)
: null;
const turns = latestTurn ? [latestTurn] : [];
result[jid] = buildWebDashboardRoomActivity({
serviceId: snapshot.serviceId,
entry,
pairedTask,
turns,
attempts: [],
outputs,
messages,
progressMessages,
outputLimit: 8,
});
}
return result;
}
function computeRoomsCacheKey(deps: RoomsTimelineRouteDependencies): string {
return deps
.readSnapshots(deps.statusMaxAgeMs)
.map((s) => s.updatedAt)
.sort()
.join('|');
}
export function ensureRoomsTimelineCache(
deps: RoomsTimelineRouteDependencies,
): RoomsTimelineCache {
const key = computeRoomsCacheKey(deps);
if (roomsTimelineCache && roomsTimelineCache.key === key) {
return roomsTimelineCache;
}
const result = buildRoomsTimelineResult(deps);
const rawJson = JSON.stringify(result);
const gzipBuffer = gzipSync(new TextEncoder().encode(rawJson));
roomsTimelineCache = {
key,
builtAt: Date.now(),
rawJson,
gzipBuffer,
};
return roomsTimelineCache;
}
export function startRoomsTimelineCacheRefresh(
deps: RoomsTimelineRouteDependencies,
): void {
setTimeout(() => {
try {
ensureRoomsTimelineCache(deps);
} catch {
/* warm-up failure is non-fatal */
}
}, 0);
setInterval(() => {
try {
ensureRoomsTimelineCache(deps);
} catch {
/* refresh failure is non-fatal */
}
}, ROOMS_TIMELINE_BG_INTERVAL_MS).unref();
}
function createRoomsTimelineStream(
deps: RoomsTimelineRouteDependencies,
request: Request,
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
let lastBuiltAt = 0;
let closed = false;
const enqueue = (chunk: string) => {
if (closed) return;
try {
controller.enqueue(encoder.encode(chunk));
} catch {
closed = true;
}
};
enqueue(`retry: 3000\n\n`);
try {
const cache = ensureRoomsTimelineCache(deps);
lastBuiltAt = cache.builtAt;
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
} catch {
/* warm-up failure is non-fatal */
}
const tick = () => {
if (closed) return;
try {
const cache = ensureRoomsTimelineCache(deps);
if (cache.builtAt !== lastBuiltAt) {
lastBuiltAt = cache.builtAt;
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
} else {
enqueue(`: ping ${Date.now()}\n\n`);
}
} catch {
/* skip this tick */
}
};
const interval = setInterval(tick, 1500);
const close = () => {
if (closed) return;
closed = true;
clearInterval(interval);
try {
controller.close();
} catch {
/* already closed */
}
};
request.signal.addEventListener('abort', close);
},
});
}
function handleRoomsStream(
deps: RoomsTimelineRouteDependencies,
request: Request,
jsonResponse: JsonResponse,
): Response {
if (!isReadMethod(request.method)) {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
return new Response(createRoomsTimelineStream(deps, request), {
headers: {
'content-type': 'text/event-stream; charset=utf-8',
'cache-control': 'no-cache, no-transform',
connection: 'keep-alive',
'x-accel-buffering': 'no',
},
});
}
function handleRoomsTimelineSnapshot(
deps: RoomsTimelineRouteDependencies,
request: Request,
jsonResponse: JsonResponse,
): Response {
if (!isReadMethod(request.method)) {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
const cache = ensureRoomsTimelineCache(deps);
const acceptsGzip =
request.headers.get('accept-encoding')?.includes('gzip') ?? false;
if (acceptsGzip) {
return new Response(cache.gzipBuffer, {
headers: {
'content-type': 'application/json; charset=utf-8',
'content-encoding': 'gzip',
vary: 'accept-encoding',
'x-cache-age': String(Date.now() - cache.builtAt),
},
});
}
return new Response(cache.rawJson, {
headers: {
'content-type': 'application/json; charset=utf-8',
'x-cache-age': String(Date.now() - cache.builtAt),
},
});
}
function handleSingleRoomTimeline({
request,
jsonResponse,
...deps
}: RoomsTimelineRouteContext & { timelineRoomJid: string }): Response {
if (!isReadMethod(request.method)) {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
const snapshots = deps.readSnapshots(deps.statusMaxAgeMs);
const matched = latestSnapshotEntry(snapshots, deps.timelineRoomJid);
if (!matched) {
return jsonResponse({ error: 'Room timeline not found' }, { status: 404 });
}
return jsonResponse(buildRoomActivity(deps, matched.snapshot, matched.entry));
}
export function handleRoomTimelineRoute(
context: RoomsTimelineRouteContext,
): Response | null {
const timelineRoomJid = parseRoomTimelinePath(context.url.pathname);
if (timelineRoomJid) {
return handleSingleRoomTimeline({ ...context, timelineRoomJid });
}
if (context.url.pathname === '/api/stream') {
return handleRoomsStream(context, context.request, context.jsonResponse);
}
if (context.url.pathname === '/api/rooms-timeline') {
return handleRoomsTimelineSnapshot(
context,
context.request,
context.jsonResponse,
);
}
return null;
}

View File

@@ -42,10 +42,14 @@ import type {
} from './types.js';
import { isWatchCiTask } from './task-watch-status.js';
import {
buildWebDashboardRoomActivity,
buildWebDashboardOverview,
sanitizeScheduledTask,
} from './web-dashboard-data.js';
import {
handleRoomTimelineRoute,
startRoomsTimelineCacheRefresh,
type RoomsTimelineRouteDependencies,
} from './web-dashboard-room-routes.js';
import { handleSimpleGetRoute } from './web-dashboard-routes.js';
import {
handleServiceRoute,
@@ -153,14 +157,6 @@ export interface StartedWebDashboardServer {
stop: () => void;
}
const ROOMS_TIMELINE_BG_INTERVAL_MS = 2000;
let roomsTimelineCache: {
key: string;
builtAt: number;
rawJson: string;
gzipBuffer: Uint8Array;
} | null = null;
function jsonResponse(
value: unknown,
init?: ResponseInit,
@@ -300,16 +296,6 @@ function parseRoomMessagePath(pathname: string): string | null {
}
}
function parseRoomTimelinePath(pathname: string): string | null {
const match = pathname.match(/^\/api\/rooms\/([^/]+)\/timeline$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
function parsePairedInboxTarget(
inboxId: string,
): { taskId: string; status: PairedTask['status'] } | null {
@@ -682,100 +668,20 @@ export function createWebDashboardHandler(
opts.restartServiceStack ?? restartEjclawStackServices;
const statusMaxAgeMs = opts.statusMaxAgeMs ?? DEFAULT_STATUS_MAX_AGE_MS;
function buildRoomsTimelineResult(): Record<
string,
ReturnType<typeof buildWebDashboardRoomActivity>
> {
const snapshots = readSnapshots(statusMaxAgeMs);
const uniqueByJid = new Map<
string,
{
snapshot: (typeof snapshots)[number];
entry: (typeof snapshots)[number]['entries'][number];
}
>();
for (const snapshot of snapshots) {
for (const entry of snapshot.entries) {
const existing = uniqueByJid.get(entry.jid);
if (
!existing ||
existing.snapshot.updatedAt.localeCompare(snapshot.updatedAt) < 0
) {
uniqueByJid.set(entry.jid, { snapshot, entry });
}
}
}
const result: Record<
string,
ReturnType<typeof buildWebDashboardRoomActivity>
> = {};
for (const [jid, { snapshot, entry }] of uniqueByJid) {
const pairedTask = loadLatestPairedTaskForChat(jid) ?? null;
const messages = loadRecentChatMessages(jid, 8);
const progressMessages = pairedTask
? loadRecentChatMessages(jid, 40)
: messages;
const outputs = loadRecentPairedTurnOutputsForChat(jid, 8);
if (!pairedTask && messages.length === 0 && outputs.length === 0)
continue;
const latestTurn = pairedTask
? loadLatestPairedTurnForTask(pairedTask.id)
: null;
const turns = latestTurn ? [latestTurn] : [];
result[jid] = buildWebDashboardRoomActivity({
serviceId: snapshot.serviceId,
entry,
pairedTask,
turns,
attempts: [],
outputs,
messages,
progressMessages,
outputLimit: 8,
});
}
return result;
}
function computeRoomsCacheKey(): string {
return readSnapshots(statusMaxAgeMs)
.map((s) => s.updatedAt)
.sort()
.join('|');
}
function ensureRoomsTimelineCache(): NonNullable<typeof roomsTimelineCache> {
const key = computeRoomsCacheKey();
if (roomsTimelineCache && roomsTimelineCache.key === key) {
return roomsTimelineCache;
}
const result = buildRoomsTimelineResult();
const rawJson = JSON.stringify(result);
const gzipBuffer = Bun.gzipSync(new TextEncoder().encode(rawJson));
roomsTimelineCache = {
key,
builtAt: Date.now(),
rawJson,
gzipBuffer,
};
return roomsTimelineCache;
}
const roomsTimelineDeps: RoomsTimelineRouteDependencies = {
statusMaxAgeMs,
readSnapshots,
loadLatestPairedTaskForChat,
loadPairedTurnsForTask,
loadLatestPairedTurnForTask,
loadPairedTurnAttempts,
loadPairedTurnOutputs,
loadRecentPairedTurnOutputsForChat,
loadRecentChatMessages,
};
if (opts.startBackgroundCacheRefresh !== false) {
setTimeout(() => {
try {
ensureRoomsTimelineCache();
} catch {
/* warm-up failure is non-fatal */
}
}, 0);
setInterval(() => {
try {
ensureRoomsTimelineCache();
} catch {
/* refresh failure is non-fatal */
}
}, ROOMS_TIMELINE_BG_INTERVAL_MS).unref();
startRoomsTimelineCacheRefresh(roomsTimelineDeps);
}
const seenRoomMessageIds: string[] = [];
@@ -811,7 +717,6 @@ export function createWebDashboardHandler(
const taskId = parseTaskPath(url.pathname);
const actionInboxId = parseInboxActionPath(url.pathname);
const messageRoomJid = parseRoomMessagePath(url.pathname);
const timelineRoomJid = parseRoomTimelinePath(url.pathname);
if (actionTaskId) {
if (request.method !== 'POST') {
@@ -1061,47 +966,13 @@ export function createWebDashboardHandler(
return jsonResponse({ ok: true, id, queued: true });
}
if (timelineRoomJid) {
if (request.method !== 'GET' && request.method !== 'HEAD') {
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
}
const snapshots = readSnapshots(statusMaxAgeMs);
const matched = snapshots
.flatMap((snapshot) =>
snapshot.entries
.filter((entry) => entry.jid === timelineRoomJid)
.map((entry) => ({ snapshot, entry })),
)
.sort((a, b) =>
b.snapshot.updatedAt.localeCompare(a.snapshot.updatedAt),
)
.at(0);
if (!matched) {
return jsonResponse(
{ error: 'Room timeline not found' },
{ status: 404 },
);
}
const pairedTask = loadLatestPairedTaskForChat(timelineRoomJid) ?? null;
const turns = pairedTask ? loadPairedTurnsForTask(pairedTask.id) : [];
const attempts = turns.flatMap((turn) =>
loadPairedTurnAttempts(turn.turn_id),
);
return jsonResponse(
buildWebDashboardRoomActivity({
serviceId: matched.snapshot.serviceId,
entry: matched.entry,
pairedTask,
turns,
attempts,
outputs: pairedTask ? loadPairedTurnOutputs(pairedTask.id) : [],
messages: loadRecentChatMessages(timelineRoomJid, 8),
progressMessages: loadRecentChatMessages(timelineRoomJid, 40),
}),
);
}
const roomTimelineRoute = handleRoomTimelineRoute({
url,
request,
jsonResponse,
...roomsTimelineDeps,
});
if (roomTimelineRoute) return roomTimelineRoute;
const serviceRoute = await handleServiceRoute({
url,
@@ -1312,95 +1183,6 @@ export function createWebDashboardHandler(
});
}
if (url.pathname === '/api/stream') {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
let lastBuiltAt = 0;
let closed = false;
const enqueue = (chunk: string) => {
if (closed) return;
try {
controller.enqueue(encoder.encode(chunk));
} catch {
closed = true;
}
};
// Initial: send retry hint and seed event
enqueue(`retry: 3000\n\n`);
try {
const cache = ensureRoomsTimelineCache();
lastBuiltAt = cache.builtAt;
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
} catch {
/* warm-up failure is non-fatal */
}
const tick = () => {
if (closed) return;
try {
const cache = ensureRoomsTimelineCache();
if (cache.builtAt !== lastBuiltAt) {
lastBuiltAt = cache.builtAt;
enqueue(`event: rooms-timeline\ndata: ${cache.rawJson}\n\n`);
} else {
// heartbeat to keep connection alive through proxies
enqueue(`: ping ${Date.now()}\n\n`);
}
} catch {
/* skip this tick */
}
};
const interval = setInterval(tick, 1500);
const close = () => {
if (closed) return;
closed = true;
clearInterval(interval);
try {
controller.close();
} catch {
/* already closed */
}
};
request.signal.addEventListener('abort', close);
},
});
return new Response(stream, {
headers: {
'content-type': 'text/event-stream; charset=utf-8',
'cache-control': 'no-cache, no-transform',
connection: 'keep-alive',
'x-accel-buffering': 'no',
},
});
}
if (url.pathname === '/api/rooms-timeline') {
const cache = ensureRoomsTimelineCache();
const acceptsGzip =
request.headers.get('accept-encoding')?.includes('gzip') ?? false;
if (acceptsGzip) {
return new Response(cache.gzipBuffer, {
headers: {
'content-type': 'application/json; charset=utf-8',
'content-encoding': 'gzip',
vary: 'accept-encoding',
'x-cache-age': String(Date.now() - cache.builtAt),
},
});
}
return new Response(cache.rawJson, {
headers: {
'content-type': 'application/json; charset=utf-8',
'x-cache-age': String(Date.now() - cache.builtAt),
},
});
}
if (url.pathname.startsWith('/api/')) {
return jsonResponse({ error: 'Not found' }, { status: 404 });
}