diff --git a/packages/dashboard/app/components/ReliabilityView.css b/packages/dashboard/app/components/ReliabilityView.css index 7338802f96..c45d2dd601 100644 --- a/packages/dashboard/app/components/ReliabilityView.css +++ b/packages/dashboard/app/components/ReliabilityView.css @@ -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)); + } } diff --git a/packages/dashboard/app/components/ReliabilityView.tsx b/packages/dashboard/app/components/ReliabilityView.tsx index a655402672..43c63c9012 100644 --- a/packages/dashboard/app/components/ReliabilityView.tsx +++ b/packages/dashboard/app/components/ReliabilityView.tsx @@ -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(() => { + 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( + () => 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")} +
+

{t("reliability.flowChart.heading", "Entered vs bounced trend")}

+ +
@@ -196,8 +230,16 @@ export function ReliabilityView() {

{t("reliability.mergeAttempts.heading", "Merge attempts")}

{t("reliability.mergeAttempts.mean", "Mean")}{data?.mergeAttempts.mean?.toFixed(2) ?? "—"}
{t("reliability.mergeAttempts.max", "Max")}{data?.mergeAttempts.max ?? "—"}
+
+

{t("reliability.mergeAttemptsChart.heading", "Attempts distribution")}

+ +
{t("reliability.table.date", "Date")}{t("reliability.table.entered", "Entered")}{t("reliability.table.bounced", "Bounced")}