feat(dashboard): expose inbox activity data (#22)
This commit is contained in:
@@ -86,6 +86,7 @@ export {
|
||||
createServiceHandoff,
|
||||
failPairedTurn,
|
||||
failServiceHandoff,
|
||||
getAllOpenPairedTasks,
|
||||
getAllChannelOwnerLeases,
|
||||
getAllPendingServiceHandoffs,
|
||||
getChannelOwnerLease,
|
||||
|
||||
@@ -266,6 +266,22 @@ export function getLatestOpenPairedTaskForChatFromDatabase(
|
||||
return row ? hydratePairedTaskRow(database, row) : undefined;
|
||||
}
|
||||
|
||||
export function getAllOpenPairedTasksFromDatabase(
|
||||
database: Database,
|
||||
): PairedTask[] {
|
||||
const rows = database
|
||||
.prepare(
|
||||
`
|
||||
SELECT *
|
||||
FROM paired_tasks
|
||||
WHERE status NOT IN ('completed')
|
||||
ORDER BY updated_at DESC, created_at DESC
|
||||
`,
|
||||
)
|
||||
.all() as StoredPairedTaskRow[];
|
||||
return rows.map((row) => hydratePairedTaskRow(database, row));
|
||||
}
|
||||
|
||||
export function getLatestPreviousPairedTaskForChatFromDatabase(
|
||||
database: Database,
|
||||
chatJid: string,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
clearPairedTurnReservationsInDatabase,
|
||||
type PairedTaskUpdates,
|
||||
createPairedTaskInDatabase,
|
||||
getAllOpenPairedTasksFromDatabase,
|
||||
getLastBotFinalMessageFromDatabase,
|
||||
getLatestOpenPairedTaskForChatFromDatabase,
|
||||
getLatestPreviousPairedTaskForChatFromDatabase,
|
||||
@@ -113,6 +114,10 @@ export function getLatestPreviousPairedTaskForChat(
|
||||
);
|
||||
}
|
||||
|
||||
export function getAllOpenPairedTasks(): PairedTask[] {
|
||||
return getAllOpenPairedTasksFromDatabase(requireDatabase());
|
||||
}
|
||||
|
||||
export function updatePairedTask(id: string, updates: PairedTaskUpdates): void {
|
||||
updatePairedTaskInDatabase(requireDatabase(), id, updates);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { StatusSnapshot } from './status-dashboard.js';
|
||||
import type { ScheduledTask } from './types.js';
|
||||
import type { PairedTask, ScheduledTask } from './types.js';
|
||||
import {
|
||||
buildWebDashboardOverview,
|
||||
sanitizeScheduledTask,
|
||||
@@ -28,6 +28,36 @@ function makeTask(overrides: Partial<ScheduledTask>): ScheduledTask {
|
||||
};
|
||||
}
|
||||
|
||||
function makePairedTask(overrides: Partial<PairedTask>): PairedTask {
|
||||
return {
|
||||
id: 'paired-1',
|
||||
chat_jid: 'dc:general',
|
||||
group_folder: 'general',
|
||||
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: 0,
|
||||
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: 'review_ready',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: null,
|
||||
created_at: '2026-04-26T04:00:00.000Z',
|
||||
updated_at: '2026-04-26T04:30:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('web dashboard data', () => {
|
||||
it('builds overview counts from status snapshots and scheduled tasks', () => {
|
||||
const snapshots: StatusSnapshot[] = [
|
||||
@@ -199,4 +229,114 @@ describe('web dashboard data', () => {
|
||||
);
|
||||
expect(sanitized.promptPreview).not.toContain('plain-secret-value');
|
||||
});
|
||||
|
||||
it('builds typed inbox items from pending rooms, paired tasks, and CI failures', () => {
|
||||
const snapshots: StatusSnapshot[] = [
|
||||
{
|
||||
serviceId: 'codex-main',
|
||||
agentType: 'codex',
|
||||
assistantName: 'Codex',
|
||||
updatedAt: '2026-04-26T05:00:00.000Z',
|
||||
entries: [
|
||||
{
|
||||
jid: 'dc:ops',
|
||||
name: '#ops',
|
||||
folder: 'ops',
|
||||
agentType: 'codex',
|
||||
status: 'waiting',
|
||||
elapsedMs: 1000,
|
||||
pendingMessages: true,
|
||||
pendingTasks: 0,
|
||||
},
|
||||
{
|
||||
jid: 'dc:idle',
|
||||
name: '#idle',
|
||||
folder: 'idle',
|
||||
agentType: 'codex',
|
||||
status: 'inactive',
|
||||
elapsedMs: null,
|
||||
pendingMessages: false,
|
||||
pendingTasks: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const overview = buildWebDashboardOverview({
|
||||
now: '2026-04-26T05:10:00.000Z',
|
||||
snapshots,
|
||||
pairedTasks: [
|
||||
makePairedTask({
|
||||
id: 'review-1',
|
||||
status: 'review_ready',
|
||||
review_requested_at: '2026-04-26T05:03:00.000Z',
|
||||
}),
|
||||
makePairedTask({
|
||||
id: 'merge-1',
|
||||
status: 'merge_ready',
|
||||
title: 'Ready to merge',
|
||||
updated_at: '2026-04-26T05:04:00.000Z',
|
||||
}),
|
||||
makePairedTask({
|
||||
id: 'done-1',
|
||||
status: 'completed',
|
||||
}),
|
||||
],
|
||||
tasks: [
|
||||
makeTask({
|
||||
id: 'ci-1',
|
||||
prompt:
|
||||
'[BACKGROUND CI WATCH]\nWatch target:\nPR #21\n\nCheck instructions:\nwatch',
|
||||
last_run: '2026-04-26T05:05:00.000Z',
|
||||
last_result: 'Error: BOT_TOKEN=plain-secret-value failed',
|
||||
status: 'paused',
|
||||
}),
|
||||
makeTask({
|
||||
id: 'cron-1',
|
||||
prompt: 'regular cron',
|
||||
last_run: '2026-04-26T05:06:00.000Z',
|
||||
last_result: 'Error: non-watch task failed',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(overview.inbox.map((item) => item.kind)).toEqual([
|
||||
'ci-failure',
|
||||
'approval',
|
||||
'reviewer-request',
|
||||
'pending-room',
|
||||
]);
|
||||
expect(overview.inbox).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'room:codex-main:dc:ops',
|
||||
kind: 'pending-room',
|
||||
severity: 'info',
|
||||
roomJid: 'dc:ops',
|
||||
serviceId: 'codex-main',
|
||||
occurredAt: '2026-04-26T05:00:00.000Z',
|
||||
createdAt: '2026-04-26T05:10:00.000Z',
|
||||
}),
|
||||
);
|
||||
expect(overview.inbox).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'paired:review-1:review_ready',
|
||||
kind: 'reviewer-request',
|
||||
severity: 'warn',
|
||||
serviceId: 'claude-reviewer',
|
||||
taskStatus: 'review_ready',
|
||||
occurredAt: '2026-04-26T05:03:00.000Z',
|
||||
}),
|
||||
);
|
||||
const ciFailure = overview.inbox.find((item) => item.kind === 'ci-failure');
|
||||
expect(ciFailure).toMatchObject({
|
||||
id: 'ci:ci-1',
|
||||
severity: 'error',
|
||||
source: 'scheduled-task',
|
||||
taskId: 'ci-1',
|
||||
});
|
||||
expect(ciFailure?.summary).toContain('BOT_TOKEN=<redacted>');
|
||||
expect(ciFailure?.summary).not.toContain('plain-secret-value');
|
||||
expect(overview.inbox.some((item) => item.taskId === 'cron-1')).toBe(false);
|
||||
expect(overview.inbox.some((item) => item.taskId === 'done-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isWatchCiTask } from './task-watch-status.js';
|
||||
import type { StatusSnapshot, UsageRowSnapshot } from './status-dashboard.js';
|
||||
import type { ScheduledTask } from './types.js';
|
||||
import type { PairedTask, ScheduledTask } from './types.js';
|
||||
|
||||
export interface SanitizedScheduledTask {
|
||||
id: string;
|
||||
@@ -54,6 +54,34 @@ export interface WebDashboardOverview {
|
||||
rows: UsageRowSnapshot[];
|
||||
fetchedAt: string | null;
|
||||
};
|
||||
inbox: InboxItem[];
|
||||
}
|
||||
|
||||
export type InboxItemKind =
|
||||
| 'pending-room'
|
||||
| 'reviewer-request'
|
||||
| 'approval'
|
||||
| 'arbiter-request'
|
||||
| 'ci-failure'
|
||||
| 'mention';
|
||||
|
||||
export type InboxItemSeverity = 'info' | 'warn' | 'error';
|
||||
|
||||
export interface InboxItem {
|
||||
id: string;
|
||||
kind: InboxItemKind;
|
||||
severity: InboxItemSeverity;
|
||||
title: string;
|
||||
summary: string;
|
||||
occurredAt: string;
|
||||
createdAt: string;
|
||||
source: 'status-snapshot' | 'paired-task' | 'scheduled-task';
|
||||
roomJid?: string;
|
||||
roomName?: string;
|
||||
groupFolder?: string;
|
||||
serviceId?: string;
|
||||
taskId?: string;
|
||||
taskStatus?: string;
|
||||
}
|
||||
|
||||
const SECRET_ASSIGNMENT_RE =
|
||||
@@ -76,6 +104,10 @@ function buildPromptPreview(prompt: string): string {
|
||||
return truncateText(redactSensitiveText(prompt).replace(/\s+/g, ' ').trim());
|
||||
}
|
||||
|
||||
function buildInboxPreview(value: string): string {
|
||||
return truncateText(redactSensitiveText(value).replace(/\s+/g, ' ').trim());
|
||||
}
|
||||
|
||||
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
|
||||
const rows: UsageRowSnapshot[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -96,6 +128,134 @@ function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
|
||||
return rows;
|
||||
}
|
||||
|
||||
function failedTaskResult(task: ScheduledTask): string | null {
|
||||
if (!task.last_result) return null;
|
||||
const normalized = task.last_result.toLowerCase();
|
||||
if (
|
||||
normalized.includes('fail') ||
|
||||
normalized.includes('error') ||
|
||||
normalized.includes('timeout') ||
|
||||
normalized.includes('cancel') ||
|
||||
normalized.includes('reject')
|
||||
) {
|
||||
return buildInboxPreview(task.last_result);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pairedTaskInboxKind(
|
||||
status: PairedTask['status'],
|
||||
): InboxItemKind | null {
|
||||
if (status === 'merge_ready') return 'approval';
|
||||
if (status === 'review_ready' || status === 'in_review') {
|
||||
return 'reviewer-request';
|
||||
}
|
||||
if (status === 'arbiter_requested' || status === 'in_arbitration') {
|
||||
return 'arbiter-request';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectInboxItems(args: {
|
||||
snapshots: StatusSnapshot[];
|
||||
tasks: ScheduledTask[];
|
||||
pairedTasks: PairedTask[];
|
||||
createdAt: string;
|
||||
}): InboxItem[] {
|
||||
const items: InboxItem[] = [];
|
||||
|
||||
for (const snapshot of args.snapshots) {
|
||||
for (const entry of snapshot.entries) {
|
||||
if (!entry.pendingMessages && entry.pendingTasks === 0) continue;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (entry.pendingMessages) parts.push('pending messages');
|
||||
if (entry.pendingTasks > 0) parts.push(`${entry.pendingTasks} tasks`);
|
||||
|
||||
items.push({
|
||||
id: `room:${snapshot.serviceId}:${entry.jid}`,
|
||||
kind: 'pending-room',
|
||||
severity: entry.pendingTasks > 0 ? 'warn' : 'info',
|
||||
title: entry.name || entry.folder || entry.jid,
|
||||
summary: parts.join(' · '),
|
||||
occurredAt: snapshot.updatedAt,
|
||||
createdAt: args.createdAt,
|
||||
source: 'status-snapshot',
|
||||
roomJid: entry.jid,
|
||||
roomName: entry.name,
|
||||
groupFolder: entry.folder,
|
||||
serviceId: snapshot.serviceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of args.pairedTasks) {
|
||||
const kind = pairedTaskInboxKind(task.status);
|
||||
if (!kind) continue;
|
||||
|
||||
items.push({
|
||||
id: `paired:${task.id}:${task.status}`,
|
||||
kind,
|
||||
severity:
|
||||
kind === 'approval' || kind === 'arbiter-request' ? 'error' : 'warn',
|
||||
title: task.title || task.group_folder,
|
||||
summary: task.status,
|
||||
occurredAt:
|
||||
kind === 'arbiter-request'
|
||||
? (task.arbiter_requested_at ?? task.updated_at)
|
||||
: kind === 'reviewer-request'
|
||||
? (task.review_requested_at ?? task.updated_at)
|
||||
: task.updated_at,
|
||||
createdAt: args.createdAt,
|
||||
source: 'paired-task',
|
||||
roomJid: task.chat_jid,
|
||||
groupFolder: task.group_folder,
|
||||
serviceId:
|
||||
kind === 'reviewer-request'
|
||||
? task.reviewer_service_id
|
||||
: task.owner_service_id,
|
||||
taskId: task.id,
|
||||
taskStatus: task.status,
|
||||
});
|
||||
}
|
||||
|
||||
for (const task of args.tasks) {
|
||||
if (!isWatchCiTask(task)) continue;
|
||||
const result = failedTaskResult(task);
|
||||
if (!result) continue;
|
||||
|
||||
items.push({
|
||||
id: `ci:${task.id}`,
|
||||
kind: 'ci-failure',
|
||||
severity: task.status === 'paused' ? 'error' : 'warn',
|
||||
title: 'CI watcher failed',
|
||||
summary: result,
|
||||
occurredAt: task.last_run ?? task.created_at,
|
||||
createdAt: args.createdAt,
|
||||
source: 'scheduled-task',
|
||||
roomJid: task.chat_jid,
|
||||
groupFolder: task.group_folder,
|
||||
serviceId: task.agent_type ?? undefined,
|
||||
taskId: task.id,
|
||||
taskStatus: task.status,
|
||||
});
|
||||
}
|
||||
|
||||
const severityRank: Record<InboxItemSeverity, number> = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
};
|
||||
|
||||
return items
|
||||
.sort((a, b) => {
|
||||
const severityDelta = severityRank[a.severity] - severityRank[b.severity];
|
||||
if (severityDelta !== 0) return severityDelta;
|
||||
return b.occurredAt.localeCompare(a.occurredAt);
|
||||
})
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
export function sanitizeScheduledTask(
|
||||
task: ScheduledTask,
|
||||
): SanitizedScheduledTask {
|
||||
@@ -125,7 +285,9 @@ export function buildWebDashboardOverview(args: {
|
||||
now?: string;
|
||||
snapshots: StatusSnapshot[];
|
||||
tasks: ScheduledTask[];
|
||||
pairedTasks?: PairedTask[];
|
||||
}): WebDashboardOverview {
|
||||
const generatedAt = args.now ?? new Date().toISOString();
|
||||
const rooms = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
@@ -191,7 +353,7 @@ export function buildWebDashboardOverview(args: {
|
||||
.at(-1) ?? null;
|
||||
|
||||
return {
|
||||
generatedAt: args.now ?? new Date().toISOString(),
|
||||
generatedAt,
|
||||
services,
|
||||
rooms,
|
||||
tasks,
|
||||
@@ -199,5 +361,11 @@ export function buildWebDashboardOverview(args: {
|
||||
rows: usageRows,
|
||||
fetchedAt: usageFetchedAt,
|
||||
},
|
||||
inbox: collectInboxItems({
|
||||
snapshots: args.snapshots,
|
||||
tasks: args.tasks,
|
||||
pairedTasks: args.pairedTasks ?? [],
|
||||
createdAt: generatedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,63 @@ import path from 'path';
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { StatusSnapshot } from './status-dashboard.js';
|
||||
import type { PairedTask, ScheduledTask } from './types.js';
|
||||
import { createWebDashboardHandler } from './web-dashboard-server.js';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function makePairedTask(overrides: Partial<PairedTask>): PairedTask {
|
||||
return {
|
||||
id: 'paired-1',
|
||||
chat_jid: 'dc:general',
|
||||
group_folder: 'general',
|
||||
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: 0,
|
||||
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: 'review_ready',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: null,
|
||||
created_at: '2026-04-26T04:00:00.000Z',
|
||||
updated_at: '2026-04-26T04:30:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -19,6 +72,7 @@ describe('web dashboard server handler', () => {
|
||||
const handler = createWebDashboardHandler({
|
||||
readStatusSnapshots: () => [],
|
||||
getTasks: () => [],
|
||||
getPairedTasks: () => [],
|
||||
});
|
||||
|
||||
const health = await handler(new Request('http://localhost/api/health'));
|
||||
@@ -32,9 +86,11 @@ describe('web dashboard server handler', () => {
|
||||
const body = (await overview.json()) as {
|
||||
rooms: { total: number };
|
||||
tasks: { total: number };
|
||||
inbox: unknown[];
|
||||
};
|
||||
expect(body.rooms.total).toBe(0);
|
||||
expect(body.tasks.total).toBe(0);
|
||||
expect(body.inbox).toEqual([]);
|
||||
});
|
||||
|
||||
it('serves full Claude, Kimi, and Codex usage rows through overview JSON', async () => {
|
||||
@@ -73,6 +129,7 @@ describe('web dashboard server handler', () => {
|
||||
},
|
||||
],
|
||||
getTasks: () => [],
|
||||
getPairedTasks: () => [],
|
||||
});
|
||||
|
||||
const overview = await handler(
|
||||
@@ -90,6 +147,92 @@ describe('web dashboard server handler', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('serves typed inbox items through overview JSON', async () => {
|
||||
const snapshots: StatusSnapshot[] = [
|
||||
{
|
||||
serviceId: 'codex-main',
|
||||
agentType: 'codex',
|
||||
assistantName: 'Codex',
|
||||
updatedAt: '2026-04-26T05:00:00.000Z',
|
||||
entries: [
|
||||
{
|
||||
jid: 'dc:ops',
|
||||
name: '#ops',
|
||||
folder: 'ops',
|
||||
agentType: 'codex',
|
||||
status: 'waiting',
|
||||
elapsedMs: 2500,
|
||||
pendingMessages: true,
|
||||
pendingTasks: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handler = createWebDashboardHandler({
|
||||
readStatusSnapshots: () => snapshots,
|
||||
getTasks: () => [
|
||||
makeTask({
|
||||
id: 'ci-1',
|
||||
prompt:
|
||||
'[BACKGROUND CI WATCH]\nWatch target:\nPR #21\n\nCheck instructions:\nwatch',
|
||||
last_run: '2026-04-26T05:05:00.000Z',
|
||||
last_result: 'failed with API_KEY=plain-secret-value',
|
||||
status: 'paused',
|
||||
}),
|
||||
],
|
||||
getPairedTasks: () => [
|
||||
makePairedTask({
|
||||
id: 'merge-1',
|
||||
status: 'merge_ready',
|
||||
title: 'Ready to merge',
|
||||
updated_at: '2026-04-26T05:04:00.000Z',
|
||||
}),
|
||||
],
|
||||
now: () => '2026-04-26T05:10:00.000Z',
|
||||
});
|
||||
|
||||
const overview = await handler(
|
||||
new Request('http://localhost/api/overview'),
|
||||
);
|
||||
expect(overview.status).toBe(200);
|
||||
const body = (await overview.json()) as {
|
||||
inbox: Array<{
|
||||
kind: string;
|
||||
id: string;
|
||||
summary: string;
|
||||
roomJid?: string;
|
||||
serviceId?: string;
|
||||
taskId?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
expect(body.inbox.map((item) => item.kind)).toEqual([
|
||||
'ci-failure',
|
||||
'approval',
|
||||
'pending-room',
|
||||
]);
|
||||
expect(body.inbox).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: 'pending-room',
|
||||
id: 'room:codex-main:dc:ops',
|
||||
roomJid: 'dc:ops',
|
||||
serviceId: 'codex-main',
|
||||
}),
|
||||
);
|
||||
expect(body.inbox).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: 'approval',
|
||||
id: 'paired:merge-1:merge_ready',
|
||||
taskId: 'merge-1',
|
||||
serviceId: 'codex-main',
|
||||
}),
|
||||
);
|
||||
const ciFailure = body.inbox.find((item) => item.kind === 'ci-failure');
|
||||
expect(ciFailure?.summary).toContain('API_KEY=<redacted>');
|
||||
expect(ciFailure?.summary).not.toContain('plain-secret-value');
|
||||
});
|
||||
|
||||
it('serves Vite static assets and falls back to index for SPA routes', async () => {
|
||||
const staticDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-dashboard-'),
|
||||
|
||||
@@ -2,13 +2,13 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { WEB_DASHBOARD } from './config.js';
|
||||
import { getAllTasks } from './db.js';
|
||||
import { getAllOpenPairedTasks, getAllTasks } from './db.js';
|
||||
import { logger } from './logger.js';
|
||||
import {
|
||||
readStatusSnapshots,
|
||||
type StatusSnapshot,
|
||||
} from './status-dashboard.js';
|
||||
import type { ScheduledTask } from './types.js';
|
||||
import type { PairedTask, ScheduledTask } from './types.js';
|
||||
import {
|
||||
buildWebDashboardOverview,
|
||||
sanitizeScheduledTask,
|
||||
@@ -21,6 +21,7 @@ export interface WebDashboardHandlerOptions {
|
||||
statusMaxAgeMs?: number;
|
||||
readStatusSnapshots?: (maxAgeMs: number) => StatusSnapshot[];
|
||||
getTasks?: () => ScheduledTask[];
|
||||
getPairedTasks?: () => PairedTask[];
|
||||
now?: () => string;
|
||||
}
|
||||
|
||||
@@ -105,6 +106,7 @@ export function createWebDashboardHandler(
|
||||
): (request: Request) => Response | Promise<Response> {
|
||||
const readSnapshots = opts.readStatusSnapshots ?? readStatusSnapshots;
|
||||
const loadTasks = opts.getTasks ?? getAllTasks;
|
||||
const loadPairedTasks = opts.getPairedTasks ?? getAllOpenPairedTasks;
|
||||
const statusMaxAgeMs = opts.statusMaxAgeMs ?? DEFAULT_STATUS_MAX_AGE_MS;
|
||||
|
||||
return (request: Request): Response => {
|
||||
@@ -121,11 +123,13 @@ export function createWebDashboardHandler(
|
||||
if (url.pathname === '/api/overview') {
|
||||
const snapshots = readSnapshots(statusMaxAgeMs);
|
||||
const tasks = loadTasks();
|
||||
const pairedTasks = loadPairedTasks();
|
||||
return jsonResponse(
|
||||
buildWebDashboardOverview({
|
||||
now: opts.now?.(),
|
||||
snapshots,
|
||||
tasks,
|
||||
pairedTasks,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user