feat: simplify assurance reports with progressive disclosure
This commit is contained in:
@@ -172,5 +172,80 @@ export function CoverageBar({
|
||||
);
|
||||
}
|
||||
|
||||
export function DistributionBars({
|
||||
label,
|
||||
rows,
|
||||
}: {
|
||||
label: string;
|
||||
rows: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
detail?: ReactNode;
|
||||
tone?: Tone;
|
||||
}>;
|
||||
}) {
|
||||
const maximum = Math.max(...rows.map((row) => row.value), 1);
|
||||
return (
|
||||
<div role="img" aria-label={label}>
|
||||
<ol className="space-y-4">
|
||||
{rows.map((row) => {
|
||||
const bounded = Math.max(row.value, 0);
|
||||
const width = bounded === 0 ? 0 : Math.max((bounded / maximum) * 100, 3);
|
||||
const tone = row.tone ?? 'info';
|
||||
return (
|
||||
<li key={row.label}>
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs font-semibold text-slate-800">{row.label}</p>
|
||||
{row.detail && <div className="mt-1 text-[11px] text-slate-500">{row.detail}</div>}
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xs font-semibold text-slate-900">{integerLabel(bounded)}</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-slate-100" aria-hidden="true">
|
||||
<div className={`h-full rounded-full ${TONE[tone].line}`} style={{ width: `${width}%` }} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
{rows.length === 0 && <p className="text-sm text-slate-500">No measured distribution is available.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function integerLabel(value: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(value);
|
||||
}
|
||||
|
||||
export function DisclosurePanel({
|
||||
summary,
|
||||
description,
|
||||
badge,
|
||||
children,
|
||||
open = false,
|
||||
}: {
|
||||
summary: string;
|
||||
description: string;
|
||||
badge?: ReactNode;
|
||||
children: ReactNode;
|
||||
open?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<details open={open} className="group overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-[0_10px_28px_rgba(15,23,42,0.04)]">
|
||||
<summary className="flex min-h-16 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 outline-none transition hover:bg-slate-50 focus-visible:ring-4 focus-visible:ring-inset focus-visible:ring-cyan-100 [&::-webkit-details-marker]:hidden sm:px-6">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900">{summary}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
{badge}
|
||||
<span className="select-none text-lg text-slate-400 transition-transform group-open:rotate-45" aria-hidden="true">+</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div className="border-t border-slate-100 p-5 sm:p-6">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
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,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api, runReportExportUrl, type HarnessGateNode, type HarnessTraceGraph } from '../../lib/api';
|
||||
import { ReportPanel } from '../report/ReportPrimitives';
|
||||
import { DisclosurePanel, ReportPanel } from '../report/ReportPrimitives';
|
||||
import { StatusBadge } from '../ui/Card';
|
||||
import { AssuranceRail } from './AssuranceRail';
|
||||
|
||||
@@ -35,8 +35,10 @@ function GateDossier({ node }: { node: HarnessGateNode }) {
|
||||
</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>
|
||||
<DisclosurePanel
|
||||
summary="Event history"
|
||||
description={`${node.events.length} sanitized lifecycle events · hidden by default`}
|
||||
>
|
||||
<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">
|
||||
@@ -55,33 +57,30 @@ function GateDossier({ node }: { node: HarnessGateNode }) {
|
||||
))}
|
||||
{node.events.length === 0 && <p className="text-sm text-slate-500">No individual event was emitted for this control.</p>}
|
||||
</div>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</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>
|
||||
<DisclosurePanel
|
||||
summary="Safe evidence manifest"
|
||||
description="Sanitized fields persisted by the harness · hidden by default"
|
||||
badge={<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-mono text-[10px] text-slate-500">{entries.length} fields</span>}
|
||||
>
|
||||
{entries.length > 0 ? (
|
||||
<dl className="divide-y divide-slate-800">
|
||||
<dl className="-m-5 divide-y divide-slate-100 sm:-m-6">
|
||||
{entries.map(([key, value]) => (
|
||||
<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>
|
||||
<dd className="break-all font-mono text-xs leading-5 text-slate-700"><EvidenceValue value={value} /></dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<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 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 safe evidence fields are available yet.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
CoverageBar,
|
||||
DisclosurePanel,
|
||||
DistributionBars,
|
||||
ReportHero,
|
||||
ReportPanel,
|
||||
SignalMetric,
|
||||
@@ -140,11 +142,10 @@ export function H6ReportPage() {
|
||||
) : 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>}
|
||||
<DisclosurePanel
|
||||
summary="Filter the evidence boundary"
|
||||
description="Project, date and trace filters stay out of the executive view until they are needed."
|
||||
badge={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</span> : <span className="text-xs text-slate-400">All 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">
|
||||
@@ -168,7 +169,7 @@ export function H6ReportPage() {
|
||||
<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>
|
||||
</form>
|
||||
</ReportPanel>
|
||||
</DisclosurePanel>
|
||||
|
||||
{report.isLoading && <LoadingReport />}
|
||||
{report.isError && (
|
||||
@@ -187,24 +188,17 @@ export function H6ReportPage() {
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<DisclosurePanel
|
||||
summary="Provider telemetry"
|
||||
description="Token, cost, call and retry counters are technical evidence, hidden by default."
|
||||
>
|
||||
<section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<SignalMetric label="Provider tokens" value={data.summary.coverage.token_records > 0 ? integer(data.summary.tokens.provider_total ?? data.summary.tokens.total ?? 0) : 'Unavailable'} detail={`${data.summary.coverage.token_pct}% record coverage`} tone={data.summary.coverage.token_records > 0 ? 'info' : 'warning'} />
|
||||
<SignalMetric label="Actual cost" value={data.summary.cost_usd.provider_actual !== null ? money(data.summary.cost_usd.provider_actual) : 'Unavailable'} detail={`${data.summary.coverage.cost_pct}% record coverage`} tone={data.summary.cost_usd.provider_actual !== null ? 'info' : 'warning'} />
|
||||
<SignalMetric label="Provider calls" value={integer(data.summary.provider_calls)} detail="Provider-attributed requests" />
|
||||
<SignalMetric label="Retries" value={integer(data.summary.retries)} detail="Observed reroute attempts" tone={data.summary.retries > 0 ? 'warning' : 'neutral'} />
|
||||
</section>
|
||||
</DisclosurePanel>
|
||||
|
||||
<div className="grid gap-5 xl:grid-cols-[1.18fr_0.82fr]">
|
||||
<ReportPanel
|
||||
@@ -236,37 +230,20 @@ export function H6ReportPage() {
|
||||
</ReportPanel>
|
||||
|
||||
<ReportPanel
|
||||
eyebrow="Source integrity"
|
||||
title="Evidence provenance"
|
||||
description="Freshness is evaluated independently for every canonical source."
|
||||
right={<StatusBadge value={data.freshness.status} />}
|
||||
eyebrow="Outcome mix"
|
||||
title="Run distribution"
|
||||
description="Counts are grouped from canonical runtime status values; the longest bar is the largest observed group."
|
||||
right={<span className="font-mono text-xs text-slate-400">{integer(data.summary.runs)} total</span>}
|
||||
>
|
||||
<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">
|
||||
<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" />
|
||||
<DistributionBars
|
||||
label="Governed run outcome distribution"
|
||||
rows={data.details.by_status.map((row) => ({
|
||||
label: row.status.replaceAll('_', ' '),
|
||||
value: row.count,
|
||||
detail: `${data.summary.runs ? Math.round((row.count / data.summary.runs) * 100) : 0}% of selected runs`,
|
||||
tone: row.status === 'failed' ? 'danger' as const : row.status === 'degraded' ? 'warning' as const : 'success' as const,
|
||||
}))}
|
||||
/>
|
||||
</ReportPanel>
|
||||
</div>
|
||||
|
||||
@@ -297,17 +274,47 @@ export function H6ReportPage() {
|
||||
</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>
|
||||
<DisclosurePanel
|
||||
summary="Technical evidence and breakdowns"
|
||||
description="Canonical paths, step tables and provider attribution are preserved for audit without crowding the decision view."
|
||||
badge={<StatusBadge value={data.freshness.status} />}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<div className="mb-4">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.16em] text-cyan-700">Source integrity</p>
|
||||
<h3 className="mt-1 text-base font-semibold text-slate-900">Evidence provenance</h3>
|
||||
</div>
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
{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>
|
||||
<h4 className="text-sm font-semibold capitalize text-slate-900">{source.source}</h4>
|
||||
<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>
|
||||
</section>
|
||||
<div className="grid gap-5 xl:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-slate-900">Execution profile by step</h3>
|
||||
<BreakdownTable rows={data.details.by_step} subject="Step" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-slate-900">Model usage and provenance</h3>
|
||||
<BreakdownTable rows={data.details.by_provider} subject="Provider · model" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</DisclosurePanel>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user