feat: add treemap and punchcard heatmap to dashboard L1/L2 (#94)

Treemap: rectangle area = transaction volume, color = SLA compliance
(green→red). Shows apps at L1, routes at L2. Click navigates deeper.

Punchcard heatmap: 7-day rolling weekday x 24-hour grid showing
transaction volume and error patterns. Two side-by-side views
(transactions + errors) reveal temporal clustering.

Backend: new GET /search/stats/punchcard endpoint aggregating
stats_1m_all/app by DOW x hour over rolling 7 days.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-30 10:26:26 +02:00
parent b5c19b6774
commit 9d2d87e7e1
9 changed files with 433 additions and 17 deletions

View File

@@ -18,8 +18,12 @@ import {
useTimeseriesByRoute,
useTopErrors,
useAppSettings,
usePunchcard,
} from '../../api/queries/dashboard';
import type { TopError } from '../../api/queries/dashboard';
import { Treemap } from './Treemap';
import type { TreemapItem } from './Treemap';
import { PunchcardHeatmap } from './PunchcardHeatmap';
import type { RouteMetrics } from '../../api/types';
import {
trendArrow,
@@ -272,28 +276,34 @@ export default function DashboardL2() {
const { data: metrics } = useRouteMetrics(timeFrom, timeTo, appId);
const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId);
const { data: errors } = useTopErrors(timeFrom, timeTo, appId);
const { data: punchcardData } = usePunchcard(appId);
const { data: appSettings } = useAppSettings(appId);
const slaThresholdMs = appSettings?.slaThresholdMs ?? 300;
// Route performance table rows
const routeRows: RouteRow[] = useMemo(() =>
(metrics || []).map((m: RouteMetrics) => {
const sla = m.p99DurationMs <= slaThresholdMs
? 99.9
: Math.max(0, 100 - ((m.p99DurationMs - slaThresholdMs) / slaThresholdMs) * 10);
return {
id: m.routeId,
routeId: m.routeId,
exchangeCount: m.exchangeCount,
successRate: m.successRate,
avgDurationMs: m.avgDurationMs,
p99DurationMs: m.p99DurationMs,
slaCompliance: sla,
sparkline: m.sparkline ?? [],
};
}),
[metrics, slaThresholdMs],
(metrics || []).map((m: RouteMetrics) => ({
id: m.routeId,
routeId: m.routeId,
exchangeCount: m.exchangeCount,
successRate: m.successRate,
avgDurationMs: m.avgDurationMs,
p99DurationMs: m.p99DurationMs,
slaCompliance: (m as Record<string, unknown>).slaCompliance as number ?? -1,
sparkline: m.sparkline ?? [],
})),
[metrics],
);
// Treemap items: one per route, sized by exchange count, colored by SLA
const treemapItems: TreemapItem[] = useMemo(
() => routeRows.map(r => ({
id: r.routeId, label: r.routeId,
value: r.exchangeCount,
slaCompliance: r.slaCompliance >= 0 ? r.slaCompliance : 100,
})),
[routeRows],
);
// KPI sparklines from timeseries
@@ -416,6 +426,30 @@ export default function DashboardL2() {
/>
</div>
)}
{/* Treemap: route volume vs SLA compliance */}
{treemapItems.length > 0 && (
<Card title="Route Volume vs SLA Compliance">
<Treemap
items={treemapItems}
width={800}
height={250}
onItemClick={(id) => navigate(`/dashboard/${appId}/${id}`)}
/>
</Card>
)}
{/* Punchcard heatmaps: transactions and errors by weekday x hour */}
{(punchcardData?.length ?? 0) > 0 && (
<div className={styles.chartGrid}>
<Card title="Transaction Volume (7-day pattern)">
<PunchcardHeatmap cells={punchcardData!} mode="transactions" width={380} height={300} />
</Card>
<Card title="Error Volume (7-day pattern)">
<PunchcardHeatmap cells={punchcardData!} mode="errors" width={380} height={300} />
</Card>
</div>
)}
</div>
);
}