milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 17422fac21
commit 648d9b4496
89 changed files with 7222 additions and 4239 deletions
+282
View File
@@ -0,0 +1,282 @@
/**
* Client activity over the same window as the query-volume chart: one stacked
* series per named client, plus everything outside the top eight as "Other".
*
* The x-axis is the timeseries endpoint's own bucket alignment, so the two
* charts stack directly above one another and a spike in one is at the same
* horizontal position in the other. Colour keys on the client string, so a
* client that changes rank between polls keeps its colour.
*/
import { useEffect, useRef, useState } from "react";
import * as stylex from "@stylexjs/stylex";
import { formatTime } from "@/lib/format";
import type { StatsClients } from "@/lib/types";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { layoutStacked } from "./chartLayout";
import { clientLabel, useClientNames, type ClientNames } from "@/features/clients/clientNames";
import { OTHER_KEY, clientKey, seriesColor } from "./seriesColors";
const CHART_HEIGHT = 240;
const FALLBACK_WIDTH = 640;
const styles = stylex.create({
empty: {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: CHART_HEIGHT,
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "dashed",
borderColor: colors.borderStrong,
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
root: {
position: "relative",
},
gridLine: {
stroke: colors.border,
},
axisLine: {
stroke: colors.borderStrong,
},
axisLabel: {
fill: colors.textMuted,
fontSize: "10px",
},
/** The hairline separating touching segments is the page ground, not a colour. */
segment: {
stroke: colors.surface,
},
legend: {
marginTop: "0.5rem",
display: "flex",
flexWrap: "wrap",
columnGap: "1rem",
rowGap: "0.25rem",
listStyleType: "none",
padding: 0,
margin: 0,
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textSecondary,
},
legendItem: {
display: "flex",
alignItems: "center",
gap: "0.375rem",
},
swatch: {
display: "inline-block",
width: "0.625rem",
height: "0.625rem",
borderRadius: "0.125rem",
},
/** Dynamic: the swatch takes the colour the bars are drawn in. */
swatchColor: (color: string) => ({ backgroundColor: color }),
});
function useContainerWidth(): [React.RefObject<HTMLDivElement | null>, number] {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const el = ref.current;
if (el === null) return;
setWidth(el.clientWidth);
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => setWidth(el.clientWidth));
observer.observe(el);
return () => observer.disconnect();
}, []);
return [ref, width];
}
const compact = new Intl.NumberFormat(undefined, { notation: "compact" });
function formatTick(ts: number, bucketSeconds: number): string {
const date = new Date(ts * 1000);
if (bucketSeconds >= 86_400) {
return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date);
}
return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(date);
}
interface Series {
key: string;
label: string;
/** Kept beside the label so a renamed client is still identifiable by address. */
address: string | null;
color: string;
buckets: number[];
}
/**
* "Other" last, so it sits at the top of every column rather than under a
* client, and always present: the response always carries the series, and a
* legend that dropped it on a quiet period would make the reader think the
* chart's clients were all of them.
*/
function seriesOf(data: StatsClients, names: ClientNames): Series[] {
const named = data.clients.map((client) => ({
key: clientKey(client.client),
// The name if the client is registered under one, the address otherwise —
// the same precedence and the same lookup the query tables use. The colour
// keys on the address regardless, so naming a client never repaints it.
label: clientLabel(client.client, names)?.text ?? client.client,
address: client.client,
color: seriesColor(clientKey(client.client)),
buckets: client.buckets,
}));
return [
...named,
{ key: OTHER_KEY, label: "Other", address: null, color: seriesColor(OTHER_KEY), buckets: data.other },
];
}
export default function ClientChart({ data }: { data: StatsClients }) {
const [containerRef, measuredWidth] = useContainerWidth();
const names = useClientNames();
const width = measuredWidth > 0 ? measuredWidth : FALLBACK_WIDTH;
const series = seriesOf(data, names);
const bucketCount = data.other.length;
if (bucketCount === 0) {
return (
<div ref={containerRef} {...stylex.props(styles.empty)}>
No queries in this period.
</div>
);
}
const columns = Array.from({ length: bucketCount }, (_, i) => ({
ts: data.since + i * data.bucket_seconds,
values: series.map((one) => one.buckets[i] ?? 0),
}));
if (columns.every((column) => column.values.every((value) => value === 0))) {
return (
<div ref={containerRef} {...stylex.props(styles.empty)}>
No queries in this period.
</div>
);
}
const layout = layoutStacked(columns, width, CHART_HEIGHT);
const baseline = layout.plot.y + layout.plot.height;
return (
<div ref={containerRef} {...stylex.props(styles.root)}>
<svg
role="img"
aria-label={`Client activity over time, ${bucketCount} buckets, ${series.length} series`}
width="100%"
height={CHART_HEIGHT}
viewBox={`0 0 ${width} ${CHART_HEIGHT}`}
>
{layout.yTicks.map((tick) => (
<g key={tick.value}>
<line
x1={layout.plot.x}
x2={layout.plot.x + layout.plot.width}
y1={tick.y}
y2={tick.y}
{...stylex.props(styles.gridLine)}
/>
<text
x={layout.plot.x - 6}
y={tick.y}
textAnchor="end"
dominantBaseline="middle"
{...stylex.props(styles.axisLabel, shared.tabularNums)}
>
{compact.format(tick.value)}
</text>
</g>
))}
<line
x1={layout.plot.x}
x2={layout.plot.x + layout.plot.width}
y1={baseline}
y2={baseline}
{...stylex.props(styles.axisLine)}
/>
{layout.xTicks.map((tick) => (
<text
key={tick.ts}
x={tick.x}
y={baseline + 14}
textAnchor="middle"
{...stylex.props(styles.axisLabel)}
>
{formatTick(tick.ts, data.bucket_seconds)}
</text>
))}
{layout.columns.map((column) => (
<g key={column.ts}>
{column.segments.map((rect, index) =>
rect.height <= 0 ? null : (
<rect
key={series[index].key}
x={rect.x}
y={rect.y}
width={rect.width}
height={rect.height}
fill={series[index].color}
strokeWidth={rect.width > 3 ? 1 : 0}
{...stylex.props(styles.segment)}
/>
),
)}
<rect
x={column.slot.x}
y={column.slot.y}
width={column.slot.width}
height={column.slot.height}
fill="transparent"
>
<title>
{`${formatTime(column.ts)}: ${column.total} ${column.total === 1 ? "query" : "queries"}`}
</title>
</rect>
</g>
))}
</svg>
<ul {...stylex.props(styles.legend)}>
{series.map((one) => (
<li key={one.key} title={one.address ?? undefined} {...stylex.props(styles.legendItem)}>
<span aria-hidden="true" {...stylex.props(styles.swatch, styles.swatchColor(one.color))} />
{one.label}
</li>
))}
</ul>
<div {...stylex.props(shared.srOnly)}>
<table>
<caption>Queries per client per time bucket</caption>
<thead>
<tr>
<th scope="col">Time</th>
{series.map((one) => (
<th key={one.key} scope="col">
{one.label}
</th>
))}
</tr>
</thead>
<tbody>
{columns.map((column) => (
<tr key={column.ts}>
<th scope="row">{formatTime(column.ts)}</th>
{column.values.map((value, index) => (
<td key={series[index].key}>{value}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}