Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,8 +19,9 @@ __pycache__/
htmlcov/
coverage.xml

# generated evidence packs (served locally via PACKS_DIR)
runs/
# generated evidence packs (served locally via PACKS_DIR) — root-anchored so it
# does not also ignore dashboard/app/runs/ (the /runs/[run_id] route)
/runs/

# node
node_modules/
Expand All@@ -37,3 +38,6 @@ mongo-build-plan.md
.claude/
.codex/
.gstack/

# local design reference images (visual specs, not pushed)
design-refs/
3 changes: 3 additions & 0 deletions dashboard/.eslintrc.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
193 changes: 193 additions & 0 deletions dashboard/app/audit/audit.module.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
.main {
max-width: 1080px;
margin: 0 auto;
padding: 32px 28px 64px;
display: flex;
flex-direction: column;
gap: 18px;
}

.head {
display: flex;
flex-direction: column;
gap: 8px;
}

.titleRow {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}

.title {
font-family: var(--mono);
font-size: 22px;
font-weight: 700;
color: var(--text);
}

.sub {
font-size: 14px;
color: var(--text-dim);
line-height: 1.6;
}

.sub code {
font-family: var(--mono);
color: var(--text);
}

.link {
color: var(--cyan);
}

.notice {
color: var(--text-faint);
font-style: italic;
}

.section {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
}

.sectionHead {
display: flex;
align-items: center;
gap: 9px;
font-family: var(--mono);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-dim);
}

.sectionIcon {
color: var(--text-faint);
display: inline-flex;
}

.sectionBody {
display: flex;
flex-direction: column;
gap: 10px;
}

.row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}

.ok {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 13px;
color: var(--green);
}

.bad {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 13px;
color: var(--red);
}

.field {
display: flex;
flex-direction: column;
gap: 2px;
}

.fieldKey {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-faint);
}

.fieldVal {
font-size: 13px;
color: var(--text);
}

.fieldValMono {
font-family: var(--mono);
font-size: 13px;
color: var(--text);
}

.empty {
font-size: 13px;
color: var(--text-faint);
font-style: italic;
}

.trace {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}

.traceItem {
display: grid;
grid-template-columns: 90px 130px 1fr;
gap: 12px;
align-items: baseline;
padding: 8px 12px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-left-width: 2px;
border-radius: 4px;
}

.traceItem[data-status="ok"] {
border-left-color: var(--green);
}
.traceItem[data-status="drift"] {
border-left-color: var(--amber);
}
.traceItem[data-status="failed"] {
border-left-color: var(--red);
}

.traceStage {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text);
}

.traceActor {
font-family: var(--mono);
font-size: 11px;
color: var(--cyan);
}

.traceSummary {
font-size: 12px;
color: var(--text-dim);
line-height: 1.45;
}

@media (max-width: 640px) {
.main {
padding: 24px 18px 48px;
}
.traceItem {
grid-template-columns: 1fr;
gap: 4px;
}
}
142 changes: 142 additions & 0 deletions dashboard/app/audit/page.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
import Link from "next/link";
import {
CheckCircle,
XCircle,
SealCheck,
ListChecks,
Scroll,
} from "@phosphor-icons/react/dist/ssr";
import { loadPack } from "@/lib/api";
import { displayStatus, shortHash } from "@/lib/evidence";
import { StatusPill } from "@/components/StatusPill";
import styles from "./audit.module.css";

export const dynamic = "force-dynamic";

// Audit & Compliance (Layer 1): approvals, verification trail, trace items, and
// policy events. Policy events are not part of EvidencePack v1, so that section
// shows an honest empty state.
export default async function AuditPage({
searchParams,
}: {
searchParams: Promise<{ run_id?: string }>;
}) {
const { run_id } = await searchParams;
const { pack, source } = await loadPack(run_id);
const ds = displayStatus(pack);

const decision = pack.decision;
const gate = pack.approval_gate;
const trace = pack.agent_trace ?? [];
const verified = ds.key === "verified";
const verificationFailed = ds.key === "verification-failed";

return (
<main className={styles.main}>
<header className={styles.head}>
<div className={styles.titleRow}>
<h1 className={styles.title}>Audit &amp; Compliance</h1>
<StatusPill status={ds.key} label={ds.label} />
</div>
<p className={styles.sub}>
Approval and verification trail for run <code>{pack.run_id}</code>.{" "}
<Link href={`/runs/${encodeURIComponent(pack.run_id)}`} className={styles.link}>
Open in Run Review
</Link>
.
{source === "fallback" && (
<span className={styles.notice}> Showing the bundled example pack.</span>
)}
</p>
</header>

{/* approvals */}
<Section icon={<ListChecks />} title="Approvals">
{decision ? (
<div className={styles.row}>
<span className={styles.ok}>
<CheckCircle /> Decision recorded
</span>
<Field k="approver" v={String(decision.approved_by ?? "—")} />
<Field k="approved at" v={String(decision.approved_at ?? "—")} />
<Field k="bound hash" v={shortHash(decision.evidence_hash)} mono />
</div>
) : gate ? (
<p className={styles.empty}>
Gate state: {gate.state}. No approval decision recorded yet for this run.
</p>
) : (
<p className={styles.empty}>No approval recorded for this run.</p>
)}
</Section>

{/* verification trail */}
<Section icon={<SealCheck />} title="Verification trail">
{verified ? (
<span className={styles.ok}>
<CheckCircle /> Re-explain after apply confirmed the fix (SORT removed).
</span>
) : verificationFailed ? (
<span className={styles.bad}>
<XCircle /> Apply ran but re-explain did not confirm the fix — see trace.
</span>
) : (
<p className={styles.empty}>
Not yet verified. Verification runs after a hash-bound approval applies the index.
</p>
)}
</Section>

{/* trace items */}
<Section icon={<Scroll />} title="Trace">
{trace.length === 0 ? (
<p className={styles.empty}>No trace events recorded for this run.</p>
) : (
<ul className={styles.trace}>
{trace.map((t, i) => (
<li key={i} className={styles.traceItem} data-status={t.status}>
<span className={styles.traceStage}>{t.stage}</span>
<span className={styles.traceActor}>{t.actor.replace(/_/g, " ")}</span>
<span className={styles.traceSummary}>{t.summary}</span>
</li>
))}
</ul>
)}
</Section>

{/* policy events — not in EvidencePack v1 */}
<Section icon={<ListChecks />} title="Policy events">
<p className={styles.empty}>No policy events recorded for this run.</p>
</Section>
</main>
);
}

function Section({
icon,
title,
children,
}: {
icon: React.ReactNode;
title: string;
children: React.ReactNode;
}) {
return (
<section className={styles.section}>
<h2 className={styles.sectionHead}>
<span className={styles.sectionIcon}>{icon}</span>
{title}
</h2>
<div className={styles.sectionBody}>{children}</div>
</section>
);
}

function Field({ k, v, mono }: { k: string; v: string; mono?: boolean }) {
return (
<span className={styles.field}>
<span className={styles.fieldKey}>{k}</span>
<span className={mono ? styles.fieldValMono : styles.fieldVal}>{v}</span>
</span>
);
}
Loading
Loading