feat: prepare CASAN paid PoC release package
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { api, type EvidencePackDetail, type EvidencePackSummary } from '../lib/api';
|
||||
import { Card, StatusBadge } from '../components/ui/Card';
|
||||
|
||||
const HARNESSES = [
|
||||
['H1', 'Context', 'h1-context-report.json'], ['H2', 'Tool', 'h2-tool-audit.json'],
|
||||
['H3', 'Evaluation', 'h3-eval-scorecard.json'], ['H4', 'Security', 'h4-security-report.json'],
|
||||
['H5', 'Governance', 'h5-audit-chain-proof.json'], ['H6', 'AgentOps', 'h6-cost-telemetry.json'],
|
||||
['H7', 'Orchestration', 'h7-orchestration-report.json'],
|
||||
] as const;
|
||||
|
||||
function shortHash(value: string | null) { return value ? `${value.slice(0, 12)}…${value.slice(-8)}` : 'not recorded'; }
|
||||
function label(reason: string) { return reason.replaceAll('_', ' '); }
|
||||
function statusFor(report: Record<string, unknown> | undefined, detail: EvidencePackDetail) {
|
||||
if (!report) return 'not recorded';
|
||||
if (report.chain_ok === false || report.audit_chain_ok === false || report.telemetry_ok === false) return 'failed';
|
||||
if (Number(report.blocked ?? 0) > 0 || report.cost_spike_status === 'spike_detected') return 'attention';
|
||||
return detail.integrity.ok ? 'recorded' : 'unverified';
|
||||
}
|
||||
|
||||
function PackRow({ pack, selected, onSelect }: { pack: EvidencePackSummary; selected: boolean; onSelect: () => void }) {
|
||||
return <button type="button" onClick={onSelect} className={`w-full rounded-xl border p-3.5 text-left transition ${selected ? 'border-indigo-300 bg-indigo-50/70 shadow-sm' : 'border-transparent hover:border-slate-200 hover:bg-slate-50'}`}>
|
||||
<div className="flex items-start justify-between gap-2"><span className="font-mono text-xs font-semibold text-slate-700">{pack.run_id}</span><StatusBadge value={pack.certified ? 'certified' : 'uncertified'} /></div>
|
||||
<div className="mt-2 flex items-center justify-between text-[11px] text-slate-500"><span>{new Date(pack.created_at).toLocaleString()}</span><span>{pack.file_count} files</span></div>
|
||||
</button>;
|
||||
}
|
||||
|
||||
function Detail({ pack }: { pack: EvidencePackDetail }) {
|
||||
const reportEntries = Object.entries(pack.reports);
|
||||
return <div className="space-y-5">
|
||||
<section className={`overflow-hidden rounded-2xl border ${pack.certified ? 'border-emerald-200 bg-gradient-to-br from-emerald-50 to-white' : 'border-amber-200 bg-gradient-to-br from-amber-50 to-white'} p-5 sm:p-6`}>
|
||||
<div className="flex flex-col justify-between gap-5 sm:flex-row sm:items-start">
|
||||
<div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500">Evidence verdict</div><h1 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900">{pack.certified ? 'Certified evidence pack' : 'Evidence captured — certification withheld'}</h1><p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">Run <span className="font-mono text-xs">{pack.run_id}</span> is bound to a file manifest and verified independently before it is shown here.</p></div>
|
||||
<div className="flex flex-wrap gap-2"><StatusBadge value={pack.integrity.ok ? 'manifest verified' : 'integrity failed'} /><StatusBadge value={pack.signature === 'present' ? 'signature present' : 'unsigned'} /></div>
|
||||
</div>
|
||||
{pack.certification_reasons.length > 0 && <div className="mt-5 rounded-xl border border-amber-200 bg-white/80 p-3.5"><div className="text-[11px] font-bold uppercase tracking-[0.13em] text-amber-700">Why certification is withheld</div><div className="mt-2 flex flex-wrap gap-2">{pack.certification_reasons.map((reason) => <span key={reason} className="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800">{label(reason)}</span>)}</div></div>}
|
||||
</section>
|
||||
|
||||
<Card title="H1–H7 chain of custody" right={<span className="font-mono text-[11px] text-slate-400">manifest {shortHash(pack.manifest_head)}</span>}>
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-7">{HARNESSES.map(([id, name, file]) => { const report = pack.reports[file]; const state = statusFor(report, pack); return <div key={id} className="relative rounded-xl border border-slate-200 bg-slate-50/70 p-3"><div className="flex items-center justify-between"><span className="font-mono text-xs font-bold text-indigo-700">{id}</span><span className={`h-2 w-2 rounded-full ${state === 'recorded' ? 'bg-emerald-500' : state === 'failed' ? 'bg-rose-500' : state === 'attention' ? 'bg-amber-500' : 'bg-slate-300'}`} /></div><div className="mt-2 text-sm font-semibold text-slate-800">{name}</div><div className="mt-1 text-[11px] text-slate-500">{state}</div></div>; })}</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-5 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<Card title="Gate reports"><div className="space-y-3">{reportEntries.map(([name, report]) => <details key={name} className="rounded-xl border border-slate-200 bg-slate-50/60 p-3.5"><summary className="cursor-pointer list-none text-sm font-semibold text-slate-700"><span className="font-mono text-xs text-indigo-700">{name.replace('-report.json', '').replace('.json', '')}</span></summary><dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 border-t border-slate-200 pt-3 text-xs">{Object.entries(report).filter(([, value]) => typeof value !== 'object').map(([key, value]) => <div key={key}><dt className="text-slate-400">{key.replaceAll('_', ' ')}</dt><dd className="mt-0.5 break-words font-medium text-slate-700">{String(value)}</dd></div>)}</dl></details>)}</div></Card>
|
||||
<Card title="Evidence manifest" right={<span className="text-xs text-slate-400">{pack.files.length} bound files</span>}><div className="max-h-[430px] space-y-2 overflow-y-auto pr-1">{pack.files.map((file) => <div key={file.path} className="rounded-xl border border-slate-100 bg-slate-50/70 p-3"><div className="break-all font-mono text-xs font-medium text-slate-700">{file.path}</div><div className="mt-1.5 flex justify-between gap-3 font-mono text-[10px] text-slate-400"><span>{shortHash(file.sha256)}</span><span>{file.bytes.toLocaleString()} B</span></div></div>)}</div></Card>
|
||||
</div>
|
||||
<Card title="Decision log"><pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-xl bg-slate-950 p-4 font-mono text-xs leading-5 text-slate-200">{pack.decision_log ?? 'No decision log was recorded.'}</pre></Card>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function EvidencePacks() {
|
||||
const { data, isLoading, isError } = useQuery({ queryKey: ['evidence-packs'], queryFn: api.evidencePacks });
|
||||
const [selected, setSelected] = useState('');
|
||||
const activeId = selected || data?.packs[0]?.run_id || '';
|
||||
const detail = useQuery({ queryKey: ['evidence-pack', activeId], queryFn: () => api.evidencePack(activeId), enabled: Boolean(activeId) });
|
||||
const heading = useMemo(() => data?.packs.length ?? 0, [data]);
|
||||
if (isLoading) return <div className="text-slate-500">Loading evidence inventory…</div>;
|
||||
if (isError || !data) return <div className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-rose-700">Evidence inventory could not be loaded.</div>;
|
||||
if (!data.packs.length) return <Card title="Evidence packs"><div className="py-12 text-center"><div className="text-lg font-semibold text-slate-800">No evidence pack has been created yet</div><p className="mx-auto mt-2 max-w-lg text-sm leading-6 text-slate-500">Create one with <code className="rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs">casan pack <run-id></code>; it will appear here only after the on-disk manifest is available.</p></div></Card>;
|
||||
return <div className="space-y-5"><div><div className="text-[11px] font-bold uppercase tracking-[0.16em] text-indigo-600">Assurance workspace</div><h1 className="mt-1 text-2xl font-semibold tracking-tight text-slate-900">Evidence packs <span className="text-slate-400">({heading})</span></h1><p className="mt-1 text-sm text-slate-500">Inspect certification, provenance, gates and immutable file hashes without opening raw logs.</p></div><div className="grid gap-5 xl:grid-cols-[288px_minmax(0,1fr)]"><aside className="rounded-2xl border border-slate-200 bg-white p-2 shadow-sm"><div className="px-3 py-2 text-[11px] font-bold uppercase tracking-[0.13em] text-slate-400">Available packs</div><div className="space-y-1.5">{data.packs.map((pack) => <PackRow key={pack.run_id} pack={pack} selected={pack.run_id === activeId} onSelect={() => setSelected(pack.run_id)} />)}</div></aside><main>{detail.isLoading && <div className="text-slate-500">Verifying manifest…</div>}{detail.isError && <div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">The selected evidence pack could not be read safely.</div>}{detail.data && <Detail pack={detail.data} />}</main></div></div>;
|
||||
}
|
||||
Reference in New Issue
Block a user