`;
+ html += `
${cat}
`;
+ keys.forEach((k, ki) => {
+ const v = counts[ki][dataPointIndex];
+ if (!v) return;
+ const lbl = STATE_LABELS[k];
+ html += `
${lbl}: ${v}
`;
+ const tn = tenantsByBucket[dataPointIndex][lbl];
+ if (tn) {
+ const parts = Object.entries(tn)
+ .map(([t, c]) => `${t} (${c})`)
+ .join(", ");
+ html += `
${parts}
`;
+ }
+ });
+ html += `
`;
+ return html;
+ },
+ },
+ };
+ return { series, options };
+ }, [rows, theme]);
+
+ // Latency stage breakdown + audit volume over the same buckets.
+ const trendCharts = useMemo(() => {
+ const b = buildBuckets(rows);
+ if (!b) return null;
+ const sums = [0, 1, 2].map(() => new Array(b.count).fill(0));
+ const cnts = new Array(b.count).fill(0);
+ const recSum = new Array(b.count).fill(0);
+ const matSum = new Array(b.count).fill(0);
+ for (const r of rows) {
+ if (r.Type === "Reconciliation" || r.Type === "Manual") continue;
+ const i = bucketIndexOf(toMs(r.WindowStart), b);
+ if (i < 0) continue;
+ recSum[i] += Number(r.RecordCount) || 0;
+ matSum[i] += Number(r.MatchedCount) || 0;
+ if (r.State === "Processed") {
+ const l = latencyMinutes(r);
+ if (l.total != null && l.total >= 0) {
+ sums[0][i] += Math.max(0, l.create || 0);
+ sums[1][i] += Math.max(0, l.download || 0);
+ sums[2][i] += Math.max(0, l.process || 0);
+ cnts[i] += 1;
+ }
+ }
+ }
+ const labels = Array.from({ length: b.count }, (_, i) => bucketLabel(bucketStartMs(i, b)));
+ const avg = (si) => sums[si].map((s, i) => (cnts[i] ? Math.round(s / cnts[i]) : null));
+ const axis = {
+ xaxis: {
+ categories: labels,
+ tickAmount: Math.min(10, b.count),
+ labels: { rotate: -45, hideOverlappingLabels: true, style: { fontSize: "10px" } },
+ },
+ yaxis: { min: 0, forceNiceScale: true, labels: { formatter: (v) => Math.round(v) } },
+ legend: { show: true, position: "top" },
+ grid: { borderColor: theme.palette.divider },
+ dataLabels: { enabled: false },
+ theme: { mode: theme.palette.mode },
+ };
+ return {
+ latSeries: [
+ { name: "Create lag", data: avg(0) },
+ { name: "Download lag", data: avg(1) },
+ { name: "Process lag", data: avg(2) },
+ ],
+ latOptions: {
+ ...axis,
+ chart: { type: "area", stacked: true, background: "transparent", toolbar: { show: false }, zoom: { enabled: false } },
+ colors: [theme.palette.warning.main, theme.palette.info?.main || theme.palette.primary.main, theme.palette.success.main],
+ stroke: { curve: "smooth", width: 2 },
+ fill: { type: "solid", opacity: 0.25 },
+ tooltip: { theme: theme.palette.mode, y: { formatter: (v) => (v == null ? "โ" : `${Math.round(v)} min`) } },
+ },
+ volSeries: [
+ { name: "Records", data: recSum },
+ { name: "Matched", data: matSum },
+ ],
+ volOptions: {
+ ...axis,
+ chart: { type: "line", background: "transparent", toolbar: { show: false }, zoom: { enabled: false } },
+ colors: [theme.palette.primary.main, theme.palette.error.main],
+ stroke: { curve: "smooth", width: 2 },
+ markers: { size: 0, hover: { size: 4 } },
+ tooltip: { theme: theme.palette.mode },
+ },
+ };
+ }, [rows, theme]);
+
+ // Triage feed: windows that errored, retried, throttled, were skipped or dead-lettered. Newest first.
+ const problems = useMemo(() => {
+ const num = (v) => Number(v) || 0;
+ const list = rows.filter(
+ (r) =>
+ r.State === "DeadLetter" ||
+ r.State === "Skipped" ||
+ num(r.RetryCount) > 0 ||
+ num(r.ThrottleCount) > 0
+ );
+ list.sort((a, b) => {
+ const ta = toMs(a.LastErrorUtc) || toMs(a.WindowStart) || 0;
+ const tb = toMs(b.LastErrorUtc) || toMs(b.WindowStart) || 0;
+ return tb - ta;
+ });
+ return list.slice(0, 12);
+ }, [rows]);
+
+ const offCanvas = {
+ children: (row) =>