FN-6719: add reliability charts to Command Center

Adds visual reliability trends and distributions alongside the existing Command Center reliability tables.

- Render an entered-vs-bounced line chart from the filtered reliability per-day rows.
- Render a merge-attempts pie chart from the sorted histogram entries.
- Add scoped chart layout styles and regression coverage for populated, empty, and filtered chart states.

Files changed:
 .../dashboard/app/components/ReliabilityView.css   | 37 +++++++++++
 .../dashboard/app/components/ReliabilityView.tsx   | 50 ++++++++++++--
 .../components/__tests__/ReliabilityView.test.tsx  | 76 +++++++++++++++++++++-
 3 files changed, 158 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-6719

Fusion-Task-Lineage: 4b366f66-1fa2-4014-a51a-cb1c992447db
This commit is contained in:
gsxdsm
2026-06-19 08:06:38 -07:00
parent c75135d9be
commit 82068f0b59
3 changed files with 158 additions and 5 deletions

View File

@@ -72,6 +72,34 @@
gap: var(--space-sm);
}
/*
FNXC:Reliability 2026-06-19-00:00:
Reliability charts use shared recharts wrappers, whose ResponsiveContainer needs a measurable parent block-size. Keep the scoped section token-sized so the new charts render while preserving the view's flex-fill scroll contract.
*/
.reliability-chart-section {
display: flex;
flex-direction: column;
gap: var(--space-sm);
min-inline-size: 0;
margin-block-start: var(--space-md);
padding: var(--space-md);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
background: var(--surface-1);
}
.reliability-chart-section h4 {
margin: 0;
color: var(--text-muted);
font-size: var(--font-size-xs);
font-weight: 600;
}
.reliability-chart-section .cc-recharts-chart,
.reliability-chart-section .cc-recharts-empty {
block-size: clamp(calc(var(--space-2xl) * 4), 34vh, calc(var(--space-2xl) * 7));
}
.reliability-table {
width: 100%;
border-collapse: collapse;
@@ -159,4 +187,13 @@
flex-direction: column;
align-items: flex-start;
}
.reliability-chart-section {
padding: var(--space-sm);
}
.reliability-chart-section .cc-recharts-chart,
.reliability-chart-section .cc-recharts-empty {
block-size: clamp(calc(var(--space-2xl) * 3), 46vh, calc(var(--space-2xl) * 6));
}
}

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertCircle, Loader2 } from "lucide-react";
import { LineChart, PieChart } from "./command-center/charts/recharts";
import type { LineChartSeries, PieChartDatum } from "./command-center/charts/recharts";
import "./ReliabilityView.css";
type ReliabilityResponse = {
@@ -102,14 +104,38 @@ export function ReliabilityView() {
const perDayRows = useMemo(() => {
if (!data?.perDay) return [];
return showEmptyDays ? data.perDay : data.perDay.filter((row) => row.hasSamples !== false);
const filteredRows = showEmptyDays ? data.perDay : data.perDay.filter((row) => row.hasSamples !== false);
return [...filteredRows].sort((left, right) => left.date.localeCompare(right.date));
}, [data?.perDay, showEmptyDays]);
const mergeAttemptTaskCount = useMemo(
() => Object.values(data?.mergeAttempts.histogram ?? {}).reduce((sum, count) => sum + count, 0),
/*
FNXC:Reliability 2026-06-19-00:00:
The in-review trend chart must reuse the same perDayRows source as the table, including the Show empty days filter, so visual and tabular reliability surfaces never disagree about which dates are represented.
*/
const flowChartSeries = useMemo<LineChartSeries[]>(() => {
const hasFlowSamples = perDayRows.some((row) => row.tasksEnteredInReview > 0 || row.tasksBouncedToInProgress > 0);
if (!hasFlowSamples) return [];
return [
{ label: t("reliability.flowChart.entered", "Entered"), values: perDayRows.map((row) => row.tasksEnteredInReview) },
{ label: t("reliability.flowChart.bounced", "Bounced"), values: perDayRows.map((row) => row.tasksBouncedToInProgress) },
];
}, [perDayRows, t]);
const mergeAttemptsHistogramEntries = useMemo(
() => Object.entries(data?.mergeAttempts.histogram ?? {}).sort(([left], [right]) => left.localeCompare(right, undefined, { numeric: true })),
[data?.mergeAttempts.histogram],
);
const mergeAttemptsChartData = useMemo<PieChartDatum[]>(
() => mergeAttemptsHistogramEntries.map(([bucket, count]) => ({ label: bucket, value: count })),
[mergeAttemptsHistogramEntries],
);
const mergeAttemptTaskCount = useMemo(
() => mergeAttemptsHistogramEntries.reduce((sum, [, count]) => sum + count, 0),
[mergeAttemptsHistogramEntries],
);
const windowStartLabel = data
? formatDateTime(data.resetAt ?? new Date(Date.parse(data.generatedAt) - data.windowDays * 86_400_000).toISOString())
: "—";
@@ -166,6 +192,14 @@ export function ReliabilityView() {
{showEmptyDays ? t("reliability.hideEmptyDays", "Hide empty days") : t("reliability.showEmptyDays", "Show empty days")}
</button>
</div>
<div className="reliability-chart-section" data-testid="reliability-flow-chart">
<h4>{t("reliability.flowChart.heading", "Entered vs bounced trend")}</h4>
<LineChart
series={flowChartSeries}
ariaLabel={t("reliability.flowChart.aria", "In-review entered vs bounced per day")}
emptyLabel={t("reliability.flowChart.empty", "No in-review flow data")}
/>
</div>
<table className="reliability-table">
<thead><tr><th>{t("reliability.table.date", "Date")}</th><th>{t("reliability.table.entered", "Entered")}</th><th>{t("reliability.table.bounced", "Bounced")}</th></tr></thead>
<tbody>
@@ -196,8 +230,16 @@ export function ReliabilityView() {
<h3>{t("reliability.mergeAttempts.heading", "Merge attempts")}</h3>
<div className="reliability-stat-row"><span>{t("reliability.mergeAttempts.mean", "Mean")}</span><strong>{data?.mergeAttempts.mean?.toFixed(2) ?? "—"}</strong></div>
<div className="reliability-stat-row"><span>{t("reliability.mergeAttempts.max", "Max")}</span><strong>{data?.mergeAttempts.max ?? "—"}</strong></div>
<div className="reliability-chart-section" data-testid="reliability-merge-attempts-chart">
<h4>{t("reliability.mergeAttemptsChart.heading", "Attempts distribution")}</h4>
<PieChart
data={mergeAttemptsChartData}
ariaLabel={t("reliability.mergeAttemptsChart.aria", "Merge attempts histogram")}
emptyLabel={t("reliability.mergeAttemptsChart.empty", "No merge attempt data")}
/>
</div>
<ul className="reliability-histogram">
{Object.entries(data?.mergeAttempts.histogram ?? {}).map(([bucket, count]) => (
{mergeAttemptsHistogramEntries.map(([bucket, count]) => (
<li key={bucket}>
<span>{bucket}</span>
<div className="reliability-histogram-bar-wrap"><div className="reliability-histogram-bar" style={{ width: `${Math.min(count * 20, 100)}%` }} /></div>

View File

@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ReliabilityView } from "../ReliabilityView";
@@ -167,6 +167,80 @@ describe("ReliabilityView", () => {
expect(screen.getByText("2026-05-12")).toBeInTheDocument();
});
it("renders the in-review flow chart for populated reliability data", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => baseResponse } as Response);
render(<ReliabilityView />);
await waitFor(() => expect(screen.getByRole("img", { name: "In-review entered vs bounced per day" })).toBeInTheDocument());
expect(within(screen.getByTestId("reliability-flow-chart")).queryByText("No in-review flow data")).not.toBeInTheDocument();
});
it("renders the merge-attempts chart for populated reliability data", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => baseResponse } as Response);
render(<ReliabilityView />);
await waitFor(() => expect(screen.getByRole("img", { name: "Merge attempts histogram" })).toBeInTheDocument());
expect(within(screen.getByTestId("reliability-merge-attempts-chart")).queryByText("No merge attempt data")).not.toBeInTheDocument();
});
it("renders chart empty states without throwing when reliability series are empty", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({
...baseResponse,
headline: { inReviewFailureRate7d: null, reason: "no-in-review-entries" },
perDay: [],
mergeAttempts: { mean: null, max: null, histogram: {}, reason: "no-audit-coverage" },
}),
} as Response);
render(<ReliabilityView />);
await waitFor(() => expect(screen.getByRole("img", { name: "In-review entered vs bounced per day" })).toBeInTheDocument());
expect(screen.getByRole("img", { name: "Merge attempts histogram" })).toBeInTheDocument();
expect(screen.getByText("No in-review flow data")).toBeInTheDocument();
expect(screen.getByText("No merge attempt data")).toBeInTheDocument();
});
it("keeps the flow chart source consistent with the Show empty days table toggle", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({
...baseResponse,
perDay: [
{
date: "2026-05-12",
tasksEnteredInReview: 4,
tasksBouncedToInProgress: 1,
postMergeAuditFailures: null,
fileScopeInvariantFailures: null,
recoverAlreadyMergedReviewTasksRecoveries: null,
hasSamples: false,
},
{
date: "2026-05-13",
tasksEnteredInReview: 0,
tasksBouncedToInProgress: 0,
postMergeAuditFailures: null,
fileScopeInvariantFailures: null,
recoverAlreadyMergedReviewTasksRecoveries: null,
hasSamples: true,
},
],
}),
} as Response);
render(<ReliabilityView />);
await waitFor(() => expect(screen.getByText("2026-05-13")).toBeInTheDocument());
expect(screen.queryByText("2026-05-12")).not.toBeInTheDocument();
expect(within(screen.getByTestId("reliability-flow-chart")).getByText("No in-review flow data")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Show empty days" }));
expect(screen.getByText("2026-05-12")).toBeInTheDocument();
expect(within(screen.getByTestId("reliability-flow-chart")).queryByText("No in-review flow data")).not.toBeInTheDocument();
});
it("opens reset modal and confirms reset with refetch", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
.mockResolvedValueOnce({ ok: true, json: async () => baseResponse } as Response)