feat: add control panel
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Single axios client for the read-only Ops Console API. Mirrors the OKR app's api.ts:
|
||||
// relative baseURL, unwrap response.data.data. All GET (read-only).
|
||||
// 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({
|
||||
@@ -11,6 +11,16 @@ async function get<T>(path: string): Promise<T> {
|
||||
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 {
|
||||
@@ -23,6 +33,73 @@ export interface Overview extends Freshness {
|
||||
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 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}`),
|
||||
@@ -31,6 +108,22 @@ export const api = {
|
||||
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)),
|
||||
};
|
||||
|
||||
// Health is raw (not enveloped) + carries HTTP status.
|
||||
|
||||
Reference in New Issue
Block a user