feat(FN-4608): complete Step 3 — add reliability details and drill-downs
Fusion-Task-Id: FN-4608 Fusion-Task-Lineage: 6cabc509-38a1-4cb8-be6f-51c53d5753ff
This commit is contained in:
@@ -32,6 +32,13 @@
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.reliability-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.reliability-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -56,6 +63,23 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.reliability-details {
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.reliability-details summary {
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reliability-details-content {
|
||||
margin-top: var(--space-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.reliability-histogram {
|
||||
list-style: none;
|
||||
margin: var(--space-sm) 0 0;
|
||||
@@ -92,4 +116,9 @@
|
||||
.reliability-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.reliability-section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "./ReliabilityView.css";
|
||||
type ReliabilityResponse = {
|
||||
windowDays: number;
|
||||
generatedAt: string;
|
||||
resetAt: string | null;
|
||||
headline: { inReviewFailureRate7d: number | null; reason?: string };
|
||||
perDay: Array<{
|
||||
date: string;
|
||||
@@ -12,6 +13,7 @@ type ReliabilityResponse = {
|
||||
postMergeAuditFailures: { block: number; warn: number; off: number } | null;
|
||||
fileScopeInvariantFailures: number | null;
|
||||
recoverAlreadyMergedReviewTasksRecoveries: number | null;
|
||||
hasSamples?: boolean;
|
||||
}>;
|
||||
duration: { p50Ms: number | null; p95Ms: number | null; sampleCount: number; reason?: string };
|
||||
mergeAttempts: { mean: number | null; max: number | null; histogram: Record<string, number>; reason?: string };
|
||||
@@ -29,8 +31,20 @@ function formatDuration(value: number | null): string {
|
||||
return `${minutes.toFixed(1)}m`;
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return "—";
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
|
||||
export function ReliabilityView() {
|
||||
const [data, setData] = useState<ReliabilityResponse | null>(null);
|
||||
const [showEmptyDays, setShowEmptyDays] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const response = await fetch("/api/health/reliability");
|
||||
@@ -63,6 +77,30 @@ export function ReliabilityView() {
|
||||
return "var(--color-error)";
|
||||
}, [data]);
|
||||
|
||||
const totalEntered = useMemo(
|
||||
() => (data?.perDay ?? []).reduce((sum, row) => sum + row.tasksEnteredInReview, 0),
|
||||
[data?.perDay],
|
||||
);
|
||||
const totalBounced = useMemo(
|
||||
() => (data?.perDay ?? []).reduce((sum, row) => sum + row.tasksBouncedToInProgress, 0),
|
||||
[data?.perDay],
|
||||
);
|
||||
|
||||
const perDayRows = useMemo(() => {
|
||||
if (!data?.perDay) {
|
||||
return [];
|
||||
}
|
||||
if (showEmptyDays) {
|
||||
return data.perDay;
|
||||
}
|
||||
return data.perDay.filter((row) => row.hasSamples !== false);
|
||||
}, [data?.perDay, showEmptyDays]);
|
||||
|
||||
const mergeAttemptTaskCount = useMemo(
|
||||
() => Object.values(data?.mergeAttempts.histogram ?? {}).reduce((sum, count) => sum + count, 0),
|
||||
[data?.mergeAttempts.histogram],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="reliability-view">
|
||||
<div className="card reliability-card reliability-headline-card">
|
||||
@@ -72,11 +110,25 @@ export function ReliabilityView() {
|
||||
? `Insufficient data — ${data?.headline.reason ?? "unknown"}`
|
||||
: formatPercent(data.headline.inReviewFailureRate7d)}
|
||||
</div>
|
||||
<details className="reliability-details">
|
||||
<summary>Details</summary>
|
||||
<div className="reliability-details-content">
|
||||
<div>{`${totalBounced} bounced / ${totalEntered} entered (last ${data?.windowDays ?? 7}d)`}</div>
|
||||
<div>{`Window: ${data ? formatDateTime(data.resetAt ?? new Date(Date.parse(data.generatedAt) - data.windowDays * 86_400_000).toISOString()) : "—"} → ${formatDateTime(data?.generatedAt)}`}</div>
|
||||
{data?.resetAt ? <div>{`Reset baseline: ${formatDateTime(data.resetAt)}`}</div> : null}
|
||||
{data?.headline.reason ? <div>{`Reason: ${data.headline.reason}`}</div> : null}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="reliability-grid">
|
||||
<div className="card reliability-card">
|
||||
<h3>In-review flow</h3>
|
||||
<div className="reliability-section-header">
|
||||
<h3>In-review flow</h3>
|
||||
<button className="btn btn-sm" onClick={() => setShowEmptyDays((value) => !value)}>
|
||||
{showEmptyDays ? "Hide empty days" : "Show empty days"}
|
||||
</button>
|
||||
</div>
|
||||
<table className="reliability-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -86,7 +138,7 @@ export function ReliabilityView() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.perDay.map((row) => (
|
||||
{perDayRows.map((row) => (
|
||||
<tr key={row.date}>
|
||||
<td>{row.date}</td>
|
||||
<td>{row.tasksEnteredInReview}</td>
|
||||
@@ -102,6 +154,15 @@ export function ReliabilityView() {
|
||||
<div className="reliability-stat-row"><span>P50</span><strong>{formatDuration(data?.duration.p50Ms ?? null)}</strong></div>
|
||||
<div className="reliability-stat-row"><span>P95</span><strong>{formatDuration(data?.duration.p95Ms ?? null)}</strong></div>
|
||||
<div className="reliability-muted">Samples: {data?.duration.sampleCount ?? 0}</div>
|
||||
<details className="reliability-details">
|
||||
<summary>More stats</summary>
|
||||
<div className="reliability-details-content">
|
||||
<div>{`P50 raw: ${data?.duration.p50Ms ?? "—"} ms`}</div>
|
||||
<div>{`P95 raw: ${data?.duration.p95Ms ?? "—"} ms`}</div>
|
||||
<div>{`Sample count: ${data?.duration.sampleCount ?? 0}`}</div>
|
||||
{data?.duration.reason ? <div>{`Reason: ${data.duration.reason}`}</div> : null}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="card reliability-card">
|
||||
@@ -117,6 +178,14 @@ export function ReliabilityView() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<details className="reliability-details">
|
||||
<summary>More stats</summary>
|
||||
<div className="reliability-details-content">
|
||||
<div>{`Tasks counted: ${mergeAttemptTaskCount}`}</div>
|
||||
<div>{`Histogram total: ${mergeAttemptTaskCount}`}</div>
|
||||
{data?.mergeAttempts.reason ? <div>{`Reason: ${data.mergeAttempts.reason}`}</div> : null}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user