admin: overview redesign, device scope, one formatting contract (milestone 39)
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s

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 centred beside the point, a stacked client chart in eight distinct hues plus one Other band that is always a series, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown. The count axis grows its margin with the widest grouped tick and draws whole-number ticks only.

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. The route reduces a pasted IPv6 scope to the RFC 5952 spelling the logger stores, mapped addresses included, and drops anything that is not an address. A failed device list says so under the selector with a retry.

All measured quantities go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. Identifiers, configured values and preset labels render as written; the module header states that scope. 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 a one-line description on every panel, the settings form sections on the same card with a floated legend, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), keyboard-focusable table scroll wrappers, 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 reviewed sessions B to D over five rounds (thirty-three findings fixed, thirteen rejected as non-quantities); the owner skipped a sixth round.

Claude-Session: https://claude.ai/code/session_01VTgx3a1zz1R78o4K55kkwR
This commit is contained in:
2026-09-07 23:11:50 +02:00
parent e656670dd4
commit 85b8be50a0
77 changed files with 2869 additions and 1163 deletions
+160
View File
@@ -0,0 +1,160 @@
/**
* The compact status block at the sidebar's foot (milestone 39): whether
* filtering is in force, how fast queries are arriving, and how long the
* process has been up. Three readings the operator glances at from any page,
* each from data the shell already holds — no request of its own.
*
* Queries/min derives from the Overview body the page in front of the reader
* is drawing, so it appears only while that page is open and its window has
* landed; on every other page the row is absent rather than stale.
*/
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useMatch } from "@tanstack/react-router";
import * as stylex from "@stylexjs/stylex";
import { formatDuration, formatRate } from "@/lib/format";
import { overviewQuery, versionQuery } from "@/lib/queries";
import { useProtection } from "@/features/pause/protection";
import { DEFAULT_PERIOD } from "@/features/overview/period";
import { styles as shared } from "@/ui/styles";
import { colors, metrics } from "@/ui/tokens.stylex";
const styles = stylex.create({
block: {
display: "flex",
flexDirection: "column",
gap: "0.4375rem",
marginInline: "1rem",
marginBottom: "0.75rem",
borderRadius: metrics.radius,
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.border,
backgroundColor: colors.surface,
paddingInline: "0.75rem",
paddingBlock: "0.875rem",
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
line: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "0.625rem",
margin: 0,
},
key: {
display: "inline-flex",
alignItems: "center",
gap: "0.4375rem",
},
value: {
fontWeight: 550,
color: colors.text,
},
dot: {
flexShrink: 0,
width: "7px",
height: "7px",
borderRadius: "999px",
},
dotActive: { backgroundColor: colors.chartGreen },
dotPaused: { backgroundColor: colors.warnBorderStrong },
dotUnavailable: { backgroundColor: colors.chartRed },
dotUnknown: { backgroundColor: colors.borderStrong },
/** The narrow header's rendering: the dot and the state alone, inline. */
inline: {
display: "inline-flex",
alignItems: "center",
gap: "0.4375rem",
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textSecondary,
},
});
const PROTECTION_TEXT = {
active: "Active",
paused: "Paused",
unavailable: "Unavailable",
unknown: "Unknown",
} as const;
const DOT_STYLE = {
active: styles.dotActive,
paused: styles.dotPaused,
unavailable: styles.dotUnavailable,
unknown: styles.dotUnknown,
} as const;
/** The dot and the word, for the narrow header where there is room for nothing more. */
export function ProtectionDot() {
const { state } = useProtection();
return (
<span {...stylex.props(styles.inline)}>
<span aria-hidden="true" {...stylex.props(styles.dot, DOT_STYLE[state])} />
<span>Protection {PROTECTION_TEXT[state].toLowerCase()}</span>
</span>
);
}
/**
* Uptime as of now, not as of the fetch: the version answer is a minute old at
* most, so the seconds it reported are aged by the time since it landed. A
* restart shows within that minute. Ticks every second, because the first
* minute reads in seconds.
*/
function useUptimeSeconds(): number | null {
const version = useQuery(versionQuery());
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
if (version.data === undefined) return null;
return version.data.uptime_seconds + Math.max(0, now - version.dataUpdatedAt) / 1000;
}
function useQueriesPerMinute(): number | null {
const match = useMatch({ from: "/shell/overview", shouldThrow: false });
const period = match?.search.period ?? DEFAULT_PERIOD;
// Reads the body the page fetched; never fetches one of its own. A failed
// refetch leaves the last body in the cache, and the page is then showing
// its error, not that body: the row goes with it.
const overview = useQuery({ ...overviewQuery(period, match?.search.client), enabled: false });
if (match === undefined || overview.isError || overview.data === undefined) return null;
if (overview.data.period !== period) return null;
const minutes = (overview.data.until - overview.data.since) / 60;
return minutes > 0 ? overview.data.totals.queries / minutes : null;
}
export default function SidebarStatus() {
const { state } = useProtection();
const rate = useQueriesPerMinute();
const uptime = useUptimeSeconds();
return (
<dl aria-label="Status" {...stylex.props(styles.block)}>
<div {...stylex.props(styles.line)}>
<dt {...stylex.props(styles.key)}>
<span aria-hidden="true" {...stylex.props(styles.dot, DOT_STYLE[state])} />
Protection
</dt>
<dd {...stylex.props(styles.value)}>{PROTECTION_TEXT[state]}</dd>
</div>
{rate !== null && (
<div {...stylex.props(styles.line)}>
<dt>Queries/min</dt>
<dd {...stylex.props(styles.value, shared.tabularNums)}>{formatRate(rate)}</dd>
</div>
)}
<div {...stylex.props(styles.line)}>
<dt>Uptime</dt>
<dd {...stylex.props(styles.value, shared.tabularNums)}>
{uptime === null ? "—" : formatDuration(uptime)}
</dd>
</div>
</dl>
);
}