feat: add production assurance dashboard flow
This commit is contained in:
+4
-4
@@ -100,14 +100,14 @@ function H6Summary({ node, goal, runReport, projectReport, loading, error }: {
|
||||
return <div className="mt-4 rounded-xl border border-amber-300/30 bg-amber-300/10 p-3 text-xs leading-5 text-amber-100">Run telemetry could not be loaded here. <Link to={reportLink} className="font-semibold underline decoration-amber-300/50 underline-offset-4 hover:text-white focus:outline-none focus:ring-2 focus:ring-cyan-300">Open the full H6 report</Link> to inspect the source status.</div>;
|
||||
}
|
||||
|
||||
const tokenTotal = runReport.summary.tokens.provider_total || runReport.summary.tokens.total;
|
||||
const tokenTotal = runReport.summary.tokens.provider_total ?? runReport.summary.tokens.total;
|
||||
const actualCost = runReport.summary.cost_usd.provider_actual;
|
||||
const displayedCost = actualCost || runReport.summary.cost_usd.estimated;
|
||||
const displayedCost = actualCost ?? runReport.summary.cost_usd.estimated;
|
||||
const provider = runReport.details.by_provider.slice(0, 2).map((row) => row.key).join(', ') || 'Not attributed';
|
||||
const metrics = [
|
||||
['Max runtime', formatDuration(runReport.summary.latency_ms.max)],
|
||||
['Tokens', tokenTotal.toLocaleString()],
|
||||
[actualCost ? 'Actual cost' : 'Estimated cost', formatCost(displayedCost)],
|
||||
['Tokens', tokenTotal === null ? 'Unavailable' : tokenTotal.toLocaleString()],
|
||||
[actualCost !== null ? 'Actual cost' : displayedCost !== null ? 'Estimated cost' : 'Cost', displayedCost === null ? 'Unavailable' : formatCost(displayedCost)],
|
||||
['Retries', runReport.summary.retries.toLocaleString()],
|
||||
['Provider calls', runReport.summary.provider_calls.toLocaleString()],
|
||||
['Freshness', runReport.freshness.status],
|
||||
|
||||
@@ -21,6 +21,7 @@ const PAGE_COPY: Record<string, { title: string; eyebrow: string }> = {
|
||||
export function Header() {
|
||||
const { data } = useQuery({ queryKey: ['health'], queryFn: health });
|
||||
const { data: session } = useQuery({ queryKey: ['session'], queryFn: api.session, staleTime: 60_000 });
|
||||
const { data: project } = useQuery({ queryKey: ['project-profile'], queryFn: api.project, staleTime: 60_000 });
|
||||
const { pathname } = useLocation();
|
||||
const stale = data ? !data.ok : true;
|
||||
const page = PAGE_COPY[pathname] ?? PAGE_COPY['/'];
|
||||
@@ -32,6 +33,10 @@ export function Header() {
|
||||
<h1 className="mt-0.5 text-lg font-semibold tracking-tight text-slate-900">{page.title}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<div className="hidden rounded-xl border border-slate-200 bg-white px-3 py-1.5 md:block">
|
||||
<div className="max-w-48 truncate font-semibold text-slate-800">{project?.project_id ?? 'Discovering project…'}</div>
|
||||
<div className="text-[10px] font-bold uppercase tracking-wide text-slate-400">{project?.edition ?? 'unknown'} edition · maturity {project?.maturity.level ? `L${project.maturity.level}` : 'not assessed'}</div>
|
||||
</div>
|
||||
<div className="hidden rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-slate-500 sm:block">
|
||||
<span className="font-medium text-slate-700">{data?.runs ?? '—'}</span> governed runs
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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 MARK: Record<HarnessGateStatus, string> = {
|
||||
queued: '○',
|
||||
running: '●',
|
||||
pass: '✓',
|
||||
warning: '!',
|
||||
blocked: '×',
|
||||
error: '×',
|
||||
skipped: '–',
|
||||
};
|
||||
|
||||
export function AssuranceRail({ graph }: { graph: HarnessTraceGraph }) {
|
||||
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>
|
||||
<div className="mt-2 truncate text-xs font-semibold">{node.title.split(' · ')[1]}</div>
|
||||
</div>
|
||||
{index < graph.nodes.length - 1 && <div className={`h-px w-3 shrink-0 ${node.status === 'pass' ? 'bg-emerald-300' : 'bg-slate-200'}`} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api, type HarnessGateNode, type HarnessGateStatus, type HarnessTraceGraph } from '../../lib/api';
|
||||
import { api, runReportExportUrl, type HarnessGateNode, type HarnessGateStatus, type HarnessTraceGraph } from '../../lib/api';
|
||||
import { Card, StatusBadge } from '../ui/Card';
|
||||
|
||||
const NODE_TONE: Record<HarnessGateStatus, string> = {
|
||||
@@ -82,7 +82,11 @@ export function TraceExplorer({ traceId, onClose }: { traceId: string; onClose?:
|
||||
return (
|
||||
<Card
|
||||
title="Harness trace explorer"
|
||||
right={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>}
|
||||
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"
|
||||
>
|
||||
<div className="mb-5 flex flex-wrap items-end justify-between gap-3">
|
||||
|
||||
@@ -59,8 +59,8 @@ export interface H6Breakdown {
|
||||
runs: number;
|
||||
failures: number;
|
||||
latency_avg_ms: number;
|
||||
tokens: number;
|
||||
cost_usd: number;
|
||||
tokens: number | null;
|
||||
cost_usd: number | null;
|
||||
}
|
||||
|
||||
export interface H6Report {
|
||||
@@ -87,10 +87,18 @@ export interface H6Report {
|
||||
failure_rate_pct: number;
|
||||
retries: number;
|
||||
latency_ms: { average: number; p50: number; p95: number; p99: number; max: number };
|
||||
tokens: { input: number; output: number; total: number; provider_total: number };
|
||||
cost_usd: { provider_actual: number; estimated: number };
|
||||
tokens: { input: number | null; output: number | null; total: number | null; provider_total: number | null };
|
||||
cost_usd: { provider_actual: number | null; estimated: number | null };
|
||||
provider_calls: number;
|
||||
alerts: number;
|
||||
coverage: {
|
||||
runtime_records: number;
|
||||
token_records: number;
|
||||
cost_records: number;
|
||||
token_pct: number;
|
||||
cost_pct: number;
|
||||
quality: { complete: number; partial: number; insufficient: number; unknown: number };
|
||||
};
|
||||
};
|
||||
thresholds: Record<string, number | string | boolean>;
|
||||
findings: H6ReportFinding[];
|
||||
@@ -141,11 +149,22 @@ export interface EvidencePackDetail extends EvidencePackSummary {
|
||||
export interface Overview extends Freshness {
|
||||
totals: {
|
||||
runs: number; total_cost: number; avg_latency_ms: number; failures: number;
|
||||
token_coverage_pct: number; cost_coverage_pct: number;
|
||||
hallucination_signals: number; provider_tokens: number; provider_cost: number;
|
||||
fallback_routes: number; tool_denies: number; action_blocks: number;
|
||||
};
|
||||
harness_signals: Record<string, Record<string, number | string>>;
|
||||
audit_chain: { records: number; head: string | null; last_decision: string | null };
|
||||
latest_assurance: {
|
||||
trace_id: string;
|
||||
certified: boolean;
|
||||
certification_strength: string | null;
|
||||
telemetry_quality: string;
|
||||
duration_ms: number | null;
|
||||
tool_calls: number;
|
||||
failures: number;
|
||||
graph: HarnessTraceGraph;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SettingsActor {
|
||||
@@ -581,8 +600,25 @@ export function h6ReportExportUrl(query: H6ReportQuery, format: 'json' | 'html')
|
||||
return `${base}/reports/h6/export?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function runReportExportUrl(traceId: string, format: 'json' | 'html'): string {
|
||||
const base = import.meta.env.VITE_API_BASE_URL ?? '/api/v1';
|
||||
return `${base}/reports/run/${encodeURIComponent(traceId)}/export?format=${format}`;
|
||||
}
|
||||
|
||||
export interface ProjectProfile {
|
||||
project_id: string;
|
||||
app_root: string;
|
||||
edition: string;
|
||||
edition_status: string;
|
||||
maturity: { level: number | null; status: string; evidence: string | null };
|
||||
enforcement_mode: string;
|
||||
integration_mode: string;
|
||||
clients: string[];
|
||||
}
|
||||
|
||||
export const api = {
|
||||
session: () => get<SettingsActor>('session'),
|
||||
project: () => get<ProjectProfile>('project'),
|
||||
overview: () => get<Overview>('overview'),
|
||||
runs: (limit = 50) => get<Freshness & { count: number; runs: HarnessRunRecord[] }>(`runs?limit=${limit}`),
|
||||
traceGraph: (traceId: string) => get<HarnessTraceGraph>(`runs/${encodeURIComponent(traceId)}/graph`),
|
||||
|
||||
@@ -31,7 +31,7 @@ function MetricCard({ label, value, sub, accent = false }: { label: string; valu
|
||||
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 text-2xl font-semibold tracking-tight text-slate-900">{value}</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>;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ function BreakdownTable({ rows, subject }: { rows: H6Breakdown[]; subject: strin
|
||||
<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>{integer(row.tokens)}</td><td>{money(row.cost_usd)}</td>
|
||||
<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>;
|
||||
}
|
||||
@@ -108,9 +108,22 @@ export function H6ReportPage() {
|
||||
<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={integer(report.data.summary.tokens.provider_total)} sub={`${report.data.summary.provider_calls} calls`} accent />
|
||||
<MetricCard label="Actual cost" value={money(report.data.summary.cost_usd.provider_actual)} sub="provider reported" />
|
||||
<MetricCard label="Estimated cost" value={money(report.data.summary.cost_usd.estimated)} sub="runtime fallback" />
|
||||
<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`} />
|
||||
</section>
|
||||
|
||||
@@ -127,6 +140,10 @@ export function H6ReportPage() {
|
||||
|
||||
<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>
|
||||
</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>}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../lib/api';
|
||||
import { Card, StatTile, StatusBadge } from '../components/ui/Card';
|
||||
import { AssuranceRail } from '../components/trace/AssuranceRail';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function SignalMark({ tone }: { tone: 'indigo' | 'emerald' | 'amber' }) {
|
||||
const color = { indigo: 'bg-indigo-500', emerald: 'bg-emerald-500', amber: 'bg-amber-500' }[tone];
|
||||
@@ -39,10 +41,41 @@ export function Overview() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{data.latest_assurance && (
|
||||
<section className="rounded-2xl border border-slate-200 bg-white p-5 shadow-[0_15px_35px_rgba(15,23,42,0.05)] sm:p-6">
|
||||
<div className="mb-4 flex flex-col justify-between gap-3 sm:flex-row sm:items-end">
|
||||
<div>
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.18em] text-indigo-600">Latest assurance receipt</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-slate-900">{data.latest_assurance.certified ? 'Certified governed run' : 'Run requires review'}</h3>
|
||||
<StatusBadge value={data.latest_assurance.certified ? 'certified' : 'attention'} />
|
||||
<StatusBadge value={`H6 ${data.latest_assurance.telemetry_quality}`} />
|
||||
</div>
|
||||
<p className="mt-1 max-w-2xl truncate font-mono text-[11px] text-slate-400">{data.latest_assurance.trace_id}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link to={`/reports/h6?run=${encodeURIComponent(data.latest_assurance.trace_id)}`} className="rounded-xl border border-indigo-200 bg-indigo-50 px-3 py-2 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100">Inspect H6</Link>
|
||||
<Link to={`/runs?trace=${encodeURIComponent(data.latest_assurance.trace_id)}`} className="rounded-xl bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-700">Open live trace</Link>
|
||||
</div>
|
||||
</div>
|
||||
<AssuranceRail graph={data.latest_assurance.graph} />
|
||||
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 border-t border-slate-100 pt-4 text-xs text-slate-500">
|
||||
<span><strong className="text-slate-800">{data.latest_assurance.graph.progress}/7</strong> gates observed</span>
|
||||
<span><strong className="text-slate-800">{data.latest_assurance.tool_calls}</strong> tools</span>
|
||||
<span><strong className="text-slate-800">{data.latest_assurance.failures}</strong> failures</span>
|
||||
<span><strong className="text-slate-800">{data.latest_assurance.duration_ms == null ? '—' : `${Math.round(data.latest_assurance.duration_ms / 1000)}s`}</strong> duration</span>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-8">
|
||||
<StatTile label="Runs" value={totals.runs} />
|
||||
<StatTile label="Failures" value={totals.failures} />
|
||||
<StatTile label="Model cost" value={`$${totals.total_cost.toFixed(4)}`} sub={`${totals.provider_tokens.toLocaleString()} tokens`} />
|
||||
<StatTile
|
||||
label="Model cost"
|
||||
value={totals.cost_coverage_pct > 0 ? `$${totals.total_cost.toFixed(4)}` : 'Unavailable'}
|
||||
sub={`${totals.cost_coverage_pct}% cost coverage`}
|
||||
/>
|
||||
<StatTile label="Latency" value={`${totals.avg_latency_ms}ms`} sub="average" />
|
||||
<StatTile label="Fallbacks" value={totals.fallback_routes} />
|
||||
<StatTile label="Tool denies" value={totals.tool_denies} />
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Runs() {
|
||||
<td><StatusBadge value={run.status ?? '—'} /></td>
|
||||
<td>{run.latency_ms ?? '—'} ms</td>
|
||||
<td>{run.total_tokens ?? '—'}</td>
|
||||
<td>${Number(run.cost_estimate ?? 0).toFixed(5)}</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">
|
||||
{run.trace_id ? (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user