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
+67 -44
View File
@@ -9,30 +9,31 @@ import { healthQuery, versionQuery } from "@/lib/queries";
import PauseControl from "@/features/pause/PauseControl";
import { diagnosticsBadge } from "./diagnosticsBadge";
import ConfigStatusNotices from "./ConfigStatusNotices";
import SidebarStatus, { ProtectionDot } from "./SidebarStatus";
import AuthorityLine from "@/features/configuration/AuthorityLine";
import { styles as shared } from "@/ui/styles";
import { colors, metrics } from "@/ui/tokens.stylex";
import { colors, layers, metrics } from "@/ui/tokens.stylex";
/** The one breakpoint the shell has: below it the sidebar becomes a drawer. */
const WIDE = "@media (min-width: 768px)";
const DARK = "@media (prefers-color-scheme: dark)";
const WIDE = "@media (min-width: 801px)";
/**
* The four operational surfaces, then the configuration group. Configuration
* is a labelled group rather than a collapsible tree: three items do not earn
* a disclosure, and a tree would hide the authority line under it.
* Two labelled groups (ui-visual-redesign.md): MONITORING is what the resolver
* is doing, SYSTEM is what it is told to do and how the box is faring. The
* three configuration pages sit flat in SYSTEM — "Configuration" is not one
* page — with the authority line under them.
*/
const NAV_ITEMS = [
const MONITORING_ITEMS = [
{ to: "/overview", label: "Overview" },
{ to: "/activity", label: "Activity" },
{ to: "/clients", label: "Clients" },
{ to: "/diagnostics", label: "Diagnostics" },
] as const;
const CONFIGURATION_ITEMS = [
const SYSTEM_ITEMS = [
{ to: "/configuration/protection", label: "Protection" },
{ to: "/configuration/resolution", label: "Resolution" },
{ to: "/configuration/system", label: "System" },
{ to: "/diagnostics", label: "Diagnostics" },
] as const;
const styles = stylex.create({
@@ -45,7 +46,8 @@ const styles = stylex.create({
display: "flex",
alignItems: "center",
gap: "0.5rem",
borderRadius: "0.25rem",
minHeight: metrics.hitTarget,
borderRadius: metrics.radius,
paddingInline: "0.75rem",
paddingBlock: "0.375rem",
// Carried by every item, active or not: a weight that changes on
@@ -57,16 +59,17 @@ const styles = stylex.create({
flex: 1,
},
navGroup: {
marginTop: "1rem",
marginTop: "1.125rem",
},
navGroupLabel: {
display: "block",
paddingInline: "0.75rem",
paddingBlock: "0.25rem",
fontSize: "0.75rem",
paddingBottom: "0.375rem",
fontSize: "0.6875rem",
lineHeight: "1rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.05em",
letterSpacing: "0.09em",
color: colors.textMuted,
},
/**
@@ -90,12 +93,12 @@ const styles = stylex.create({
/** The control sits with the footer, not in the scrolling nav list above it. */
sidebarFooter: {
paddingInline: "1rem",
paddingTop: "0.75rem",
paddingBlock: "0.75rem",
},
/** The current page reads as a filled chip, heavier than the hover fill. */
/** The current page is a soft accent-tinted block, in the accent's own colour. */
navActive: {
backgroundColor: { default: "oklch(92% 0.004 286.32)", [DARK]: "oklch(27.4% 0.006 286.033)" },
color: colors.text,
backgroundColor: colors.primarySurface,
color: colors.primaryOnSurface,
},
navIdle: {
backgroundColor: { default: "transparent", ":hover": colors.surfaceHover },
@@ -140,16 +143,18 @@ const styles = stylex.create({
flexDirection: { default: null, [WIDE]: "column" },
minHeight: { default: null, [WIDE]: 0 },
overflow: { default: null, [WIDE]: "hidden" },
backgroundColor: colors.surfaceRaised,
borderRightWidth: 1,
borderRightStyle: "solid",
borderRightColor: colors.border,
},
brand: {
paddingInline: "1rem",
paddingBlock: "1rem",
fontSize: "1.125rem",
lineHeight: "1.75rem",
fontWeight: 600,
paddingBlock: "1.25rem",
fontSize: "1rem",
lineHeight: "1.5rem",
fontWeight: 650,
letterSpacing: "-0.015em",
},
sidebarNav: {
flex: 1,
@@ -163,11 +168,19 @@ const styles = stylex.create({
minWidth: { default: null, [WIDE]: 0 },
flexDirection: "column",
},
/** Narrow only: on WIDE the sidebar carries everything this row held. */
/**
* Narrow only: on WIDE the sidebar carries everything this row held. Sticky,
* so the brand, the protection state and the menu stay in reach down a long
* page.
*/
header: {
display: { default: "flex", [WIDE]: "none" },
position: "sticky",
top: 0,
zIndex: layers.tooltip,
alignItems: "center",
gap: "0.75rem",
backgroundColor: colors.surfaceRaised,
borderBottomWidth: 1,
borderBottomStyle: "solid",
borderBottomColor: colors.border,
@@ -178,9 +191,10 @@ const styles = stylex.create({
display: { default: null, [WIDE]: "none" },
},
narrowBrand: {
fontSize: "1.125rem",
lineHeight: "1.75rem",
fontWeight: 600,
fontSize: "1rem",
lineHeight: "1.5rem",
fontWeight: 650,
letterSpacing: "-0.015em",
display: { default: null, [WIDE]: "none" },
},
headerRight: {
@@ -245,31 +259,37 @@ function NavItem({
function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const health = useQuery(healthQuery());
const badge = diagnosticsBadge(health.data, health.isError);
const groupHeadingId = useId();
const monitoringId = useId();
const systemId = useId();
return (
<>
<ul {...stylex.props(styles.navList)}>
{NAV_ITEMS.map((item) => (
<NavItem
key={item.to}
to={item.to}
label={item.label}
onNavigate={onNavigate}
badge={item.to === "/diagnostics" ? badge : undefined}
/>
))}
</ul>
<div {...stylex.props(styles.navGroup)}>
<div>
{/* A span, not a heading: the sidebar label is not a section of the
page, and an h2 here lands in the middle of the page's own outline. */}
<span id={groupHeadingId} {...stylex.props(styles.navGroupLabel)}>
Configuration
<span id={monitoringId} {...stylex.props(styles.navGroupLabel)}>
Monitoring
</span>
<ul aria-labelledby={groupHeadingId} {...stylex.props(styles.navList)}>
{CONFIGURATION_ITEMS.map((item) => (
<ul aria-labelledby={monitoringId} {...stylex.props(styles.navList)}>
{MONITORING_ITEMS.map((item) => (
<NavItem key={item.to} to={item.to} label={item.label} onNavigate={onNavigate} />
))}
</ul>
</div>
<div {...stylex.props(styles.navGroup)}>
<span id={systemId} {...stylex.props(styles.navGroupLabel)}>
System
</span>
<ul aria-labelledby={systemId} {...stylex.props(styles.navList)}>
{SYSTEM_ITEMS.map((item) => (
<NavItem
key={item.to}
to={item.to}
label={item.label}
onNavigate={onNavigate}
badge={item.to === "/diagnostics" ? badge : undefined}
/>
))}
</ul>
<AuthorityLine />
</div>
</>
@@ -279,7 +299,8 @@ function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
/**
* The sidebar's foot, in both renderings. Pause is a runtime action on the whole
* resolver rather than on the page in front of the reader, which is why it sits
* with the version label instead of in the header of every route.
* with the status block and the version label instead of in the header of every
* route.
*/
function SidebarFooter() {
return (
@@ -287,6 +308,7 @@ function SidebarFooter() {
<div {...stylex.props(styles.sidebarFooter)}>
<PauseControl />
</div>
<SidebarStatus />
<VersionFooter />
</>
);
@@ -353,6 +375,7 @@ export default function AppShell() {
</Button>
<span {...stylex.props(styles.narrowBrand)}>nxdns</span>
<div {...stylex.props(styles.headerRight)}>
<ProtectionDot />
<LogoutButton />
</div>
</header>