import { useStandardQuery } from '@szl-holdings/api-client-react'; import { AlertTriangle, CheckCircle2, Clock, ExternalLink, ShieldAlert, ShieldCheck, ShieldX, } from 'lucide-react'; import { Link } from 'wouter'; const BASE = import.meta.env.BASE_URL.replace(/\/$/, ''); type Decision = 'allow' | 'require-approval' | 'require-dual-approval' | 'block'; interface RecentRow { id: number; requestId: string; agentId: string | null; tier: string; action: string; toolId: string | null; decision: Decision; reason: string | null; decidedAt: string; traceId: string | null; traceDomain: string | null; approvalStatus: string | null; approvalType: string | null; approvalExpiresAt: string | null; } interface SummaryResponse { data: { windowMinutes: number; since: string; counts: { allow: number; requireApproval: number; requireDualApproval: number; block: number; total: number; }; pendingApprovals: number; recent: RecentRow[]; }; } function timeAgo(iso?: string | null): string { if (!iso) return '—'; const t = new Date(iso).getTime(); if (Number.isNaN(t)) return iso; const s = Math.max(1, Math.floor((Date.now() - t) / 1000)); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const d = Math.floor(h / 24); return `${d}d ago`; } function decisionLabel(d: Decision): string { switch (d) { case 'allow': return 'allow'; case 'require-approval': return 'approval'; case 'require-dual-approval': return 'dual approval'; case 'block': return 'denied'; } } const DECISION_STYLE: Record = { allow: { fg: '#22c55e', bg: 'rgba(34,197,94,0.10)', border: 'rgba(34,197,94,0.30)' }, 'require-approval': { fg: '#d4a054', bg: 'rgba(212,160,84,0.10)', border: 'rgba(212,160,84,0.30)', }, 'require-dual-approval': { fg: '#f97316', bg: 'rgba(249,115,22,0.10)', border: 'rgba(249,115,22,0.30)', }, block: { fg: '#ef4444', bg: 'rgba(239,68,68,0.10)', border: 'rgba(239,68,68,0.30)' }, }; function CountBlock({ label, value, Icon, color, }: { label: string; value: number; Icon: typeof ShieldCheck; color: string; }) { return (
{value.toLocaleString()} {label}
); } export function GuardianDecisionsTile() { const q = useStandardQuery({ queryKey: ['guardian', 'decisions-summary', '1h'], queryFn: async () => { const res = await fetch(`${BASE}/api/guardian/decisions/summary?windowMinutes=60&limit=8`, { credentials: 'include', headers: { 'Content-Type': 'application/json' }, }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`HTTP ${res.status}: ${text || res.statusText}`); } return res.json() as Promise; }, refetchInterval: 30_000, }); const data = q.data?.data; const counts = data?.counts ?? { allow: 0, requireApproval: 0, requireDualApproval: 0, block: 0, total: 0, }; const pending = data?.pendingApprovals ?? 0; const recent = data?.recent ?? []; return (
Guardian Decisions last 60m
Open Console
{q.isLoading ? (
Loading…
) : q.error ? (
Failed to load Guardian decisions
{(q.error as Error).message}
) : ( <>
Recent denials & pending approvals
{recent.length === 0 ? (
No blocked or pending-approval decisions in the last hour.
) : (
{recent.map((row) => { const ds = DECISION_STYLE[row.decision]; const traceHref = row.traceId ? `${BASE}/cognitive/traces?trace=${encodeURIComponent(row.traceId)}` : null; return (
{decisionLabel(row.decision)}
{row.action}
{row.agentId ?? 'anon-agent'} {row.toolId ? ` · ${row.toolId}` : ''} {row.traceDomain ? ` · ${row.traceDomain}` : ''} {' · '} {timeAgo(row.decidedAt)}
{row.reason && (
{row.reason}
)}
{traceHref ? ( Trace ) : ( no trace )}
); })}
)} )}
); } export default GuardianDecisionsTile;