export interface DashboardOverview { generatedAt: string; services: Array<{ serviceId: string; assistantName: string; agentType: string; updatedAt: string; totalRooms: number; activeRooms: number; }>; rooms: { total: number; active: number; waiting: number; inactive: number; }; tasks: { total: number; active: number; paused: number; completed: number; watchers: { active: number; paused: number; completed: number; }; }; usage: { rows: Array<{ name: string; h5pct: number; h5reset: string; d7pct: number; d7reset: string; }>; fetchedAt: string | null; }; operations?: { serviceRestarts: DashboardServiceRestart[]; }; inbox: Array<{ id: string; groupKey: string; kind: | 'pending-room' | 'reviewer-request' | 'approval' | 'arbiter-request' | 'ci-failure' | 'mention'; severity: 'info' | 'warn' | 'error'; title: string; summary: string; occurredAt: string; lastOccurredAt: string; createdAt: string; occurrences: number; source: 'status-snapshot' | 'paired-task' | 'scheduled-task'; roomJid?: string; roomName?: string; groupFolder?: string; serviceId?: string; taskId?: string; taskStatus?: string; }>; } export interface DashboardServiceRestart { id: string; target: 'stack'; requestedAt: string; completedAt: string | null; status: 'running' | 'success' | 'failed'; services: string[]; error?: string; } export interface StatusSnapshot { serviceId: string; agentType: string; assistantName: string; updatedAt: string; entries: Array<{ jid: string; name: string; folder: string; agentType: string; status: 'processing' | 'waiting' | 'inactive'; elapsedMs: number | null; pendingMessages: boolean; pendingTasks: number; }>; } export interface DashboardRoomActivity { serviceId: string; jid: string; name: string; folder: string; agentType: string; status: 'processing' | 'waiting' | 'inactive'; elapsedMs: number | null; pendingMessages: boolean; pendingTasks: number; messages: Array<{ id: string; sender: string; senderName: string; content: string; attachments?: Array<{ path: string; name?: string; mime?: string; }>; timestamp: string; isFromMe: boolean; isBotMessage: boolean; sourceKind: string; }>; pairedTask: { id: string; title: string | null; status: string; roundTripCount: number; updatedAt: string; currentTurn: { turnId: string; role: string; intentKind: string; state: string; attemptNo: number; executorServiceId: string | null; executorAgentType: string | null; activeRunId: string | null; createdAt: string; updatedAt: string; completedAt: string | null; lastError: string | null; progressText: string | null; progressUpdatedAt: string | null; } | null; outputs: Array<{ id: number; turnNumber: number; role: string; verdict: string | null; createdAt: string; outputText: string; attachments?: Array<{ path: string; name?: string; mime?: string; }>; }>; } | null; } export interface DashboardTask { id: string; groupFolder: string; chatJid: string; agentType: string | null; ciProvider: string | null; ciMetadata: string | null; scheduleType: string; scheduleValue: string; contextMode: string; nextRun: string | null; lastRun: string | null; lastResult: string | null; status: 'active' | 'paused' | 'completed'; suspendedUntil: string | null; createdAt: string; promptPreview: string; promptLength: number; isWatcher: boolean; } export type DashboardTaskAction = 'pause' | 'resume' | 'cancel'; export type DashboardInboxAction = 'run' | 'decline' | 'dismiss'; export type DashboardServiceAction = 'restart'; export type DashboardTaskScheduleType = 'cron' | 'interval' | 'once'; export type DashboardTaskContextMode = 'group' | 'isolated'; export interface CreateScheduledTaskInput { roomJid: string; prompt: string; scheduleType: DashboardTaskScheduleType; scheduleValue: string; contextMode: DashboardTaskContextMode; requestId?: string; } export interface UpdateScheduledTaskInput { prompt?: string; scheduleType: DashboardTaskScheduleType; scheduleValue: string; } async function fetchJson(path: string): Promise { const response = await fetch(path, { headers: { accept: 'application/json' }, }); if (!response.ok) { throw new Error(`${path} failed: ${response.status}`); } return (await response.json()) as T; } async function postJson(path: string, body: unknown): Promise { const response = await fetch(path, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { let message = `${path} failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) message = payload.error; } catch { // Keep the status-based message when the server body is not JSON. } throw new Error(message); } return (await response.json()) as T; } export async function fetchDashboardData(): Promise<{ overview: DashboardOverview; snapshots: StatusSnapshot[]; tasks: DashboardTask[]; }> { const [overview, snapshots, tasks] = await Promise.all([ fetchJson('/api/overview'), fetchJson('/api/status-snapshots'), fetchJson('/api/tasks'), ]); return { overview, snapshots, tasks }; } export async function fetchRoomTimeline( roomJid: string, ): Promise { return fetchJson( `/api/rooms/${encodeURIComponent(roomJid)}/timeline`, ); } export async function fetchRoomsTimelineBatch(): Promise< Record > { return fetchJson>( '/api/rooms-timeline', ); } export async function runScheduledTaskAction( taskId: string, action: DashboardTaskAction, ): Promise<{ ok: true; id?: string; deleted?: boolean; task?: DashboardTask | null; }> { return postJson(`/api/tasks/${encodeURIComponent(taskId)}/actions`, { action, }); } export async function createScheduledTask( input: CreateScheduledTaskInput, ): Promise<{ ok: true; task: DashboardTask | null }> { return postJson('/api/tasks', input); } export async function updateScheduledTask( taskId: string, input: UpdateScheduledTaskInput, ): Promise<{ ok: true; task: DashboardTask | null }> { const response = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`, { method: 'PATCH', headers: { accept: 'application/json', 'content-type': 'application/json', }, body: JSON.stringify(input), }); if (!response.ok) { let message = `/api/tasks/${taskId} failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) message = payload.error; } catch { // Keep the status-based message when the server body is not JSON. } throw new Error(message); } return (await response.json()) as { ok: true; task: DashboardTask | null }; } export async function runInboxAction( inboxId: string, action: DashboardInboxAction, options: { requestId?: string; lastOccurredAt?: string } = {}, ): Promise<{ ok: true; id: string; taskId?: string; intentKind?: string; status?: string; queued?: boolean; dismissed?: boolean; duplicate?: boolean; }> { return postJson(`/api/inbox/${encodeURIComponent(inboxId)}/actions`, { action, lastOccurredAt: options.lastOccurredAt, requestId: options.requestId, }); } export async function runServiceAction( serviceId: 'stack', action: DashboardServiceAction, options: { requestId?: string } = {}, ): Promise<{ ok: true; duplicate?: boolean; restart: DashboardServiceRestart; }> { return postJson(`/api/services/${encodeURIComponent(serviceId)}/actions`, { action, requestId: options.requestId, }); } export async function sendRoomMessage( roomJid: string, text: string, requestId: string, nickname?: string | null, ): Promise<{ ok: true; id: string; queued: boolean }> { return postJson(`/api/rooms/${encodeURIComponent(roomJid)}/messages`, { requestId, text, nickname: nickname ?? undefined, }); } export interface ClaudeAccountSummary { index: number; expiresAt: number | null; scopes: string[]; subscriptionType?: string; rateLimitTier?: string; exists: boolean; } export interface CodexAccountSummary { index: number; accountId: string | null; email: string | null; planType: string | null; subscriptionUntil: string | null; subscriptionLastChecked: string | null; exists: boolean; } export interface ModelRoleConfig { model: string; effort: string; } export interface ModelConfigSnapshot { owner: ModelRoleConfig; reviewer: ModelRoleConfig; arbiter: ModelRoleConfig; } export interface FastModeSnapshot { codex: boolean; claude: boolean; } export async function fetchAccounts(): Promise<{ claude: ClaudeAccountSummary[]; codex: CodexAccountSummary[]; codexCurrentIndex?: number; }> { return fetchJson('/api/settings/accounts'); } export async function fetchModelConfig(): Promise { return fetchJson('/api/settings/models'); } export async function updateModels( input: Partial<{ owner: Partial; reviewer: Partial; arbiter: Partial; }>, ): Promise { const response = await fetch('/api/settings/models', { method: 'PUT', headers: { accept: 'application/json', 'content-type': 'application/json', }, body: JSON.stringify(input), }); if (!response.ok) { let msg = `update models failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) msg = payload.error; } catch { /* ignore */ } throw new Error(msg); } return (await response.json()) as ModelConfigSnapshot; } export async function refreshCodexAccount( index: number, ): Promise { const response = await fetch( `/api/settings/accounts/codex/${index}/refresh`, { method: 'POST' }, ); if (!response.ok) { let msg = `refresh failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) msg = payload.error; } catch { /* ignore */ } throw new Error(msg); } const json = (await response.json()) as { account: CodexAccountSummary }; return json.account; } export async function refreshAllCodexAccounts(): Promise<{ refreshed: number[]; failed: Array<{ index: number; error: string }>; }> { const response = await fetch('/api/settings/accounts/codex/refresh-all', { method: 'POST', }); if (!response.ok) { throw new Error(`refresh-all failed: ${response.status}`); } return (await response.json()) as { refreshed: number[]; failed: Array<{ index: number; error: string }>; }; } export async function setCurrentCodexAccount( index: number, ): Promise<{ codexCurrentIndex: number }> { const response = await fetch('/api/settings/accounts/codex/current', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ index }), }); if (!response.ok) { let msg = `switch failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) msg = payload.error; } catch { /* ignore */ } throw new Error(msg); } return (await response.json()) as { codexCurrentIndex: number }; } export async function fetchFastMode(): Promise { return fetchJson('/api/settings/fast-mode'); } export async function updateFastMode( input: Partial, ): Promise { const response = await fetch('/api/settings/fast-mode', { method: 'PUT', headers: { accept: 'application/json', 'content-type': 'application/json', }, body: JSON.stringify(input), }); if (!response.ok) { let msg = `update fast mode failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) msg = payload.error; } catch { /* ignore */ } throw new Error(msg); } return (await response.json()) as FastModeSnapshot; } export async function deleteAccount( provider: 'claude' | 'codex', index: number, ): Promise<{ ok: true; provider: string; index: number }> { const response = await fetch(`/api/settings/accounts/${provider}/${index}`, { method: 'DELETE', }); if (!response.ok) { let msg = `delete account failed: ${response.status}`; try { const payload = (await response.json()) as { error?: string }; if (payload.error) msg = payload.error; } catch { /* ignore */ } throw new Error(msg); } return (await response.json()) as { ok: true; provider: string; index: number; }; } export async function addClaudeAccount( token: string, ): Promise<{ ok: true; index: number; accountId: string | null }> { return postJson('/api/settings/accounts/claude', { token }); }