admin: overview redesign, device scope, one formatting contract (milestone 39)

The Overview page takes the decided visual language (specs/ui-visual-redesign.md): four centred totals with their Activity links, a smoothed area chart of total and blocked queries with point hover and a tooltip beside the point, a stacked client chart in eight distinct hues plus one Other band, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown.

GET /api/overview takes a client parameter; the scoped read uses idx_query_log_ts and the cache keeps scoped slots. The device selector beside the period selector is URL state, so a scoped view is a link, and the tile links carry the scope into Activity.

All numbers go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. A sweep test refuses toFixed, toLocaleString, Intl.NumberFormat and padStart anywhere else.

Chrome: one 4px radius from the metrics constants, shared Card with a prominent title and description, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), and the accent darkened to 5.43:1 on its wash.

Not built: the spec's ranked-list primitive, which has no consumer and no API rows. Codex could not review sessions B to D, its endpoint returned 404 for every call on 2026-09-03; the owner accepted the smoke-server screenshots.
This commit is contained in:
2026-09-03 17:16:24 +02:00
parent e656670dd4
commit 209c1c77a9
60 changed files with 2329 additions and 1009 deletions
+126
View File
@@ -0,0 +1,126 @@
/**
* The cache hit rate as a progress bar (ui-visual-redesign.md): the share of
* the window's queries answered from memory, the bar it fills, and under it the
* three figures that share went with — hits, what went upstream instead, and
* how long an answer took on average.
*/
import * as stylex from "@stylexjs/stylex";
import { formatCount, formatMicros, formatPercent } from "@/lib/format";
import Card from "@/ui/Card";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
const styles = stylex.create({
body: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
},
track: {
height: "0.5rem",
borderRadius: "999px",
backgroundColor: colors.chartGreenSurface,
overflow: "hidden",
},
fill: {
display: "block",
height: "100%",
borderRadius: "999px",
backgroundColor: colors.chartGreen,
},
/** Dynamic: the bar's length is the share itself. */
fillWidth: (percent: number) => ({ width: `${percent}%` }),
percent: {
margin: 0,
fontSize: "2.5rem",
lineHeight: 1,
fontWeight: 400,
letterSpacing: "-0.02em",
color: colors.chartGreen,
},
note: {
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
split: {
display: "flex",
flexWrap: "wrap",
gap: "1.5rem",
margin: 0,
marginTop: "0.5rem",
paddingTop: "1rem",
borderTopWidth: 1,
borderTopStyle: "solid",
borderTopColor: colors.border,
},
/** Label first in the DOM, figure on top visually. */
splitItem: {
display: "flex",
flexDirection: "column-reverse",
gap: "0.125rem",
},
splitValue: {
margin: 0,
fontSize: "1.125rem",
lineHeight: "1.5rem",
fontWeight: 550,
color: colors.text,
},
splitLabel: {
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
});
export interface CacheCardData {
queries: number;
/** Answers served from cache: the window's `cached` buckets summed. */
hits: number;
/** Answers that went to an upstream resolver or a forward zone. */
forwarded: number;
avg_response_time_us: number | null;
}
export default function CacheCard({ data }: { data: CacheCardData }) {
const share = data.queries === 0 ? null : data.hits / data.queries;
const percent = share === null ? "—" : formatPercent(share);
return (
<Card
title="Cache hit rate"
description="Answers served straight from memory, without asking an upstream resolver."
>
<div {...stylex.props(styles.body)}>
<div
role="img"
aria-label={
share === null ? "No queries in this period" : `${percent} of queries served from cache`
}
{...stylex.props(styles.track)}
>
<span {...stylex.props(styles.fill, styles.fillWidth(share === null ? 0 : share * 100))} />
</div>
<p {...stylex.props(styles.percent, shared.tabularNums)}>{percent}</p>
<p {...stylex.props(styles.note)}>of queries answered from cache</p>
<dl {...stylex.props(styles.split)}>
<div {...stylex.props(styles.splitItem)}>
<dt {...stylex.props(styles.splitLabel)}>cache hits</dt>
<dd {...stylex.props(styles.splitValue, shared.tabularNums)}>{formatCount(data.hits)}</dd>
</div>
<div {...stylex.props(styles.splitItem)}>
<dt {...stylex.props(styles.splitLabel)}>forwarded upstream</dt>
<dd {...stylex.props(styles.splitValue, shared.tabularNums)}>{formatCount(data.forwarded)}</dd>
</div>
<div {...stylex.props(styles.splitItem)}>
<dt {...stylex.props(styles.splitLabel)}>avg response</dt>
<dd {...stylex.props(styles.splitValue, shared.tabularNums)}>
{data.avg_response_time_us === null ? "—" : formatMicros(data.avg_response_time_us)}
</dd>
</div>
</dl>
</div>
</Card>
);
}