feat: redesign assurance reports for production

This commit is contained in:
thanhnv
2026-07-28 22:17:28 +07:00
parent cce3cbfd42
commit 3cc897c5bb
8 changed files with 878 additions and 309 deletions
@@ -11,51 +11,113 @@ const number = (value: number): string => new Intl.NumberFormat('en-US').format(
const money = (value: number): string => `$${value.toFixed(6)}`;
function breakdownRows(rows: H6Breakdown[]): string {
if (rows.length === 0) return '<tr><td colspan="6" class="empty">No records in the selected scope.</td></tr>';
if (rows.length === 0) return '<tr><td colspan="6" class="empty">No records in the selected evidence boundary.</td></tr>';
return rows.map((row) => `<tr>
<td>${escapeHtml(row.key)}</td><td>${number(row.runs)}</td><td>${number(row.failures)}</td>
<td>${number(row.latency_avg_ms)} ms</td><td>${row.tokens === null ? 'Unavailable' : number(row.tokens)}</td><td>${row.cost_usd === null ? 'Unavailable' : money(row.cost_usd)}</td>
<td><strong>${escapeHtml(row.key)}</strong></td>
<td class="numeric">${number(row.runs)}</td>
<td class="numeric ${row.failures > 0 ? 'negative' : ''}">${number(row.failures)}</td>
<td class="numeric">${number(row.latency_avg_ms)} ms</td>
<td class="numeric">${row.tokens === null ? '<span class="unavailable">Unavailable</span>' : number(row.tokens)}</td>
<td class="numeric">${row.cost_usd === null ? '<span class="unavailable">Unavailable</span>' : money(row.cost_usd)}</td>
</tr>`).join('');
}
function coverage(label: string, value: number, detail: string): string {
const bounded = Math.min(Math.max(value, 0), 100);
const tone = bounded >= 95 ? 'good' : bounded > 0 ? 'warn' : 'empty';
return `<div class="coverage">
<div class="coverage-head"><span>${escapeHtml(label)}</span><strong>${bounded}%</strong></div>
<div class="track"><i class="${tone}" style="width:${bounded}%"></i></div>
<small>${escapeHtml(detail)}</small>
</div>`;
}
export function renderH6ReportHtml(report: H6Report): string {
const verdictClass = report.verdict === 'pass' ? 'pass' : report.verdict === 'fail' ? 'fail' : report.verdict === 'attention' ? 'attention' : 'neutral';
const scope = [
report.scope.project ? `Project: ${report.scope.project}` : 'All projects',
report.scope.run ? `Run: ${report.scope.run}` : 'All runs',
report.scope.from ? `From: ${report.scope.from}` : null,
report.scope.to ? `To: ${report.scope.to}` : null,
report.scope.project ? `Project · ${report.scope.project}` : 'All projects',
report.scope.run ? `Run · ${report.scope.run}` : 'All runs',
report.scope.from ? `From · ${report.scope.from}` : null,
report.scope.to ? `To · ${report.scope.to}` : null,
].filter((value): value is string => Boolean(value));
const findings = report.findings.length
? report.findings.map((finding) => `<li class="${finding.severity}"><strong>${escapeHtml(finding.code)}</strong><span>${escapeHtml(finding.message)}</span></li>`).join('')
: '<li class="info"><strong>NO_FINDINGS</strong><span>No threshold breach was detected in this scope.</span></li>';
const sourceRows = report.evidence_sources.map((source) => `<tr>
<td>${escapeHtml(source.source)}</td><td>${source.present ? 'present' : 'missing'}</td><td>${source.stale ? 'stale' : 'fresh'}</td>
<td>${source.age_s === null ? '—' : `${number(source.age_s)} s`}</td><td>${number(source.records)}</td><td><code>${escapeHtml(source.path)}</code></td>
</tr>`).join('');
? report.findings.map((finding, index) => `<article class="finding ${escapeHtml(finding.severity)}">
<div class="finding-index">${String(index + 1).padStart(2, '0')}</div>
<div><div class="finding-head"><strong>${escapeHtml(finding.code)}</strong>${finding.metric ? `<code>${escapeHtml(finding.metric)}: ${escapeHtml(finding.value)}${finding.threshold !== undefined ? ` / ${escapeHtml(finding.threshold)}` : ''}</code>` : ''}</div><p>${escapeHtml(finding.message)}</p></div>
</article>`).join('')
: '<article class="finding success"><div class="finding-index">✓</div><div><div class="finding-head"><strong>NO_THRESHOLD_BREACH</strong></div><p>No threshold breach was detected in this evidence boundary.</p></div></article>';
const sourceRows = report.evidence_sources.map((source) => {
const status = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return `<tr>
<td><strong>${escapeHtml(source.source)}</strong></td>
<td><span class="status ${status}">${status}</span></td>
<td class="numeric">${source.age_s === null ? 'Unavailable' : `${number(source.age_s)} s`}</td>
<td class="numeric">${number(source.records)}</td>
<td><code>${escapeHtml(source.path)}</code></td>
</tr>`;
}).join('');
const warnings = report.data_quality.warnings.length
? `<ul>${report.data_quality.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
: '<p>No data-quality warning.</p>';
? report.data_quality.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')
: '<li class="positive">All required sources are present and no estimation warning was detected.</li>';
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(report.title)} · ${escapeHtml(report.report_id)}</title>
<style>
:root{color-scheme:light;--ink:#172033;--muted:#64748b;--line:#dbe2ec;--paper:#fff;--wash:#f4f6fa;--indigo:#4f46e5;--teal:#0f766e}
*{box-sizing:border-box}body{margin:0;background:var(--wash);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.45}
main{max-width:1180px;margin:0 auto;padding:42px 28px 72px}.hero{position:relative;overflow:hidden;border:1px solid #cbd5e1;border-radius:20px;background:#111827;color:#f8fafc;padding:30px;box-shadow:0 24px 50px rgba(15,23,42,.14)}
.hero:after{content:"";position:absolute;right:-90px;top:-110px;width:300px;height:300px;border-radius:999px;background:radial-gradient(circle,#6366f1 0,transparent 68%);opacity:.5}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:#a5b4fc;font-weight:800}h1{font-size:34px;line-height:1.1;margin:8px 0}.scope{display:flex;gap:8px;flex-wrap:wrap;color:#cbd5e1;font-size:12px}.scope span{border:1px solid #334155;border-radius:999px;padding:6px 10px;background:#0f172a}.verdict{position:relative;z-index:1;display:inline-flex;margin-top:22px;border-radius:999px;padding:9px 14px;font-size:12px;text-transform:uppercase;letter-spacing:.12em;font-weight:900}.verdict.pass{background:#d1fae5;color:#065f46}.verdict.fail{background:#fee2e2;color:#991b1b}.verdict.attention{background:#fef3c7;color:#92400e}.verdict.neutral{background:#e2e8f0;color:#334155}
.meta{margin-top:10px;color:#94a3b8;font-size:12px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:18px}.card,.section{background:var(--paper);border:1px solid var(--line);border-radius:16px;box-shadow:0 5px 14px rgba(15,23,42,.04)}.card{padding:17px}.card span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.1em;font-weight:700}.card strong{display:block;margin-top:6px;font-size:26px}.section{padding:22px;margin-top:18px}h2{font-size:16px;margin:0 0 14px}table{border-collapse:collapse;width:100%;font-size:12px}th{text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.1em;padding:9px;border-bottom:1px solid var(--line)}td{padding:10px 9px;border-bottom:1px solid #edf1f5;vertical-align:top}.empty{color:var(--muted);text-align:center;padding:24px}code{font-size:11px;word-break:break-all}.findings{list-style:none;padding:0;margin:0;display:grid;gap:9px}.findings li{display:grid;grid-template-columns:190px 1fr;gap:12px;border-left:4px solid #94a3b8;background:#f8fafc;padding:11px 13px;border-radius:8px}.findings li.warning{border-color:#f59e0b}.findings li.critical{border-color:#ef4444}.findings span{color:#475569}.quality{border-left:4px solid var(--indigo);background:#eef2ff;padding:14px 16px;border-radius:10px;font-size:13px}.quality p,.quality ul{margin:0}.quality ul{padding-left:18px}.footer{margin-top:24px;color:var(--muted);font-size:11px;text-align:center}
@media(max-width:800px){main{padding:20px 14px 50px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.hero{padding:22px}h1{font-size:28px}.table-wrap{overflow:auto}.findings li{grid-template-columns:1fr}}
@media print{body{background:#fff}main{max-width:none;padding:0}.hero,.card,.section{box-shadow:none}.hero{background:#111827!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}.section{break-inside:avoid}.footer{margin-top:10px}}
:root{color-scheme:light;--ink:#101828;--muted:#667085;--soft:#98a2b3;--line:#e4e7ec;--paper:#fff;--canvas:#f2f4f7;--navy:#0b1220;--cyan:#06b6d4;--green:#12b76a;--amber:#f79009;--red:#f04438}
*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;color:var(--ink);font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}
.cover{position:relative;overflow:hidden;border-left:7px solid var(--cyan);border-radius:22px;background:var(--navy);color:#fff;padding:30px 32px;box-shadow:0 24px 64px rgba(16,24,40,.18)}.cover:after{position:absolute;inset:0;content:"";opacity:.12;background-image:linear-gradient(rgba(255,255,255,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.2) 1px,transparent 1px);background-size:32px 32px}.cover>*{position:relative;z-index:1}
.eyebrow{color:#67e8f9;font-size:10px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}.cover h1{max-width:800px;margin:12px 0 8px;font-size:36px;line-height:1.08;letter-spacing:-.035em}.cover p{max-width:720px;margin:0;color:#b3b8c3;font-size:12px}.scope{display:flex;flex-wrap:wrap;gap:7px;margin-top:22px}.scope span{border:1px solid #344054;border-radius:999px;background:#111827;padding:6px 9px;color:#d0d5dd;font:9px ui-monospace,SFMono-Regular,Menlo,monospace}
.cover-foot{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-top:26px}.verdict{display:inline-flex;align-items:center;gap:8px;border:1px solid #344054;border-radius:999px;padding:8px 13px;font-size:10px;font-weight:900;letter-spacing:.15em;text-transform:uppercase}.verdict:before{width:7px;height:7px;border-radius:99px;background:#98a2b3;content:""}.verdict.pass:before{background:var(--green)}.verdict.attention:before{background:var(--amber)}.verdict.fail:before{background:var(--red)}.document-meta{color:#98a2b3;font:9px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:right}.document-meta strong{display:block;margin-top:3px;color:#e4e7ec;font-size:10px}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px}.metric{min-height:112px;border:1px solid var(--line);border-radius:15px;background:var(--paper);padding:16px;box-shadow:0 8px 22px rgba(16,24,40,.045)}.metric span{display:block;color:var(--muted);font-size:9px;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.metric strong{display:block;margin-top:9px;font-size:22px;line-height:1.15;letter-spacing:-.025em}.metric small{display:block;margin-top:6px;color:var(--soft);font-size:10px}.unavailable{color:#b54708;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-weight:700}
.section{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #f2f4f7;padding:17px 20px}.kicker{color:#087e8b;font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.section-head h2{margin:3px 0 0;font-size:16px;letter-spacing:-.01em}.section-head p{max-width:580px;margin:3px 0 0;color:var(--muted);font-size:10px}.section-body{padding:18px 20px}
.findings{display:grid;gap:10px}.finding{display:grid;grid-template-columns:38px minmax(0,1fr);gap:12px;border:1px solid #eaecf0;border-left:4px solid var(--cyan);border-radius:10px;padding:12px}.finding.warning{border-left-color:var(--amber);background:#fffcf5}.finding.critical{border-left-color:var(--red);background:#fffbfa}.finding.success{border-left-color:var(--green);background:#f6fef9}.finding-index{color:#98a2b3;font:15px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.finding-head strong{font:10px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head code{color:#667085;font-size:8px}.finding p{margin:5px 0 0;color:#475467;font-size:11px}
.quality-grid{display:grid;grid-template-columns:.75fr 1.25fr;gap:20px}.coverage+.coverage{margin-top:20px}.coverage-head{display:flex;justify-content:space-between;gap:12px}.coverage-head span{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.coverage-head strong{font:13px ui-monospace,SFMono-Regular,Menlo,monospace}.track{height:6px;margin-top:7px;overflow:hidden;border-radius:99px;background:#f2f4f7}.track i{display:block;height:100%;border-radius:99px;background:#d0d5dd}.track i.good{background:var(--green)}.track i.warn{background:var(--amber)}.coverage small{display:block;margin-top:6px;color:#98a2b3}.disclosure{border:1px solid #eaecf0;border-radius:12px;background:#f9fafb;padding:14px}.disclosure strong{font-size:10px;text-transform:uppercase;letter-spacing:.1em}.disclosure ul{margin:10px 0 0;padding-left:17px;color:#475467;font-size:10px}.disclosure li+li{margin-top:6px}.disclosure .positive{color:#027a48}
.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:10px}th{border-block:1px solid #eaecf0;background:#f9fafb;padding:9px 10px;color:#667085;font-size:8px;letter-spacing:.1em;text-align:left;text-transform:uppercase}td{border-bottom:1px solid #f2f4f7;padding:10px;vertical-align:top}tr:last-child td{border-bottom:0}.numeric{text-align:right;font:9px ui-monospace,SFMono-Regular,Menlo,monospace}.negative{color:#b42318;font-weight:700}td code{display:block;max-width:420px;color:#98a2b3;font-size:8px;word-break:break-all}.status{display:inline-flex;border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;text-transform:uppercase}.status.fresh{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.status.stale{border-color:#fedf89;background:#fffaeb;color:#b54708}.status.missing{border-color:#fecdca;background:#fef3f2;color:#b42318}.empty{padding:30px;color:#98a2b3;text-align:center}
.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-foot{display:block}.document-meta{margin-top:18px;text-align:left}.summary{grid-template-columns:repeat(2,1fr)}.quality-grid{grid-template-columns:1fr}.finding-head{display:block}.finding-head code{display:block;margin-top:4px}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.section,.metric,.finding{break-inside:avoid}.cover h1{font-size:30px}}
</style></head><body><main>
<section class="hero"><div class="eyebrow">CASAN assurance dossier · contract v${report.schema_version}</div><h1>${escapeHtml(report.title)}</h1><div class="scope">${scope.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div><div class="verdict ${verdictClass}">${escapeHtml(report.verdict)}</div><div class="meta">Generated ${escapeHtml(report.generated_at)} · ${escapeHtml(report.report_id)} · Freshness ${escapeHtml(report.freshness.status)}</div></section>
<section class="grid"><div class="card"><span>Governed runs</span><strong>${number(report.summary.runs)}</strong></div><div class="card"><span>Failure rate</span><strong>${report.summary.failure_rate_pct}%</strong></div><div class="card"><span>P95 latency</span><strong>${number(report.summary.latency_ms.p95)} ms</strong></div><div class="card"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : 'Unavailable'}</strong></div><div class="card"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : 'Unavailable'}</strong></div><div class="card"><span>Token coverage</span><strong>${report.summary.coverage.token_pct}%</strong></div><div class="card"><span>Cost coverage</span><strong>${report.summary.coverage.cost_pct}%</strong></div><div class="card"><span>Retries</span><strong>${number(report.summary.retries)}</strong></div></section>
<section class="section"><h2>Verdict findings</h2><ul class="findings">${findings}</ul></section>
<section class="section"><h2>Evidence freshness</h2><div class="table-wrap"><table><thead><tr><th>Source</th><th>Presence</th><th>State</th><th>Age</th><th>Records</th><th>Path</th></tr></thead><tbody>${sourceRows}</tbody></table></div></section>
<section class="section"><h2>Step breakdown</h2><div class="table-wrap"><table><thead><tr><th>Step</th><th>Runs</th><th>Failures</th><th>Avg latency</th><th>Tokens</th><th>Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div></section>
<section class="section"><h2>Provider/model breakdown</h2><div class="table-wrap"><table><thead><tr><th>Provider · model</th><th>Calls</th><th>Failures</th><th>Avg latency</th><th>Tokens</th><th>Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div></section>
<section class="section"><h2>Data quality · ${escapeHtml(report.data_quality.status)}</h2><div class="quality">${warnings}</div></section>
<div class="footer">Generated from CASAN runtime evidence. No maturity score or telemetry value is hard-coded in this document.</div>
<header class="cover">
<div class="eyebrow">CASAN · H6 AgentOps assurance</div>
<h1>Operational evidence, ready for a decision.</h1>
<p>${escapeHtml(report.title)} · Runtime health, reliability, provider usage, cost provenance and telemetry integrity.</p>
<div class="scope">${scope.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div>
<div class="cover-foot">
<div class="verdict ${verdictClass}">${escapeHtml(report.verdict)}</div>
<div class="document-meta">Report ID<strong>${escapeHtml(report.report_id)}</strong>Generated ${escapeHtml(report.generated_at)} · Freshness ${escapeHtml(report.freshness.status)}</div>
</div>
</header>
<section class="summary">
<div class="metric"><span>Governed runs</span><strong>${number(report.summary.runs)}</strong><small>${number(report.summary.success)} successful · ${number(report.summary.degraded)} degraded</small></div>
<div class="metric"><span>Failure rate</span><strong>${report.summary.failure_rate_pct}%</strong><small>${number(report.summary.failed)} failed · ${number(report.summary.alerts)} alerts</small></div>
<div class="metric"><span>P95 latency</span><strong>${number(report.summary.latency_ms.p95)} ms</strong><small>P50 ${number(report.summary.latency_ms.p50)} ms</small></div>
<div class="metric"><span>Data quality</span><strong>${escapeHtml(report.data_quality.status.replaceAll('_', ' '))}</strong><small>Coverage disclosed below</small></div>
<div class="metric"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : '<span class="unavailable">Unavailable</span>'}</strong><small>${report.summary.coverage.token_pct}% record coverage</small></div>
<div class="metric"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : '<span class="unavailable">Unavailable</span>'}</strong><small>${number(report.summary.provider_calls)} provider calls</small></div>
<div class="metric"><span>Cost attribution</span><strong>${report.summary.coverage.cost_pct}%</strong><small>${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records</small></div>
<div class="metric"><span>Retries</span><strong>${number(report.summary.retries)}</strong><small>Observed reroute attempts</small></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Decision brief</div><h2>What requires attention</h2><p>Threshold findings are operational review signals, not maturity claims.</p></div><span class="status ${verdictClass}">${escapeHtml(report.verdict)}</span></div>
<div class="section-body"><div class="findings">${findings}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Evidence quality</div><h2>Coverage and disclosure</h2><p>Unavailable telemetry remains null and is never rendered as zero.</p></div><span class="status">${escapeHtml(report.data_quality.status)}</span></div>
<div class="section-body"><div class="quality-grid"><div>${coverage('Token attribution', report.summary.coverage.token_pct, `${report.summary.coverage.token_records}/${report.summary.coverage.runtime_records} runtime records`)}${coverage('Cost attribution', report.summary.coverage.cost_pct, `${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records`)}</div><div class="disclosure"><strong>Disclosure notes</strong><ul>${warnings}</ul></div></div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Source integrity</div><h2>Evidence provenance</h2><p>Freshness and presence are evaluated independently for every canonical source.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Source</th><th>State</th><th class="numeric">Age</th><th class="numeric">Records</th><th>Canonical path</th></tr></thead><tbody>${sourceRows}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Runtime</div><h2>Execution profile by step</h2></div></div>
<div class="table-wrap"><table><thead><tr><th>Step</th><th class="numeric">Runs</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Provider</div><h2>Model usage and provenance</h2><p>Only provider-reported token and actual cost values are shown.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Provider · model</th><th class="numeric">Calls</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div>
</section>
<footer class="footer"><span>Generated from CASAN runtime evidence. No maturity score or telemetry value is hard-coded in this document.</span><span>Contract v${report.schema_version} · ${escapeHtml(report.report_id)}</span></footer>
</main></body></html>`;
}
@@ -1,4 +1,4 @@
import type { RunAssuranceReport } from './run-report.js';
import type { RunAssuranceReport, RunGateSnapshot } from './run-report.js';
const escapeHtml = (value: unknown): string => String(value ?? '')
.replaceAll('&', '&amp;')
@@ -14,44 +14,91 @@ const badgeClass = (status: string): string => {
return 'fail';
};
const evidenceFields = (gate: RunGateSnapshot): string => {
const entries = Object.entries(gate.evidence);
if (entries.length === 0) return '<div class="empty-evidence">No safe evidence fields were emitted.</div>';
return `<dl class="manifest">${entries.map(([key, value]) => `
<div><dt>${escapeHtml(key)}</dt><dd>${escapeHtml(
typeof value === 'string' ? value : JSON.stringify(value),
)}</dd></div>`).join('')}</dl>`;
};
export function renderRunAssuranceHtml(report: RunAssuranceReport): string {
const gates = report.gates.map((gate) => `
<article class="gate">
<div class="gate-head"><strong>${escapeHtml(gate.title)}</strong><span class="badge ${badgeClass(gate.status)}">${escapeHtml(gate.status)}</span></div>
<p>${escapeHtml(gate.reason)}</p>
<small>${escapeHtml(gate.updated_at ?? 'No timestamp')}</small>
<details><summary>Evidence fields</summary><pre>${escapeHtml(JSON.stringify(gate.evidence, null, 2))}</pre></details>
</article>`).join('');
const tokenValue = report.summary.token_usage_available ? 'Available' : 'Unavailable';
const costValue = report.summary.cost_available ? 'Available' : 'Unavailable';
const evidenceState = report.source.trace_found && report.source.graph_found ? 'Verified' : 'Partial';
const gates = report.gates.map((gate, index) => `
<article class="gate ${badgeClass(gate.status)}">
<div class="gate-index">H${index + 1}</div>
<div class="gate-copy">
<div class="gate-heading"><h3>${escapeHtml(gate.title.split(' · ')[1] ?? gate.title)}</h3><span class="badge ${badgeClass(gate.status)}">${escapeHtml(gate.status)}</span></div>
<p class="gate-description">${escapeHtml(gate.description)}</p>
<div class="decision"><span>Latest decision</span><strong>${escapeHtml(gate.reason || 'No decision reason recorded.')}</strong><small>${escapeHtml(gate.updated_at ?? 'No timestamp')}</small></div>
<div class="evidence-title">Evidence fields</div>
${evidenceFields(gate)}
</div>
</article>`).join('');
const spine = report.gates.map((gate, index) => `
<div class="spine-node ${badgeClass(gate.status)}">
<span class="spine-dot">${gate.status === 'pass' ? '✓' : gate.status === 'warning' ? '!' : gate.status === 'queued' ? '○' : '×'}</span>
<strong>H${index + 1}</strong>
<small>${escapeHtml(gate.title.split(' · ')[1] ?? gate.title)}</small>
</div>`).join('');
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>CASAN assurance receipt · ${escapeHtml(report.trace_id)}</title>
<title>CASAN assurance dossier · ${escapeHtml(report.trace_id)}</title>
<style>
:root{color-scheme:light;--ink:#0f172a;--muted:#64748b;--line:#e2e8f0;--panel:#fff;--bg:#f8fafc;--indigo:#4f46e5}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}
main{max-width:1160px;margin:0 auto;padding:40px 24px 64px}.hero{position:relative;overflow:hidden;border-radius:28px;background:#111827;color:#fff;padding:32px;box-shadow:0 24px 60px rgba(15,23,42,.18)}
.hero:after{content:"";position:absolute;right:-80px;top:-100px;width:280px;height:280px;border-radius:50%;background:rgba(99,102,241,.3);filter:blur(50px)}.eyebrow{color:#a5b4fc;font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase}
h1{position:relative;margin:8px 0 2px;font-size:30px;letter-spacing:-.035em}.trace{position:relative;color:#94a3b8;font:12px ui-monospace,SFMono-Regular,monospace;word-break:break-all}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.metric,.section{border:1px solid var(--line);background:var(--panel);border-radius:18px;padding:18px}.metric span{display:block;color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.metric strong{display:block;margin-top:8px;font-size:22px}
.section{margin-top:16px}.section h2{margin:0 0 14px;font-size:16px}.rail{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:10px}.gate{min-width:0;border:1px solid var(--line);border-radius:15px;padding:14px;background:#fff}.gate-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.gate p{min-height:44px;color:#475569;font-size:12px}.gate small{color:#94a3b8;font-size:10px}.gate details{margin-top:10px;color:#64748b;font-size:10px}.gate summary{cursor:pointer;font-weight:700}.gate pre{max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:8px;background:#f8fafc;padding:8px;font-size:9px}.badge{border-radius:999px;padding:3px 7px;font-size:9px;font-weight:800;text-transform:uppercase}.pass{background:#dcfce7;color:#166534}.warn{background:#fef3c7;color:#92400e}.fail{background:#ffe4e6;color:#9f1239}.muted{background:#f1f5f9;color:#64748b}
.note{color:var(--muted);font-size:12px}.footer{margin-top:18px;color:#94a3b8;font-size:11px}
@media(max-width:900px){.summary{grid-template-columns:repeat(2,1fr)}.rail{grid-template-columns:1fr 1fr}}@media(max-width:520px){main{padding:18px 12px}.hero{padding:22px}.rail,.summary{grid-template-columns:1fr}}
@media print{body{background:#fff}main{max-width:none;padding:0}.hero,.metric,.section{box-shadow:none;break-inside:avoid}}
:root{color-scheme:light;--ink:#101828;--muted:#667085;--soft:#98a2b3;--line:#e4e7ec;--paper:#fff;--canvas:#f2f4f7;--navy:#0b1220;--cyan:#06b6d4;--green:#12b76a;--amber:#f79009;--red:#f04438}
*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;color:var(--ink);font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}.cover{position:relative;overflow:hidden;border-left:7px solid var(--cyan);border-radius:22px;background:var(--navy);color:#fff;padding:30px 32px;box-shadow:0 24px 64px rgba(16,24,40,.18)}
.cover:after{position:absolute;inset:0;content:"";opacity:.12;background-image:linear-gradient(rgba(255,255,255,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.2) 1px,transparent 1px);background-size:32px 32px}.cover>*{position:relative;z-index:1}.eyebrow{color:#67e8f9;font-size:10px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}.cover h1{max-width:780px;margin:12px 0 6px;font-size:36px;line-height:1.08;letter-spacing:-.035em}.trace{color:#98a2b3;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
.cover-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:24px;margin-top:26px}.verdict{display:inline-flex;align-items:center;gap:8px;border:1px solid #344054;border-radius:999px;padding:8px 13px;font-size:10px;font-weight:900;letter-spacing:.15em;text-transform:uppercase}.verdict:before{width:7px;height:7px;border-radius:99px;background:var(--cyan);content:""}.verdict.certified:before{background:var(--green)}.verdict.non_certified:before{background:var(--red)}
.document-meta{display:grid;grid-template-columns:repeat(2,minmax(130px,1fr));gap:8px 22px;color:#98a2b3;font-size:10px}.document-meta strong{display:block;margin-top:2px;color:#e4e7ec;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px}.metric{min-height:112px;border:1px solid var(--line);border-radius:15px;background:var(--paper);padding:16px;box-shadow:0 8px 22px rgba(16,24,40,.045)}.metric span{display:block;color:var(--muted);font-size:9px;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.metric strong{display:block;margin-top:9px;font-size:22px;line-height:1.15;letter-spacing:-.025em}.metric small{display:block;margin-top:6px;color:var(--soft);font-size:10px}
.section{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #f2f4f7;padding:17px 20px}.section-head .kicker{color:#087e8b;font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.section-head h2{margin:3px 0 0;font-size:16px;letter-spacing:-.01em}.section-head p{max-width:540px;margin:3px 0 0;color:var(--muted);font-size:10px}
.spine{position:relative;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));padding:24px 18px 20px}.spine:before{position:absolute;left:8%;right:8%;top:43px;height:1px;background:var(--line);content:""}.spine-node{position:relative;text-align:center}.spine-dot{display:flex;width:38px;height:38px;margin:0 auto;align-items:center;justify-content:center;border:2px solid #d0d5dd;border-radius:99px;background:#fff;color:#667085;font-weight:900;box-shadow:0 0 0 5px #fff}.spine-node.pass .spine-dot{border-color:#6ce9a6;color:#027a48}.spine-node.warn .spine-dot{border-color:#fec84b;color:#b54708}.spine-node.fail .spine-dot{border-color:#fda29b;color:#b42318}.spine-node strong{display:block;margin-top:8px;color:#98a2b3;font-size:9px;letter-spacing:.13em}.spine-node small{display:block;margin-top:3px;overflow:hidden;color:#344054;font-size:9px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}
.controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:18px}.gate{display:grid;grid-template-columns:48px minmax(0,1fr);overflow:hidden;border:1px solid var(--line);border-radius:14px;background:#fff;break-inside:avoid}.gate-index{display:flex;align-items:flex-start;justify-content:center;border-right:1px solid var(--line);padding-top:18px;color:#98a2b3;font-size:12px;font-weight:900}.gate.pass{border-top:3px solid var(--green)}.gate.warn{border-top:3px solid var(--amber)}.gate.fail{border-top:3px solid var(--red)}.gate-copy{padding:15px}.gate-heading{display:flex;align-items:center;justify-content:space-between;gap:10px}.gate-heading h3{margin:0;font-size:14px}.badge{border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.badge.pass{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.badge.warn{border-color:#fedf89;background:#fffaeb;color:#b54708}.badge.fail{border-color:#fecdca;background:#fef3f2;color:#b42318}.gate-description{min-height:34px;margin:6px 0;color:var(--muted);font-size:10px}.decision{margin-top:10px;border-left:3px solid #d0d5dd;background:#f9fafb;padding:9px 10px}.decision span,.evidence-title{display:block;color:#667085;font-size:8px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.decision strong{display:block;margin-top:4px;font-size:10px;font-weight:600}.decision small{display:block;margin-top:4px;color:#98a2b3;font:8px ui-monospace,SFMono-Regular,Menlo,monospace}.evidence-title{margin-top:12px}.manifest{margin:6px 0 0;border:1px solid #eaecf0;border-radius:8px}.manifest div{display:grid;grid-template-columns:110px minmax(0,1fr);border-bottom:1px solid #eaecf0}.manifest div:last-child{border-bottom:0}.manifest dt,.manifest dd{margin:0;padding:6px 7px;font:8px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.manifest dt{background:#f9fafb;color:#667085;font-weight:700}.manifest dd{color:#344054}.empty-evidence{margin-top:6px;border:1px dashed #d0d5dd;border-radius:8px;padding:10px;color:#98a2b3;font-size:9px;text-align:center}
.certification{display:grid;grid-template-columns:180px minmax(0,1fr);gap:20px;padding:19px 20px}.certification label{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.certification strong{display:block;margin-top:5px;font-size:15px}.certification p{margin:0;color:#475467;font-size:11px}.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-grid{grid-template-columns:1fr}.summary{grid-template-columns:repeat(2,1fr)}.spine{min-width:720px}.spine-wrap{overflow:auto}.controls{grid-template-columns:1fr}.document-meta{grid-template-columns:1fr 1fr}.certification{grid-template-columns:1fr}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.spine{padding-inline:8px}.controls{grid-template-columns:repeat(2,1fr);padding:12px}.section,.metric,.gate{break-inside:avoid}.footer{position:relative}.cover h1{font-size:30px}}
</style></head><body><main>
<section class="hero"><div class="eyebrow">CASAN · evidence-backed assurance</div><h1>${escapeHtml(report.verdict.replace('_', ' ').toUpperCase())}</h1><div class="trace">${escapeHtml(report.trace_id)}</div></section>
<header class="cover">
<div class="eyebrow">CASAN · independent assurance dossier</div>
<h1>${escapeHtml(report.verdict.replace('_', ' ').toUpperCase())} governed run</h1>
<div class="trace">${escapeHtml(report.trace_id)}</div>
<div class="cover-grid">
<div class="verdict ${escapeHtml(report.verdict)}">${escapeHtml(report.verdict.replace('_', ' '))}</div>
<div class="document-meta">
<div>Report ID<strong>${escapeHtml(report.report_id)}</strong></div>
<div>Generated<strong>${escapeHtml(report.generated_at)}</strong></div>
<div>Project<strong>${escapeHtml(report.project.id)}</strong></div>
<div>Edition / maturity<strong>${escapeHtml(report.project.edition ?? 'unknown')} · ${escapeHtml(report.project.maturity?.status ?? 'not assessed')}</strong></div>
</div>
</div>
</header>
<section class="summary">
<div class="metric"><span>Harness gates</span><strong>${report.summary.gates_observed}/7</strong></div>
<div class="metric"><span>H6 quality</span><strong>${escapeHtml(report.summary.telemetry_quality)}</strong></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong></div>
<div class="metric"><span>Cost</span><strong>${costValue}</strong></div>
<div class="metric"><span>Duration</span><strong>${report.summary.duration_ms === null ? 'Unavailable' : `${report.summary.duration_ms} ms`}</strong></div>
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong></div>
<div class="metric"><span>Evidence source</span><strong>${report.source.trace_found && report.source.graph_found ? 'Verified' : 'Partial'}</strong></div>
<div class="metric"><span>Harness controls</span><strong>${report.summary.gates_observed}/7</strong><small>H1–H7 observed</small></div>
<div class="metric"><span>Evidence source</span><strong>${evidenceState}</strong><small>Trace + graph reconstruction</small></div>
<div class="metric"><span>H6 quality</span><strong>${escapeHtml(report.summary.telemetry_quality)}</strong><small>Telemetry disclosure status</small></div>
<div class="metric"><span>Duration</span><strong>${report.summary.duration_ms === null ? 'Unavailable' : `${report.summary.duration_ms} ms`}</strong><small>End-to-end governed run</small></div>
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong><small>Observed side-effect attempts</small></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong><small>Recorded execution failures</small></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong><small>Never inferred as zero</small></div>
<div class="metric"><span>Actual cost</span><strong>${costValue}</strong><small>Provider attribution only</small></div>
</section>
<section class="section"><h2>Live assurance rail · H1 → H7</h2><div class="rail">${gates}</div></section>
<section class="section"><h2>Certification</h2><p>Strength: <strong>${escapeHtml(report.certification.strength ?? 'unknown')}</strong></p><p class="note">${escapeHtml(report.certification.reasons.join(' · ') || 'No certification reason recorded.')}</p></section>
<div class="footer">Generated ${escapeHtml(report.generated_at)} · Project ${escapeHtml(report.project.id)} · Edition ${escapeHtml(report.project.edition ?? 'unknown')} · Maturity ${escapeHtml(report.project.maturity?.status ?? 'not assessed')}. No maturity score or unavailable telemetry value is hard-coded.</div>
<section class="section">
<div class="section-head"><div><div class="kicker">Control sequence</div><h2>Live assurance rail · H1 → H7</h2><p>The evidence spine shows the terminal state of every governed control.</p></div><span class="badge ${badgeClass(report.verdict)}">${escapeHtml(report.verdict)}</span></div>
<div class="spine-wrap"><div class="spine">${spine}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Control dossiers</div><h2>Decision and evidence detail</h2><p>Every field below is sanitized before persistence. Raw prompts and tool output are excluded.</p></div></div>
<div class="controls">${gates}</div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Certification</div><h2>Assurance conclusion</h2></div></div>
<div class="certification"><div><label>Strength</label><strong>${escapeHtml(report.certification.strength ?? 'unknown')}</strong></div><p>${escapeHtml(report.certification.reasons.join(' · ') || 'No certification reason recorded.')}</p></div>
</section>
<footer class="footer"><span>Generated from CASAN runtime evidence. Unavailable values remain null.</span><span>Schema v${report.schema_version} · ${escapeHtml(report.trace_id)}</span></footer>
</main></body></html>`;
}
@@ -144,6 +144,9 @@ test('HTML export is standalone, escaped and contains no hard-coded maturity sco
assert.doesNotMatch(html, /<script>alert\(1\)<\/script>/);
assert.doesNotMatch(html, /Average\s+\d|\/100|218 core tests/i);
assert.match(html, /No maturity score or telemetry value is hard-coded/);
assert.match(html, /Operational evidence, ready for a decision/);
assert.match(html, /Evidence provenance/);
assert.match(html, /@page\{size:A4/);
});
test('per-run assurance export carries H1-H7 and truthful H6 availability', () => {
@@ -188,6 +191,8 @@ test('per-run assurance export carries H1-H7 and truthful H6 availability', () =
const html = renderRunAssuranceHtml(report);
assert.match(html, /Live assurance rail · H1 → H7/);
assert.match(html, /Evidence fields/);
assert.match(html, /Decision and evidence detail/);
assert.match(html, /@page\{size:A4/);
assert.match(html, /Unavailable/);
assert.doesNotMatch(html, /\$0(?:\.0+)?/);
});
@@ -0,0 +1,176 @@
import type { ReactNode } from 'react';
type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
const TONE: Record<Tone, {
badge: string;
line: string;
value: string;
wash: string;
}> = {
neutral: {
badge: 'border-slate-600 bg-slate-800 text-slate-200',
line: 'bg-slate-500',
value: 'text-slate-950',
wash: 'border-slate-200 bg-white',
},
success: {
badge: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-200',
line: 'bg-emerald-500',
value: 'text-emerald-700',
wash: 'border-emerald-200 bg-emerald-50/45',
},
warning: {
badge: 'border-amber-400/40 bg-amber-400/10 text-amber-100',
line: 'bg-amber-500',
value: 'text-amber-700',
wash: 'border-amber-200 bg-amber-50/45',
},
danger: {
badge: 'border-rose-400/40 bg-rose-400/10 text-rose-100',
line: 'bg-rose-500',
value: 'text-rose-700',
wash: 'border-rose-200 bg-rose-50/45',
},
info: {
badge: 'border-cyan-400/40 bg-cyan-400/10 text-cyan-100',
line: 'bg-cyan-500',
value: 'text-cyan-700',
wash: 'border-cyan-200 bg-cyan-50/45',
},
};
export function reportTone(value: string): Tone {
const normalized = value.toLowerCase();
if (['pass', 'fresh', 'certified', 'success', 'verified'].includes(normalized)) return 'success';
if (['attention', 'warning', 'warn', 'stale', 'insufficient', 'degraded'].includes(normalized)) return 'warning';
if (['fail', 'failed', 'error', 'blocked', 'critical'].includes(normalized)) return 'danger';
if (['running', 'live', 'streaming'].includes(normalized)) return 'info';
return 'neutral';
}
export function ReportHero({
eyebrow,
title,
description,
verdict,
meta,
actions,
}: {
eyebrow: string;
title: string;
description: string;
verdict?: string;
meta?: ReactNode;
actions?: ReactNode;
}) {
const tone = reportTone(verdict ?? 'neutral');
return (
<section className="relative overflow-hidden rounded-[1.75rem] border border-slate-800 bg-slate-950 text-white shadow-[0_28px_80px_rgba(15,23,42,0.22)]">
<div className="pointer-events-none absolute inset-0 opacity-[0.16] [background-image:linear-gradient(rgba(148,163,184,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.18)_1px,transparent_1px)] [background-size:32px_32px]" />
<div className={`absolute inset-y-0 left-0 w-1.5 ${TONE[tone].line}`} />
<div className="relative grid gap-8 px-5 py-6 sm:px-8 sm:py-8 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
<div>
<div className="flex flex-wrap items-center gap-3">
<span className="text-[10px] font-black uppercase tracking-[0.24em] text-cyan-300">{eyebrow}</span>
<span className="h-px w-10 bg-slate-700" />
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Evidence-backed</span>
</div>
<h1 className="mt-4 max-w-4xl text-[2rem] font-semibold leading-[1.08] tracking-[-0.035em] text-white sm:text-[2.65rem]">{title}</h1>
<p className="mt-4 max-w-3xl text-sm leading-6 text-slate-300 sm:text-[15px]">{description}</p>
{meta && <div className="mt-5 flex flex-wrap items-center gap-x-5 gap-y-2 font-mono text-[11px] text-slate-400">{meta}</div>}
</div>
<div className="flex flex-col items-start gap-3 xl:items-end">
{verdict && (
<span className={`inline-flex min-h-9 items-center rounded-full border px-4 py-2 text-[11px] font-black uppercase tracking-[0.18em] ${TONE[tone].badge}`}>
<span className={`mr-2 h-1.5 w-1.5 rounded-full ${TONE[tone].line}`} />
{verdict.replaceAll('_', ' ')}
</span>
)}
{actions && <div className="flex flex-wrap gap-2 xl:justify-end">{actions}</div>}
</div>
</div>
</section>
);
}
export function ReportPanel({
eyebrow,
title,
description,
right,
children,
className = '',
}: {
eyebrow?: string;
title: string;
description?: string;
right?: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<section className={`overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-[0_14px_36px_rgba(15,23,42,0.055)] ${className}`}>
<header className="flex flex-col gap-4 border-b border-slate-100 px-5 py-5 sm:flex-row sm:items-start sm:justify-between sm:px-6">
<div>
{eyebrow && <p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-700">{eyebrow}</p>}
<h2 className={`${eyebrow ? 'mt-1.5' : ''} text-base font-semibold tracking-[-0.01em] text-slate-950`}>{title}</h2>
{description && <p className="mt-1.5 max-w-3xl text-xs leading-5 text-slate-500">{description}</p>}
</div>
{right}
</header>
<div className="p-5 sm:p-6">{children}</div>
</section>
);
}
export function SignalMetric({
label,
value,
detail,
tone = 'neutral',
primary = false,
}: {
label: string;
value: ReactNode;
detail?: ReactNode;
tone?: Tone;
primary?: boolean;
}) {
return (
<div className={`relative min-h-[8.25rem] overflow-hidden rounded-2xl border p-4 sm:p-5 ${TONE[tone].wash}`}>
<span className={`absolute inset-y-0 left-0 w-1 ${TONE[tone].line}`} />
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">{label}</p>
<div className={`mt-3 font-semibold tracking-[-0.035em] ${primary ? 'text-3xl sm:text-[2.1rem]' : 'text-2xl'} ${TONE[tone].value}`}>{value}</div>
{detail && <div className="mt-2 text-xs leading-5 text-slate-500">{detail}</div>}
</div>
);
}
export function CoverageBar({
label,
value,
detail,
}: {
label: string;
value: number;
detail: string;
}) {
const bounded = Math.min(Math.max(value, 0), 100);
const tone = bounded >= 95 ? 'bg-emerald-500' : bounded >= 70 ? 'bg-cyan-500' : bounded > 0 ? 'bg-amber-500' : 'bg-slate-300';
return (
<div>
<div className="flex items-baseline justify-between gap-4">
<span className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-500">{label}</span>
<span className="font-mono text-sm font-semibold text-slate-900">{bounded}%</span>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-slate-100">
<div className={`h-full rounded-full ${tone}`} style={{ width: `${bounded}%` }} />
</div>
<p className="mt-2 text-xs text-slate-500">{detail}</p>
</div>
);
}
export const reportActionPrimary = 'inline-flex min-h-11 items-center justify-center rounded-xl bg-white px-4 py-2.5 text-xs font-bold text-slate-950 shadow-sm transition hover:-translate-y-px hover:bg-cyan-50 focus:outline-none focus:ring-2 focus:ring-cyan-300';
export const reportActionSecondary = 'inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-2.5 text-xs font-bold text-slate-200 transition hover:-translate-y-px hover:border-slate-500 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-400';
@@ -1,13 +1,18 @@
import type { HarnessGateStatus, HarnessTraceGraph } from '../../lib/api';
const TONE: Record<HarnessGateStatus, string> = {
queued: 'border-slate-200 bg-slate-100 text-slate-400',
running: 'border-blue-300 bg-blue-50 text-blue-700',
pass: 'border-emerald-300 bg-emerald-50 text-emerald-700',
warning: 'border-amber-300 bg-amber-50 text-amber-700',
blocked: 'border-rose-300 bg-rose-50 text-rose-700',
error: 'border-rose-300 bg-rose-50 text-rose-700',
skipped: 'border-slate-200 bg-slate-100 text-slate-400',
const TONE: Record<HarnessGateStatus, {
ring: string;
dot: string;
text: string;
surface: string;
}> = {
queued: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
running: { ring: 'border-cyan-400', dot: 'bg-cyan-400', text: 'text-cyan-800', surface: 'bg-cyan-50' },
pass: { ring: 'border-emerald-400', dot: 'bg-emerald-500', text: 'text-emerald-800', surface: 'bg-emerald-50' },
warning: { ring: 'border-amber-400', dot: 'bg-amber-500', text: 'text-amber-800', surface: 'bg-amber-50' },
blocked: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
error: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
skipped: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
};
const MARK: Record<HarnessGateStatus, string> = {
@@ -20,22 +25,47 @@ const MARK: Record<HarnessGateStatus, string> = {
skipped: '–',
};
export function AssuranceRail({ graph }: { graph: HarnessTraceGraph }) {
export function AssuranceRail({
graph,
selectedId,
onSelect,
}: {
graph: HarnessTraceGraph;
selectedId?: string;
onSelect?: (id: string) => void;
}) {
return (
<div className="overflow-x-auto pb-1">
<div className="flex min-w-[760px] items-center">
{graph.nodes.map((node, index) => (
<div key={node.id} className="flex min-w-0 flex-1 items-center">
<div className={`min-w-0 flex-1 rounded-xl border p-3 ${TONE[node.status]}`}>
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-black uppercase tracking-[0.12em]">{node.title.split(' · ')[0]}</span>
<span className={`flex h-5 w-5 items-center justify-center rounded-full border border-current text-[10px] ${node.status === 'running' ? 'animate-pulse' : ''}`}>{MARK[node.status]}</span>
<div className="overflow-x-auto pb-2">
<div className="relative min-w-[860px] px-3 pt-2">
<div className="absolute left-[4.3rem] right-[4.3rem] top-[2.05rem] h-px bg-slate-200" aria-hidden="true" />
<div className="relative grid grid-cols-7">
{graph.nodes.map((node, index) => {
const tone = TONE[node.status];
const selected = selectedId === node.id;
const Element = onSelect ? 'button' : 'div';
return (
<Element
key={node.id}
{...(onSelect ? {
type: 'button' as const,
onClick: () => onSelect(node.id),
'aria-pressed': selected,
} : {})}
className={`group min-w-0 px-1.5 text-center ${onSelect ? 'cursor-pointer focus:outline-none' : ''}`}
>
<div className={`relative mx-auto flex h-11 w-11 items-center justify-center rounded-full border-2 bg-white text-sm font-black shadow-[0_0_0_5px_white] transition ${tone.ring} ${tone.text} ${selected ? 'scale-110 ring-4 ring-cyan-100' : onSelect ? 'group-hover:-translate-y-0.5 group-hover:shadow-md' : ''}`}>
<span className={`absolute right-0 top-0 h-2.5 w-2.5 rounded-full border-2 border-white ${tone.dot} ${node.status === 'running' ? 'animate-pulse' : ''}`} />
{MARK[node.status]}
</div>
<div className="mt-2 truncate text-xs font-semibold">{node.title.split(' · ')[1]}</div>
<div className={`mx-auto mt-3 rounded-lg px-2 py-2 transition ${selected ? tone.surface : 'bg-transparent'}`}>
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">H{index + 1}</p>
<p className={`mt-1 truncate text-xs font-semibold ${tone.text}`}>{node.title.split(' · ')[1] ?? node.title}</p>
<p className="mt-1 truncate text-[10px] capitalize text-slate-400">{node.status}</p>
</div>
{index < graph.nodes.length - 1 && <div className={`h-px w-3 shrink-0 ${node.status === 'pass' ? 'bg-emerald-300' : 'bg-slate-200'}`} />}
</Element>
);
})}
</div>
))}
</div>
</div>
);
@@ -1,56 +1,86 @@
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api, runReportExportUrl, type HarnessGateNode, type HarnessGateStatus, type HarnessTraceGraph } from '../../lib/api';
import { Card, StatusBadge } from '../ui/Card';
import { api, runReportExportUrl, type HarnessGateNode, type HarnessTraceGraph } from '../../lib/api';
import { ReportPanel } from '../report/ReportPrimitives';
import { StatusBadge } from '../ui/Card';
import { AssuranceRail } from './AssuranceRail';
const NODE_TONE: Record<HarnessGateStatus, string> = {
queued: 'border-slate-200 bg-slate-50 text-slate-500',
running: 'border-blue-300 bg-blue-50 text-blue-800 shadow-[0_0_0_4px_rgba(59,130,246,0.1)]',
pass: 'border-emerald-300 bg-emerald-50 text-emerald-800',
warning: 'border-amber-300 bg-amber-50 text-amber-800',
blocked: 'border-rose-400 bg-rose-50 text-rose-800 shadow-[0_0_0_4px_rgba(244,63,94,0.09)]',
error: 'border-rose-400 bg-rose-50 text-rose-800 shadow-[0_0_0_4px_rgba(244,63,94,0.09)]',
skipped: 'border-slate-200 bg-slate-100 text-slate-400',
};
function displayTime(value: string | null): string {
if (!value) return 'No timestamp';
return value.replace('T', ' ').replace('Z', ' UTC');
}
const STATUS_MARK: Record<HarnessGateStatus, string> = {
queued: '○',
running: '●',
pass: '✓',
warning: '!',
blocked: '×',
error: '×',
skipped: '–',
};
function EvidenceValue({ value }: { value: unknown }) {
if (value === null || value === undefined || value === '') return <span className="text-slate-500">Unavailable</span>;
return <>{typeof value === 'string' ? value : JSON.stringify(value)}</>;
}
function Evidence({ node }: { node: HarnessGateNode }) {
function GateDossier({ node }: { node: HarnessGateNode }) {
const entries = Object.entries(node.evidence);
return (
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.8fr)]">
<div className="grid gap-5 xl:grid-cols-[minmax(0,0.88fr)_minmax(24rem,1.12fr)]">
<div className="space-y-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-lg font-semibold text-slate-900">{node.title}</h3>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-700">Selected control</p>
<StatusBadge value={node.status} />
</div>
<p className="mt-1 text-sm text-slate-500">{node.description}</p>
<div className="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Latest decision</div>
<p className="mt-2 text-sm leading-6 text-slate-700">{node.reason}</p>
{node.updated_at && <p className="mt-2 text-xs text-slate-400">{node.updated_at.replace('T', ' ').replace('Z', '')}</p>}
<h3 className="mt-2 text-xl font-semibold tracking-[-0.02em] text-slate-950">{node.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-500">{node.description}</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Latest decision</p>
<span className="font-mono text-[10px] text-slate-400">{displayTime(node.updated_at)}</span>
</div>
<p className="mt-3 text-sm font-medium leading-6 text-slate-800">{node.reason || 'No decision reason was emitted.'}</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-4">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Event history</p>
<div className="mt-3 space-y-3">
{node.events.slice(-4).reverse().map((event, index) => (
<div key={`${event.timestamp}-${index}`} className="grid grid-cols-[0.65rem_minmax(0,1fr)] gap-3">
<div className="flex flex-col items-center">
<span className={`mt-1 h-2 w-2 rounded-full ${event.status === 'pass' ? 'bg-emerald-500' : event.status === 'warning' ? 'bg-amber-500' : event.status === 'error' || event.status === 'blocked' ? 'bg-rose-500' : 'bg-cyan-500'}`} />
{index < Math.min(node.events.length, 4) - 1 && <span className="mt-1 h-full w-px bg-slate-200" />}
</div>
<div className="pb-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-xs font-semibold capitalize text-slate-800">{event.status}</span>
<span className="font-mono text-[10px] text-slate-400">{displayTime(event.timestamp)}</span>
</div>
<p className="mt-1 text-xs leading-5 text-slate-500">{event.reason}</p>
</div>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-950 p-4 text-slate-100">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Safe evidence</div>
))}
{node.events.length === 0 && <p className="text-sm text-slate-500">No individual event was emitted for this control.</p>}
</div>
</div>
</div>
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 text-slate-100 shadow-[0_18px_50px_rgba(15,23,42,0.18)]">
<div className="flex items-center justify-between gap-3 border-b border-slate-800 px-5 py-4">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Safe evidence manifest</p>
<p className="mt-1 text-xs text-slate-500">Sanitized fields persisted by the harness</p>
</div>
<span className="rounded-full border border-slate-700 bg-slate-900 px-2.5 py-1 font-mono text-[10px] text-slate-400">{entries.length} fields</span>
</div>
{entries.length > 0 ? (
<dl className="mt-3 space-y-2 text-xs">
<dl className="divide-y divide-slate-800">
{entries.map(([key, value]) => (
<div key={key} className="grid grid-cols-[8rem_minmax(0,1fr)] gap-3 border-b border-slate-800 pb-2 last:border-0">
<dt className="font-medium text-slate-400">{key}</dt>
<dd className="break-all font-mono text-slate-200">{typeof value === 'string' ? value : JSON.stringify(value)}</dd>
<div key={key} className="grid gap-1 px-5 py-3.5 sm:grid-cols-[10rem_minmax(0,1fr)] sm:gap-4">
<dt className="font-mono text-[10px] font-semibold uppercase tracking-[0.08em] text-slate-500">{key}</dt>
<dd className="break-all font-mono text-xs leading-5 text-slate-200"><EvidenceValue value={value} /></dd>
</div>
))}
</dl>
) : <p className="mt-3 text-sm text-slate-500">No evidence has been emitted for this gate yet.</p>}
) : (
<div className="px-5 py-12 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-700 text-slate-500">∅</div>
<p className="mt-3 text-sm text-slate-400">No safe evidence fields are available yet.</p>
</div>
)}
</div>
</div>
);
@@ -79,61 +109,60 @@ export function TraceExplorer({ traceId, onClose }: { traceId: string; onClose?:
[graph, selectedId],
);
if (query.isError && !graph) {
return <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">The assurance trace could not be loaded.</div>;
}
return (
<Card
title="Harness trace explorer"
right={<div className="flex items-center gap-2">
<a href={`/reports/h6?run=${encodeURIComponent(traceId)}`} className="rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100">Open H6</a>
<a href={runReportExportUrl(traceId, 'html')} className="rounded-lg bg-slate-900 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-slate-700">Export evidence</a>
{onClose && <button type="button" onClick={onClose} className="rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-50">Close</button>}
</div>}
className="overflow-hidden"
<ReportPanel
eyebrow="Run assurance"
title="Governed execution dossier"
description="A control-by-control reconstruction of the run, backed by sanitized lifecycle evidence."
right={(
<div className="flex flex-wrap gap-2">
<a href={`/reports/h6?run=${encodeURIComponent(traceId)}`} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:bg-slate-50">Open H6</a>
<a href={runReportExportUrl(traceId, 'json')} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:bg-slate-50">JSON</a>
<a href={runReportExportUrl(traceId, 'html')} className="inline-flex min-h-11 items-center rounded-xl bg-slate-950 px-4 py-2 text-xs font-bold text-white transition hover:-translate-y-px hover:bg-slate-800">Export dossier</a>
{onClose && <button type="button" onClick={onClose} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 px-4 py-2 text-xs font-bold text-slate-600 transition hover:bg-slate-50">Close</button>}
</div>
)}
className="scroll-mt-24"
>
<div className="mb-5 flex flex-wrap items-end justify-between gap-3">
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 px-5 py-5 text-white sm:px-6">
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.13em] text-slate-400">Trace ID</p>
<p className="mt-1 break-all font-mono text-sm text-slate-700">{traceId}</p>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-slate-500">Trace identity</p>
<p className="mt-2 break-all font-mono text-sm text-slate-200">{traceId}</p>
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-xs text-slate-400">
<span><strong className="text-white">{graph?.progress ?? 0}/7</strong> controls observed</span>
<span><strong className="text-white">{graph?.events.length ?? 0}</strong> evidence events</span>
<span>Updated <strong className="text-white">{displayTime(graph?.updated_at ?? null)}</strong></span>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`h-2.5 w-2.5 rounded-full ${graph?.terminal ? 'bg-emerald-400' : 'animate-pulse bg-cyan-400'}`} />
<span className="text-xs font-bold uppercase tracking-[0.14em] text-slate-300">{graph?.terminal ? 'Evidence sealed' : 'Live evidence stream'}</span>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className={`h-2 w-2 rounded-full ${graph?.terminal ? 'bg-emerald-500' : 'animate-pulse bg-blue-500'}`} />
{graph?.terminal ? 'Completed' : 'Live · waiting for events'}
<span className="text-slate-300">·</span>
{graph?.progress ?? 0}/7 gates
</div>
</div>
{query.isLoading && !graph ? <div className="py-10 text-center text-sm text-slate-500">Loading trace…</div> : (
{query.isLoading && !graph ? (
<div className="py-16 text-center text-sm text-slate-500">Reconstructing assurance evidence…</div>
) : graph ? (
<>
<div className="overflow-x-auto pb-5">
<div className="flex min-w-max items-center px-1 py-3">
{graph?.nodes.map((node, index) => {
const next = graph.nodes[index + 1];
const edgeActive = node.status === 'pass' || node.status === 'running' || node.status === 'warning' || next?.status === 'running';
return (
<div key={node.id} className="flex items-center">
<button
type="button"
aria-pressed={selectedId === node.id}
onClick={() => setSelectedId(node.id)}
className={`w-40 rounded-xl border p-3 text-left transition duration-200 hover:-translate-y-0.5 hover:shadow-md ${NODE_TONE[node.status]} ${selectedId === node.id ? 'ring-2 ring-slate-900/10' : ''}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-bold tracking-wide">{node.title.split(' · ')[0]}</span>
<span className={`flex h-6 w-6 items-center justify-center rounded-full border border-current text-xs ${node.status === 'running' ? 'animate-pulse' : ''}`}>{STATUS_MARK[node.status]}</span>
<div className="mt-7">
<div className="mb-3 flex items-center justify-between gap-4">
<p className="text-[10px] font-black uppercase tracking-[0.17em] text-slate-500">Evidence spine · select a control</p>
<span className="text-xs text-slate-400">H1 context → H7 orchestration</span>
</div>
<div className="mt-3 text-sm font-semibold">{node.title.split(' · ')[1]}</div>
<div className="mt-1 truncate text-[11px] opacity-70">{node.reason}</div>
</button>
{next && <div className={`trace-edge mx-2 ${edgeActive ? 'trace-edge-active' : ''}`} aria-hidden="true" />}
<AssuranceRail graph={graph} selectedId={selected?.id} onSelect={setSelectedId} />
</div>
);
})}
<div className="mt-7 border-t border-slate-100 pt-7">
{selected && <GateDossier node={selected} />}
</div>
</div>
{selected && <Evidence node={selected} />}
{!graph?.found && <div className="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">This is a legacy or not-yet-started trace. New chat turns emit detailed H1–H7 events automatically.</div>}
{!graph.found && <div className="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">This is a legacy or not-yet-started trace. Detailed H1–H7 evidence becomes available as new lifecycle events arrive.</div>}
</>
)}
</Card>
) : null}
</ReportPanel>
);
}
@@ -1,17 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api, h6ReportExportUrl, type H6Breakdown, type H6ReportQuery, type HarnessReportVerdict } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
import {
CoverageBar,
ReportHero,
ReportPanel,
SignalMetric,
reportActionPrimary,
reportActionSecondary,
reportTone,
} from '../components/report/ReportPrimitives';
import { StatusBadge } from '../components/ui/Card';
import { api, h6ReportExportUrl, type H6Breakdown, type H6ReportQuery } from '../lib/api';
const money = (value: number): string => `$${value.toFixed(6)}`;
const integer = (value: number): string => new Intl.NumberFormat('en-US').format(value);
const verdictTone: Record<HarnessReportVerdict, string> = {
pass: 'border-emerald-300 bg-emerald-50 text-emerald-900',
attention: 'border-amber-300 bg-amber-50 text-amber-950',
fail: 'border-rose-300 bg-rose-50 text-rose-950',
no_data: 'border-slate-300 bg-slate-50 text-slate-800',
};
function queryFromSearch(search: URLSearchParams): H6ReportQuery {
return {
@@ -24,31 +27,62 @@ function queryFromSearch(search: URLSearchParams): H6ReportQuery {
}
function FilterField({ label, children }: { label: string; children: ReactNode }) {
return <label className="block"><span className="mb-1.5 block text-[10px] font-bold uppercase tracking-[0.13em] text-slate-500">{label}</span>{children}</label>;
}
function MetricCard({ label, value, sub, accent = false }: { label: string; value: string; sub?: string; accent?: boolean }) {
return <div className={`relative overflow-hidden rounded-2xl border p-4 ${accent ? 'border-indigo-200 bg-indigo-50/70' : 'border-slate-200 bg-white'}`}>
{accent && <span className="absolute -right-5 -top-5 h-16 w-16 rounded-full bg-indigo-200/50 blur-xl" />}
<div className="relative text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">{label}</div>
<div className={`relative mt-2 font-semibold tracking-tight text-slate-900 ${value.length > 9 ? 'text-base' : 'text-2xl'}`}>{value}</div>
{sub && <div className="relative mt-1 text-xs text-slate-500">{sub}</div>}
</div>;
return (
<label className="block">
<span className="mb-2 block text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">{label}</span>
{children}
</label>
);
}
function BreakdownTable({ rows, subject }: { rows: H6Breakdown[]; subject: string }) {
return <div className="overflow-x-auto"><table className="w-full min-w-[680px] text-sm">
<thead><tr className="border-b border-slate-200 text-left text-[10px] font-bold uppercase tracking-[0.1em] text-slate-400">
<th className="py-2.5 pr-3">{subject}</th><th>runs</th><th>failures</th><th>avg latency</th><th>tokens</th><th>cost</th>
</tr></thead>
<tbody>{rows.map((row) => <tr key={row.key} className="border-b border-slate-100 transition-colors hover:bg-slate-50/80">
<td className="max-w-[300px] py-3 pr-3 font-medium text-slate-800">{row.key}</td><td>{integer(row.runs)}</td><td>{integer(row.failures)}</td><td>{integer(row.latency_avg_ms)} ms</td><td>{row.tokens === null ? 'Unavailable' : integer(row.tokens)}</td><td>{row.cost_usd === null ? 'Unavailable' : money(row.cost_usd)}</td>
</tr>)}{rows.length === 0 && <tr><td colSpan={6} className="py-8 text-center text-slate-500">No matching {subject.toLowerCase()} records.</td></tr>}</tbody>
</table></div>;
return (
<div className="-mx-5 overflow-x-auto sm:-mx-6">
<table className="w-full min-w-[760px] text-sm">
<thead>
<tr className="border-y border-slate-100 bg-slate-50 text-left text-[10px] font-black uppercase tracking-[0.12em] text-slate-500">
<th className="px-5 py-3 sm:pl-6">{subject}</th>
<th className="px-3 py-3 text-right">Runs</th>
<th className="px-3 py-3 text-right">Failures</th>
<th className="px-3 py-3 text-right">Avg latency</th>
<th className="px-3 py-3 text-right">Tokens</th>
<th className="px-5 py-3 text-right sm:pr-6">Cost</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.key} className="border-b border-slate-100 transition-colors last:border-0 hover:bg-cyan-50/30">
<td className="max-w-[320px] px-5 py-4 font-semibold text-slate-900 sm:pl-6">{row.key}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{integer(row.runs)}</td>
<td className={`px-3 py-4 text-right font-mono text-xs ${row.failures > 0 ? 'font-semibold text-rose-700' : 'text-slate-600'}`}>{integer(row.failures)}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{integer(row.latency_avg_ms)} ms</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{row.tokens === null ? <span className="font-sans text-amber-700">Unavailable</span> : integer(row.tokens)}</td>
<td className="px-5 py-4 text-right font-mono text-xs text-slate-600 sm:pr-6">{row.cost_usd === null ? <span className="font-sans text-amber-700">Unavailable</span> : money(row.cost_usd)}</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td colSpan={6} className="px-6 py-14 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 text-slate-400">∅</div>
<p className="mt-3 text-sm text-slate-500">No matching {subject.toLowerCase()} records.</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function LoadingReport() {
return <div className="space-y-4" aria-label="Loading H6 report"><div className="h-44 animate-pulse rounded-3xl bg-slate-200" /><div className="grid grid-cols-2 gap-3 lg:grid-cols-4">{Array.from({ length: 8 }, (_, index) => <div key={index} className="h-28 animate-pulse rounded-2xl bg-slate-200" />)}</div></div>;
return (
<div className="space-y-5" aria-label="Loading H6 report">
<div className="h-72 animate-pulse rounded-[1.75rem] bg-slate-200" />
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
{Array.from({ length: 4 }, (_, index) => <div key={index} className="h-36 animate-pulse rounded-2xl bg-slate-200" />)}
</div>
</div>
);
}
export function H6ReportPage() {
@@ -60,7 +94,11 @@ export function H6ReportPage() {
to: searchParams.get('to') ?? '',
run: searchParams.get('run') ?? '',
}));
const report = useQuery({ queryKey: ['reports', 'h6', query], queryFn: () => api.h6Report(query), retry: false });
const report = useQuery({
queryKey: ['reports', 'h6', query],
queryFn: () => api.h6Report(query),
retry: false,
});
const applyFilters = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -77,80 +115,201 @@ export function H6ReportPage() {
setSearchParams({});
};
const inputClass = 'w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-800 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100';
const inputClass = 'min-h-11 w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 hover:border-slate-400 focus:border-cyan-500 focus:ring-4 focus:ring-cyan-100';
const data = report.data;
return <div className="space-y-5">
<section className="relative overflow-hidden rounded-3xl border border-slate-800 bg-[#111827] px-5 py-6 text-white shadow-[0_22px_55px_rgba(15,23,42,0.16)] sm:px-7">
<div className="absolute right-[-60px] top-[-90px] h-64 w-64 rounded-full bg-indigo-500/25 blur-3xl" />
<div className="absolute bottom-[-120px] left-1/3 h-56 w-56 rounded-full bg-teal-500/10 blur-3xl" />
<div className="relative flex flex-col justify-between gap-5 lg:flex-row lg:items-end">
<div><div className="text-[10px] font-bold uppercase tracking-[0.2em] text-indigo-300">H6 · evidence-backed operations</div><h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">AgentOps assurance dossier</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-slate-300">One report surface for runtime health, provider usage, cost provenance, failures, retries, alerts and source freshness.</p></div>
{report.data && <div className="flex flex-wrap items-center gap-2"><span className={`rounded-full border px-3 py-2 text-xs font-black uppercase tracking-[0.12em] ${verdictTone[report.data.verdict]}`}>{report.data.verdict}</span><span className="rounded-full border border-slate-700 bg-slate-900/70 px-3 py-2 text-xs text-slate-300">{report.data.freshness.status} · {report.data.report_id}</span></div>}
return (
<div className="space-y-5">
<ReportHero
eyebrow="H6 · AgentOps assurance"
title="Operational evidence, ready for a decision."
description="A production dossier for runtime health, reliability, cost provenance and telemetry integrity—designed to answer what happened, why it matters and where the evidence lives."
verdict={data?.verdict}
meta={data ? (
<>
<span>{data.report_id}</span>
<span>Generated {data.generated_at.replace('T', ' ').replace('Z', ' UTC')}</span>
<span>Freshness {data.freshness.status}</span>
</>
) : <span>Generating from canonical evidence…</span>}
actions={data ? (
<>
<a href={h6ReportExportUrl(query, 'json')} className={reportActionSecondary}>JSON evidence</a>
<a href={h6ReportExportUrl(query, 'html')} className={reportActionPrimary}>Export dossier</a>
</>
) : undefined}
/>
<ReportPanel
eyebrow="Scope"
title="Evidence boundary"
description="Filters are applied server-side to the screen, JSON contract and print-ready dossier."
right={Object.values(draft).some(Boolean) ? <span className="rounded-full bg-cyan-50 px-3 py-1 text-[10px] font-black uppercase tracking-[0.12em] text-cyan-700">Filtered view</span> : <span className="text-xs text-slate-400">All available evidence</span>}
>
<form onSubmit={applyFilters} className="grid gap-4 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1fr_1.45fr_auto] xl:items-end">
<FilterField label="Project">
<select value={draft.project} onChange={(event) => setDraft((current) => ({ ...current, project: event.target.value }))} className={inputClass}>
<option value="">All projects</option>
{data?.available_filters.projects.map((project) => <option key={project} value={project}>{project}</option>)}
</select>
</FilterField>
<FilterField label="From">
<input type="date" value={draft.from} onChange={(event) => setDraft((current) => ({ ...current, from: event.target.value }))} className={inputClass} />
</FilterField>
<FilterField label="To">
<input type="date" value={draft.to} onChange={(event) => setDraft((current) => ({ ...current, to: event.target.value }))} className={inputClass} />
</FilterField>
<FilterField label="Run / trace">
<input list="h6-run-options" value={draft.run} onChange={(event) => setDraft((current) => ({ ...current, run: event.target.value }))} placeholder="All governed runs" className={inputClass} />
<datalist id="h6-run-options">{data?.available_filters.runs.map((run) => <option key={run} value={run} />)}</datalist>
</FilterField>
<div className="flex gap-2">
<button type="submit" className="min-h-11 rounded-xl bg-slate-950 px-5 py-2.5 text-sm font-bold text-white transition hover:-translate-y-px hover:bg-slate-800 focus:outline-none focus:ring-4 focus:ring-cyan-100">Apply</button>
<button type="button" onClick={clearFilters} className="min-h-11 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-bold text-slate-600 transition hover:bg-slate-50">Clear</button>
</div>
</section>
<Card title="Report scope" right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Filters apply to API and both exports</span>}>
<form onSubmit={applyFilters} className="grid gap-3 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1fr_1.4fr_auto] xl:items-end">
<FilterField label="Project"><select value={draft.project} onChange={(event) => setDraft((current) => ({ ...current, project: event.target.value }))} className={inputClass}><option value="">All projects</option>{report.data?.available_filters.projects.map((project) => <option key={project} value={project}>{project}</option>)}</select></FilterField>
<FilterField label="From"><input type="date" value={draft.from} onChange={(event) => setDraft((current) => ({ ...current, from: event.target.value }))} className={inputClass} /></FilterField>
<FilterField label="To"><input type="date" value={draft.to} onChange={(event) => setDraft((current) => ({ ...current, to: event.target.value }))} className={inputClass} /></FilterField>
<FilterField label="Run / trace"><input list="h6-run-options" value={draft.run} onChange={(event) => setDraft((current) => ({ ...current, run: event.target.value }))} placeholder="All runs" className={inputClass} /><datalist id="h6-run-options">{report.data?.available_filters.runs.map((run) => <option key={run} value={run} />)}</datalist></FilterField>
<div className="flex gap-2"><button type="submit" className="rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">Apply</button><button type="button" onClick={clearFilters} className="rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50">Clear</button></div>
</form>
</Card>
</ReportPanel>
{report.isLoading && <LoadingReport />}
{report.isError && <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800"><div className="font-semibold">The H6 report could not be generated.</div><p className="mt-1 text-rose-700">Check the selected time range and confirm the Control Panel API can read the canonical telemetry sources.</p></div>}
{report.isError && (
<div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">
<div className="font-semibold">The H6 dossier could not be generated.</div>
<p className="mt-1 text-rose-700">Check the selected range and confirm that the Control Plane can read the canonical telemetry sources.</p>
</div>
)}
{report.data && <>
<section className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-8">
<MetricCard label="Runs" value={integer(report.data.summary.runs)} sub={`${report.data.summary.success} successful`} accent />
<MetricCard label="Failures" value={integer(report.data.summary.failed)} sub={`${report.data.summary.failure_rate_pct}% rate`} />
<MetricCard label="Degraded" value={integer(report.data.summary.degraded)} />
<MetricCard label="P95 latency" value={`${integer(report.data.summary.latency_ms.p95)}ms`} sub={`P50 ${integer(report.data.summary.latency_ms.p50)}ms`} />
<MetricCard
label="Provider tokens"
value={report.data.summary.coverage.token_records > 0 ? integer(report.data.summary.tokens.provider_total ?? report.data.summary.tokens.total ?? 0) : 'Unavailable'}
sub={`${report.data.summary.coverage.token_pct}% run coverage`}
accent
/>
<MetricCard
label="Actual cost"
value={report.data.summary.cost_usd.provider_actual !== null ? money(report.data.summary.cost_usd.provider_actual) : 'Unavailable'}
sub={`${report.data.summary.provider_calls} provider calls`}
/>
<MetricCard
label="Cost coverage"
value={`${report.data.summary.coverage.cost_pct}%`}
sub={`${report.data.summary.coverage.cost_records}/${report.data.summary.coverage.runtime_records} runs`}
/>
<MetricCard label="Alerts" value={integer(report.data.summary.alerts)} sub={`${report.data.summary.retries} retries`} />
{data && (
<>
<section aria-label="Executive signals" className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<SignalMetric label="Governed runs" value={integer(data.summary.runs)} detail={`${data.summary.success} successful · ${data.summary.degraded} degraded`} tone="info" primary />
<SignalMetric label="Reliability" value={`${data.summary.failure_rate_pct}%`} detail={`${data.summary.failed} failed · ${data.summary.alerts} alerts`} tone={data.summary.failed > 0 ? 'danger' : 'success'} primary />
<SignalMetric label="P95 latency" value={`${integer(data.summary.latency_ms.p95)} ms`} detail={`P50 ${integer(data.summary.latency_ms.p50)} ms`} tone={data.findings.some((finding) => finding.code.includes('LATENCY')) ? 'warning' : 'neutral'} primary />
<SignalMetric label="Telemetry integrity" value={data.data_quality.status.replaceAll('_', ' ')} detail={`${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost coverage`} tone={reportTone(data.data_quality.status)} primary />
</section>
<div className="grid gap-5 xl:grid-cols-[1.15fr_0.85fr]">
<Card title="Verdict findings" right={<StatusBadge value={report.data.verdict} />}>
<div className="space-y-2.5">{report.data.findings.map((finding) => <div key={finding.code} className={`rounded-xl border-l-4 p-3.5 ${finding.severity === 'critical' ? 'border-rose-500 bg-rose-50' : finding.severity === 'warning' ? 'border-amber-500 bg-amber-50' : 'border-indigo-500 bg-indigo-50'}`}><div className="flex flex-wrap items-center justify-between gap-2"><span className="font-mono text-xs font-bold text-slate-800">{finding.code}</span>{finding.metric && <span className="text-[11px] text-slate-500">{finding.metric}: {String(finding.value)}{finding.threshold !== undefined ? ` · threshold ${String(finding.threshold)}` : ''}</span>}</div><p className="mt-1 text-sm leading-5 text-slate-600">{finding.message}</p></div>)}{report.data.findings.length === 0 && <div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">No threshold breach was detected in this scope.</div>}</div>
</Card>
<Card title="Evidence freshness" right={<StatusBadge value={report.data.freshness.status} />}>
<div className="space-y-3">{report.data.evidence_sources.map((source) => <div key={source.source} className="rounded-xl border border-slate-200 bg-slate-50/70 p-3"><div className="flex items-center justify-between gap-3"><span className="text-sm font-semibold capitalize text-slate-800">{source.source}</span><StatusBadge value={!source.present ? 'missing' : source.stale ? 'stale' : 'fresh'} /></div><div className="mt-2 flex justify-between text-xs text-slate-500"><span>{source.records} records</span><span>{source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</span></div><div className="mt-2 break-all font-mono text-[10px] leading-4 text-slate-400">{source.path}</div></div>)}</div>
</Card>
<section className="grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-[0_12px_32px_rgba(15,23,42,0.04)] sm:grid-cols-2 lg:grid-cols-4">
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider tokens</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.coverage.token_records > 0 ? integer(data.summary.tokens.provider_total ?? data.summary.tokens.total ?? 0) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 lg:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Actual cost</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.cost_usd.provider_actual !== null ? money(data.summary.cost_usd.provider_actual) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider calls</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.provider_calls)}</p>
</div>
<div className="p-2">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Retries</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.retries)}</p>
</div>
</section>
<div className="grid gap-5 xl:grid-cols-[1.18fr_0.82fr]">
<ReportPanel
eyebrow="Decision brief"
title="What requires attention"
description="Threshold findings are ordered as review signals, not maturity claims."
right={<StatusBadge value={data.verdict} />}
>
<div className="space-y-3">
{data.findings.map((finding, index) => {
const tone = finding.severity === 'critical' ? 'border-rose-500 bg-rose-50/60' : finding.severity === 'warning' ? 'border-amber-500 bg-amber-50/60' : 'border-cyan-500 bg-cyan-50/60';
return (
<article key={finding.code} className={`grid gap-3 rounded-xl border-l-4 p-4 sm:grid-cols-[2rem_minmax(0,1fr)] ${tone}`}>
<div className="font-mono text-lg font-semibold text-slate-400">{String(index + 1).padStart(2, '0')}</div>
<div>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-mono text-xs font-bold text-slate-900">{finding.code}</span>
{finding.metric && <span className="font-mono text-[10px] text-slate-500">{finding.metric}: {String(finding.value)}{finding.threshold !== undefined ? ` / ${String(finding.threshold)}` : ''}</span>}
</div>
<p className="mt-2 text-sm leading-6 text-slate-700">{finding.message}</p>
</div>
</article>
);
})}
{data.findings.length === 0 && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-5 text-sm text-emerald-800">No threshold breach was detected in the selected evidence boundary.</div>
)}
</div>
</ReportPanel>
<ReportPanel
eyebrow="Source integrity"
title="Evidence provenance"
description="Freshness is evaluated independently for every canonical source."
right={<StatusBadge value={data.freshness.status} />}
>
<div className="space-y-3">
{data.evidence_sources.map((source) => {
const state = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return (
<article key={source.source} className="rounded-xl border border-slate-200 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-sm font-semibold capitalize text-slate-900">{source.source}</h3>
<p className="mt-1 font-mono text-[10px] text-slate-400">{source.records} records · {source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</p>
</div>
<StatusBadge value={state} />
</div>
<p className="mt-3 break-all border-t border-slate-100 pt-3 font-mono text-[10px] leading-4 text-slate-400">{source.path}</p>
</article>
);
})}
</div>
</ReportPanel>
</div>
<div className="grid gap-5 xl:grid-cols-2"><Card title="Runtime by step"><BreakdownTable rows={report.data.details.by_step} subject="Step" /></Card><Card title="Provider usage"><BreakdownTable rows={report.data.details.by_provider} subject="Provider · model" /></Card></div>
<div className="grid gap-5 xl:grid-cols-2">
<ReportPanel eyebrow="Runtime" title="Execution profile by step" description="Latency, failure and attribution by governed lifecycle step.">
<BreakdownTable rows={data.details.by_step} subject="Step" />
</ReportPanel>
<ReportPanel eyebrow="Provider" title="Model usage and provenance" description="Only provider-reported token and actual cost values are shown.">
<BreakdownTable rows={data.details.by_provider} subject="Provider · model" />
</ReportPanel>
</div>
<div className="grid gap-5 xl:grid-cols-[0.8fr_1.2fr]">
<Card title={`Data quality · ${report.data.data_quality.status}`}>
<div className="mb-4 grid grid-cols-2 gap-3">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Token coverage</div><div className="mt-1 text-xl font-semibold text-slate-900">{report.data.summary.coverage.token_pct}%</div></div>
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Cost coverage</div><div className="mt-1 text-xl font-semibold text-slate-900">{report.data.summary.coverage.cost_pct}%</div></div>
<ReportPanel
eyebrow="Evidence quality"
title="Coverage and disclosure"
description="Unavailable telemetry remains null. Coverage gaps are surfaced as review findings."
right={<StatusBadge value={data.data_quality.status} />}
>
<div className="grid gap-7 xl:grid-cols-[0.75fr_1.25fr]">
<div className="space-y-6">
<CoverageBar label="Token attribution" value={data.summary.coverage.token_pct} detail={`${data.summary.coverage.token_records}/${data.summary.coverage.runtime_records} runtime records`} />
<CoverageBar label="Cost attribution" value={data.summary.coverage.cost_pct} detail={`${data.summary.coverage.cost_records}/${data.summary.coverage.runtime_records} runtime records`} />
</div>
{report.data.data_quality.warnings.length > 0 ? <ul className="space-y-2 text-sm leading-5 text-slate-600">{report.data.data_quality.warnings.map((warning) => <li key={warning} className="flex gap-2"><span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-amber-400" />{warning}</li>)}</ul> : <div className="text-sm text-emerald-700">All required sources are present and no estimation warning was detected.</div>}
</Card>
<Card title="Independent export" right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Generated on demand · no hard-coded score</span>}>
<p className="text-sm leading-6 text-slate-600">Both exports use the same contract, filters, verdict rules and canonical sources as this screen. JSON is machine-auditable; HTML is print-ready for team review.</p>
<div className="mt-4 flex flex-wrap gap-3"><a href={h6ReportExportUrl(query, 'json')} className="rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-700">Download JSON</a><a href={h6ReportExportUrl(query, 'html')} className="rounded-xl border border-indigo-300 bg-indigo-50 px-4 py-2.5 text-sm font-semibold text-indigo-700 transition hover:bg-indigo-100">Download HTML</a></div>
</Card>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-5">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Disclosure notes</p>
{data.data_quality.warnings.length > 0 ? (
<ul className="mt-4 space-y-3">
{data.data_quality.warnings.map((warning) => (
<li key={warning} className="grid grid-cols-[0.55rem_minmax(0,1fr)] gap-3 text-sm leading-6 text-slate-700">
<span className="mt-2 h-2 w-2 rounded-full bg-amber-500" />
<span>{warning}</span>
</li>
))}
</ul>
) : <p className="mt-4 text-sm text-emerald-700">All required sources are present and no estimation warning was detected.</p>}
</div>
</>}
</div>;
</div>
</ReportPanel>
<div className="rounded-2xl border border-slate-800 bg-slate-950 p-5 text-white shadow-[0_18px_48px_rgba(15,23,42,0.16)] sm:flex sm:items-center sm:justify-between sm:gap-6 sm:p-6">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Independent evidence package</p>
<h2 className="mt-2 text-lg font-semibold">Same contract. Two review formats.</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">JSON is machine-auditable. HTML is a self-contained, print-ready assurance dossier generated only when requested.</p>
</div>
<div className="mt-5 flex shrink-0 flex-wrap gap-2 sm:mt-0">
<a href={h6ReportExportUrl(query, 'json')} className={reportActionSecondary}>Download JSON</a>
<a href={h6ReportExportUrl(query, 'html')} className={reportActionPrimary}>Download HTML</a>
</div>
</div>
</>
)}
</div>
);
}
@@ -1,53 +1,114 @@
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import { api, type HarnessRunRecord } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
import { Link, useSearchParams } from 'react-router-dom';
import { ReportHero, ReportPanel, SignalMetric, reportActionPrimary } from '../components/report/ReportPrimitives';
import { TraceExplorer } from '../components/trace/TraceExplorer';
import { StatusBadge } from '../components/ui/Card';
import { api, type HarnessRunRecord } from '../lib/api';
const integer = (value: number): string => new Intl.NumberFormat('en-US').format(value);
export function Runs() {
const { data, isLoading } = useQuery({ queryKey: ['runs'], queryFn: () => api.runs(100) });
const { data, isLoading, isError } = useQuery({ queryKey: ['runs'], queryFn: () => api.runs(100) });
const [searchParams, setSearchParams] = useSearchParams();
const selectedTrace = searchParams.get('trace') ?? '';
const runs = data?.runs ?? [];
const failures = runs.filter((run) => run.status === 'failed').length;
const attributedCost = runs.filter((run) => run.cost_estimate != null).length;
const latencyValues = runs.map((run) => Number(run.latency_ms)).filter((value) => Number.isFinite(value) && value > 0);
const averageLatency = latencyValues.length > 0
? Math.round(latencyValues.reduce((sum, value) => sum + value, 0) / latencyValues.length)
: null;
if (isLoading || !data) return <div className="text-slate-500">Loading…</div>;
return (
<div className="space-y-5">
<ReportHero
eyebrow="Run assurance registry"
title="Every governed run, one evidence trail."
description="Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent assurance dossier."
verdict={failures > 0 ? 'attention' : data ? 'operational' : undefined}
meta={data ? (
<>
<span>{data.count} records in current registry</span>
<span>{failures} failures</span>
<span>{attributedCost}/{runs.length} cost-attributed</span>
</>
) : <span>Loading governed run registry…</span>}
actions={<Link to="/reports/h6" className={reportActionPrimary}>Open H6 dossier</Link>}
/>
{selectedTrace && <TraceExplorer traceId={selectedTrace} onClose={() => setSearchParams({})} />}
<Card
title={`Recent runs (${data.count})`}
right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Select a run to inspect H1–H7</span>}
{isLoading && (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{Array.from({ length: 4 }, (_, index) => <div key={index} className="h-32 animate-pulse rounded-2xl bg-slate-200" />)}
</div>
)}
{isError && <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">The governed run registry could not be loaded.</div>}
{data && (
<>
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<SignalMetric label="Governed records" value={integer(data.count)} detail="Latest 100 lifecycle records" tone="info" />
<SignalMetric label="Failures" value={integer(failures)} detail={`${data.count ? Math.round((failures / data.count) * 100) : 0}% observed failure rate`} tone={failures > 0 ? 'danger' : 'success'} />
<SignalMetric label="Average latency" value={averageLatency === null ? 'Unavailable' : `${integer(averageLatency)} ms`} detail={`${latencyValues.length}/${runs.length} records measured`} tone="neutral" />
<SignalMetric label="Cost attribution" value={`${runs.length ? Math.round((attributedCost / runs.length) * 100) : 0}%`} detail={`${attributedCost}/${runs.length} records`} tone={attributedCost === runs.length && runs.length > 0 ? 'success' : 'warning'} />
</section>
<ReportPanel
eyebrow="Evidence index"
title="Recent governed runs"
description="Operational values are shown only when present in canonical telemetry. Select a trace to open the evidence spine."
right={<span className="text-xs text-slate-400">{data.count} total records</span>}
>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="border-b border-slate-200 text-left text-slate-500">
<th className="py-2">time</th><th>step</th><th>status</th><th>latency</th><th>tokens</th><th>cost</th><th className="text-right">trace</th>
</tr></thead>
<div className="-mx-5 overflow-x-auto sm:-mx-6">
<table className="w-full min-w-[900px] text-sm">
<thead>
<tr className="border-y border-slate-100 bg-slate-50 text-left text-[10px] font-black uppercase tracking-[0.12em] text-slate-500">
<th className="px-5 py-3 sm:pl-6">Observed at</th>
<th className="px-3 py-3">Lifecycle step</th>
<th className="px-3 py-3">Verdict</th>
<th className="px-3 py-3 text-right">Latency</th>
<th className="px-3 py-3 text-right">Tokens</th>
<th className="px-3 py-3 text-right">Cost</th>
<th className="px-5 py-3 text-right sm:pr-6">Evidence</th>
</tr>
</thead>
<tbody>
{data.runs.map((run: HarnessRunRecord, index: number) => (
<tr key={`${run.trace_id ?? 'run'}-${index}`} className="border-b border-slate-100 transition-colors hover:bg-slate-50/80">
<td className="py-2 text-slate-500">{run.timestamp?.replace('T', ' ').replace('Z', '')}</td>
<td className="text-slate-700">{run.step ?? run.harness}</td>
<td><StatusBadge value={run.status ?? '—'} /></td>
<td>{run.latency_ms ?? '—'} ms</td>
<td>{run.total_tokens ?? '—'}</td>
<td>{run.cost_estimate == null ? <span className="font-medium text-amber-600">Unavailable</span> : `$${Number(run.cost_estimate).toFixed(5)}`}</td>
<td className="py-2 text-right">
{runs.map((run: HarnessRunRecord, index: number) => (
<tr key={`${run.trace_id ?? 'run'}-${index}`} className="border-b border-slate-100 transition-colors last:border-0 hover:bg-cyan-50/30">
<td className="whitespace-nowrap px-5 py-4 font-mono text-[11px] text-slate-500 sm:pl-6">{run.timestamp?.replace('T', ' ').replace('Z', ' UTC') ?? 'Unavailable'}</td>
<td className="px-3 py-4 font-semibold text-slate-900">{run.step ?? run.harness}</td>
<td className="px-3 py-4"><StatusBadge value={run.status ?? 'unknown'} /></td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.latency_ms == null ? 'Unavailable' : `${integer(Number(run.latency_ms))} ms`}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.total_tokens ?? 'Unavailable'}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.cost_estimate == null ? <span className="font-sans text-amber-700">Unavailable</span> : `$${Number(run.cost_estimate).toFixed(5)}`}</td>
<td className="px-5 py-4 text-right sm:pr-6">
{run.trace_id ? (
<button
type="button"
onClick={() => setSearchParams({ trace: run.trace_id ?? '' })}
className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-semibold text-blue-700 transition-colors hover:bg-blue-100"
onClick={() => {
setSearchParams({ trace: run.trace_id ?? '' });
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className="min-h-11 rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:border-cyan-400 hover:text-cyan-800 focus:outline-none focus:ring-4 focus:ring-cyan-100"
>
Open H1–H7
Inspect H1–H7
</button>
) : <span className="text-slate-300">—</span>}
</td>
</tr>
))}
{runs.length === 0 && (
<tr>
<td colSpan={7} className="px-6 py-16 text-center text-sm text-slate-500">No governed run has been recorded yet.</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
</ReportPanel>
</>
)}
</div>
);
}