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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user