182 lines
7.4 KiB
TypeScript
182 lines
7.4 KiB
TypeScript
// 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<T>(path: string): Promise<T> {
|
|
const res = await client.get(`/${path}`);
|
|
return res.data.data as T;
|
|
}
|
|
|
|
async function getWithHeaders<T>(path: string, headers: Record<string, string>): Promise<T> {
|
|
const res = await client.get(`/${path}`, { headers });
|
|
return res.data.data as T;
|
|
}
|
|
|
|
async function post<T>(path: string, body: unknown, headers: Record<string, string>): Promise<T> {
|
|
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<string, Record<string, number | string>>;
|
|
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<string, unknown>;
|
|
envelope: CommandEnvelope;
|
|
evidence: Record<string, unknown>;
|
|
}
|
|
|
|
export interface CommandCenterState extends Freshness {
|
|
generated_at: string;
|
|
widgets: Record<string, CommandWidget>;
|
|
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;
|
|
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 };
|
|
actor: SettingsActor;
|
|
}
|
|
|
|
export interface ChatAction {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
triggers: string[];
|
|
}
|
|
|
|
export interface SettingsState {
|
|
actor: SettingsActor;
|
|
capabilities: {
|
|
can_write_standard: boolean;
|
|
can_write_sensitive: boolean;
|
|
can_rollback: boolean;
|
|
};
|
|
policy: Record<string, { securitySensitive: boolean; description: string }>;
|
|
settings: Record<string, { value: unknown; version: number; updatedAt: string; actor: string; reason: string }>;
|
|
audit: any[];
|
|
audit_verify: { ok: boolean; output: string };
|
|
}
|
|
|
|
function actorHeaders(actor: SettingsActor): Record<string, string> {
|
|
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>('overview'),
|
|
runs: (limit = 50) => get<Freshness & { count: number; runs: any[] }>(`runs?limit=${limit}`),
|
|
governance: () => get<Freshness & { records: number; by_decision: Record<string, number>; head: string | null; recent: any[] }>('governance'),
|
|
security: () => get<Freshness & { verdicts: number; by_status: Record<string, number>; benign_fp: any; recent: any[] }>('security'),
|
|
incidents: () => get<Freshness & { total: number; kill_switch_scopes: string[]; incidents: any[] }>('incidents'),
|
|
traceability: () => get<Freshness & { matrix: any }>('traceability'),
|
|
cost: () => get<Freshness & { provider_tokens: number; provider_cost: number; by_provider: any[]; business_kpi: any }>('cost'),
|
|
command: () => get<CommandCenterState>('command'),
|
|
settings: (actor: SettingsActor) => getWithHeaders<SettingsState>('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<KillSwitchState>('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<ApprovalsState>(`approvals?status=${status}`, actorHeaders(actor)),
|
|
submitApproval: (actor: SettingsActor, body: { action: string; target: string; risk?: string; sensitive?: boolean; reason: string; payload?: Record<string, unknown> }) =>
|
|
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 }) =>
|
|
post<{ proposal: any; applied: any; audit_verify: { ok: boolean; output: string } }>('approvals/decide', body, actorHeaders(actor)),
|
|
askChat: (actor: SettingsActor, body: { message: string; chatId?: string }) =>
|
|
post<ChatAnswer>('chat/ask', body, actorHeaders(actor)),
|
|
verifyChatAudit: () => get<{ ok: boolean; output: string }>('chat/audit/verify'),
|
|
chatActions: (actor: SettingsActor) => getWithHeaders<{ success: boolean; actions: ChatAction[] }>('chat/actions', 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 };
|
|
}
|
|
}
|