// Single axios client for the Ops Console API. Mirrors the OKR app's api.ts: // relative baseURL, unwrap response.data.data. import axios from 'axios'; const client = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api/v1', }); async function get(path: string): Promise { const res = await client.get(`/${path}`); return res.data.data as T; } async function getWithHeaders(path: string, headers: Record): Promise { const res = await client.get(`/${path}`, { headers }); return res.data.data as T; } async function post(path: string, body: unknown, headers: Record): Promise { const res = await client.post(`/${path}`, body, { headers }); return res.data.data as T; } export interface Freshness { stale: boolean; age_s: number | null; stale_after_s: number } export interface Overview extends Freshness { totals: { runs: number; total_cost: number; avg_latency_ms: number; failures: number; hallucination_signals: number; provider_tokens: number; provider_cost: number; fallback_routes: number; tool_denies: number; action_blocks: number; }; harness_signals: Record>; audit_chain: { records: number; head: string | null; last_decision: string | null }; } export interface SettingsActor { actor: string; role: string; project: string; tenant: string; } export interface KillSwitchState { count: number; engaged: Array<{ scope: string; id: string; reason?: string; engaged_at?: string; actor?: string; raw?: string }>; } export interface ApprovalsState { count: number; proposals: any[]; oversight: any[]; audit_verify: { ok: boolean; output: string }; } export interface CommandEnvelope { source: string; artifact_path: string; commit: string | null; run_at: string | null; verified: boolean; status: 'verified' | 'present_unverified' | 'missing'; } export interface CommandWidget { id: string; title: string; status: string; summary: string; metrics: Record; envelope: CommandEnvelope; evidence: Record; } export interface CommandCenterState extends Freshness { generated_at: string; widgets: Record; briefing: Array<{ label: string; value: string; status: string; widget: string }>; ticker: Array<{ at: string | null; kind: string; text: string; status: string }>; } export interface ChatSource { path: string; title?: string; line?: number; excerpt?: string; score: number; hash?: string; preview?: string; envelope?: CommandEnvelope; } export interface ChatAnswer { success: boolean; chat_id?: string; turn_id?: string; trace_id?: string; mode: 'READ_ONLY' | 'OPERATOR' | 'BLOCK' | 'NOT_SUPPORTED' | string; risk: string; decision: 'ANSWERED' | 'ACTION_COMPLETED' | 'ACTION_FAILED' | 'REQUIRES_APPROVAL' | 'DENIED' | 'NOT_SUPPORTED' | string; answer: string; sources: ChatSource[]; certified: boolean; audit: { hash?: string; record_hash?: string; head?: string; seq?: number; path?: string }; audit_verify: { ok: boolean; output: string }; router: { reason?: string; matched_rules?: string[]; gates?: string[]; side_effect_allowed?: boolean; needs_approval?: boolean; }; action?: { id: string; label: string; description: string } | null; action_gate?: { outcome?: string; reason?: string; exit_code?: number }; agent_binding?: { success: boolean; decision: string; reason: string; agent_selected: string; skill_selected: string; delegation_level: number; model_role?: string; mode?: string; tool_allowlist: string[]; requires_approval: boolean; audit?: { record_hash?: string; head?: string; seq?: number }; }; loop_run?: { success: boolean; decision: string; reason: string; run_id: string; draft_ref?: string; draft_certified?: boolean; side_effect_released?: boolean; artifact?: string; success_criteria?: string; trace_verify?: { ok: boolean; output: string }; replay?: { ok: boolean; output: string }; }; codegen?: { artifact?: string; artifact_scan?: { ok: boolean; output: string }; tool_output_scan?: { ok: boolean; output: string }; }; synthesis?: { mode: 'deterministic' | 'model' | string; reason?: string; provider?: string; model?: string; class?: string; input_tokens?: number; output_tokens?: number; cost_source?: string; fallback_from?: string | null; }; actor: SettingsActor; } export type ChatStreamPhase = Partial & { phase?: 'draft' | 'final'; answer: string; decision: string; certified: boolean; mode: string; sources: ChatSource[]; }; export interface ChatAction { id: string; label: string; description: string; triggers: string[]; } export interface ChatAgent { id: string; label: string; mode: string; model_role: string; roles_allowed: string[]; skills_allowed: string[]; tool_allowlist: string[]; max_delegation_level: number; requires_approval?: boolean; allowed_for_role?: boolean; } export interface ChatReplay { ok: boolean; decision: 'MATCH' | 'DRIFT' | 'BREAK' | string; records: number; loop_replayed?: number; diffs?: any[]; audit_path?: string; } export interface ChatConversation { chat_id: string; title: string; updated_at: string; turns: number; last_decision: string; last_mode: string; } export interface ChatHistoryTurn { chat_id: string; turn_id: string; timestamp: string; mode: string; risk: string; decision: string; prompt_preview: string; answer_preview: string; certified: boolean; audit_hash: string; } export interface ChatHistory { ok: boolean; conversations: ChatConversation[]; turns: ChatHistoryTurn[]; } export interface SettingsState { actor: SettingsActor; capabilities: { can_write_standard: boolean; can_write_sensitive: boolean; can_rollback: boolean; }; policy: Record; settings: Record; audit: any[]; audit_verify: { ok: boolean; output: string }; } function actorHeaders(actor: SettingsActor): Record { return { 'x-casan-actor': actor.actor, 'x-casan-role': actor.role, 'x-casan-project': actor.project, 'x-casan-tenant': actor.tenant, }; } export const api = { overview: () => get('overview'), runs: (limit = 50) => get(`runs?limit=${limit}`), governance: () => get; head: string | null; recent: any[] }>('governance'), security: () => get; benign_fp: any; recent: any[] }>('security'), incidents: () => get('incidents'), traceability: () => get('traceability'), cost: () => get('cost'), command: () => get('command'), settings: (actor: SettingsActor) => getWithHeaders('settings', actorHeaders(actor)), setSetting: (actor: SettingsActor, body: { key: string; value: unknown; reason: string; approval?: string }) => post<{ key: string; setting: any; audit_verify: { ok: boolean; output: string } }>('settings', body, actorHeaders(actor)), rollbackSetting: (actor: SettingsActor, body: { key: string; reason: string }) => post<{ key: string; setting: any; audit_verify: { ok: boolean; output: string } }>('settings/rollback', body, actorHeaders(actor)), killSwitch: (actor: SettingsActor) => getWithHeaders('kill-switch', actorHeaders(actor)), engageKillSwitch: (actor: SettingsActor, body: { scope: string; id: string; reason: string }) => post<{ output: string; status: KillSwitchState }>('kill-switch/engage', body, actorHeaders(actor)), clearKillSwitch: (actor: SettingsActor, body: { scope: string; id: string; reason: string }) => post<{ output: string; status: KillSwitchState }>('kill-switch/clear', body, actorHeaders(actor)), approvals: (actor: SettingsActor, status = 'pending') => getWithHeaders(`approvals?status=${status}`, actorHeaders(actor)), submitApproval: (actor: SettingsActor, body: { action: string; target: string; risk?: string; sensitive?: boolean; reason: string; payload?: Record }) => post<{ proposal: any; audit_verify: { ok: boolean; output: string } }>('approvals/submit', body, actorHeaders(actor)), decideApproval: (actor: SettingsActor, body: { id: string; decision: 'approve' | 'reject'; reason: string; approvalJwt?: string }) => post<{ proposal: any; applied: any; audit_verify: { ok: boolean; output: string } }>('approvals/decide', body, actorHeaders(actor)), askChat: (actor: SettingsActor, body: { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number }) => post('chat/ask', body, actorHeaders(actor)), askChatStream: async ( actor: SettingsActor, body: { message: string; chatId?: string; agentId?: string; skillId?: string; delegationLevel?: number }, onPhase: (phase: ChatStreamPhase) => void, ): Promise => { const base = import.meta.env.VITE_API_BASE_URL ?? '/api/v1'; const resp = await fetch(`${base}/chat/ask/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...actorHeaders(actor) }, body: JSON.stringify(body), }); if (!resp.ok || !resp.body) { throw new Error(`stream failed: ${resp.status}`); } const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; const flush = (line: string) => { const trimmed = line.trim(); if (!trimmed) return; try { onPhase(JSON.parse(trimmed) as ChatStreamPhase); } catch { /* ignore partial/non-JSON chunk */ } }; for (;;) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let idx: number; while ((idx = buf.indexOf('\n')) >= 0) { flush(buf.slice(0, idx)); buf = buf.slice(idx + 1); } } flush(buf); }, verifyChatAudit: () => get<{ ok: boolean; output: string }>('chat/audit/verify'), chatHistory: (actor: SettingsActor, chatId = '', limit = 50) => getWithHeaders(`chat/history?chatId=${encodeURIComponent(chatId)}&limit=${limit}`, actorHeaders(actor)), replayChat: (chatId = '', turnId = '', tenant = '') => get(`chat/replay?chatId=${encodeURIComponent(chatId)}&turnId=${encodeURIComponent(turnId)}&tenant=${encodeURIComponent(tenant)}`), chatActions: (actor: SettingsActor) => getWithHeaders<{ success: boolean; actions: ChatAction[] }>('chat/actions', actorHeaders(actor)), chatAgents: (actor: SettingsActor) => getWithHeaders<{ success: boolean; agents: ChatAgent[] }>('chat/agents', actorHeaders(actor)), }; // Health is raw (not enveloped) + carries HTTP status. export async function health(): Promise<{ ok: boolean; status: string; metrics_age_s: number | null; runs: number }> { try { const res = await axios.get('/healthz', { baseURL: '', validateStatus: () => true }); return { ok: res.status === 200, ...res.data }; } catch { return { ok: false, status: 'unreachable', metrics_age_s: null, runs: 0 }; } }