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
+24 -14
View File
@@ -9,7 +9,7 @@ import { health } from "@/lib/healthFixture";
import type { ConfigStatus, Health } from "@/lib/types";
const NAV_LABELS = ["Overview", "Activity", "Clients", "Diagnostics"];
const CONFIGURATION_LABELS = ["Protection", "Resolution", "System"];
const SYSTEM_LABELS = ["Protection", "Resolution", "System", "Diagnostics"];
/** The pages the redesign folded into the three configuration ones. */
const GONE_LABELS = ["Groups", "Blocklists", "Rules", "Local DNS", "Upstreams", "Settings"];
@@ -110,7 +110,7 @@ test("shell renders the overview route with all nav links", async () => {
const nav = screen.getByRole("navigation", { name: "Main" });
expect(nav).toBeTruthy();
for (const label of [...NAV_LABELS, ...CONFIGURATION_LABELS]) {
for (const label of [...NAV_LABELS, ...SYSTEM_LABELS]) {
expect(screen.getByRole("link", { name: label })).toBeTruthy();
}
for (const label of GONE_LABELS) {
@@ -127,16 +127,16 @@ test("main carries the ids the router scrolls and restores", async () => {
expect(main.getAttribute("data-scroll-restoration-id")).toBe("main");
});
test("the three configuration pages sit under a labelled group, after the rest", async () => {
test("the configuration pages and Diagnostics sit under the System group, after Monitoring", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
const group = screen.getByRole("list", { name: "Configuration" });
const group = screen.getByRole("list", { name: "System" });
expect(
within(group)
.getAllByRole("link")
.map((link) => link.textContent),
).toEqual(CONFIGURATION_LABELS);
).toEqual(SYSTEM_LABELS);
// The group is a section of Main, not a nav of its own.
const nav = screen.getByRole("navigation", { name: "Main" });
expect(nav.contains(group)).toBe(true);
@@ -156,14 +156,14 @@ test("under file authority the nav states the file and when it was loaded", asyn
const line = await screen.findByText(/^File-managed ·/);
expect(line.textContent).toBe(`File-managed · ${CONFIG_PATH} · loaded ${formatTime(RECONCILED_AT)}`);
const group = screen.getByRole("list", { name: "Configuration" });
const group = screen.getByRole("list", { name: "System" });
expect(group.parentElement?.contains(line)).toBe(true);
});
test("under database authority there is no authority line to read", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
await screen.findByRole("list", { name: "Configuration" });
await screen.findByRole("list", { name: "System" });
expect(screen.queryByText(/File-managed/)).toBeNull();
});
@@ -253,16 +253,26 @@ test("Log out sits in the sidebar on wide, and only in the header below it", asy
expect(within(drawer).queryByRole("button", { name: "Log out" })).toBeNull();
});
test("the header carries no protection display at all any more", async () => {
test("the header carries the protection state as a reading, never as a control", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
// Scoped to the header: "Protection" is a nav destination now, and that is
// not the status pill this test buried.
const header = within(document.querySelector("header") as HTMLElement);
for (const gone of [/^Protection/, /^Paused/]) {
expect(header.queryByRole("link", { name: gone })).toBeNull();
expect(header.queryByText(gone)).toBeNull();
}
await waitFor(() => expect(header.getByText("Protection active")).toBeTruthy());
expect(header.queryByRole("link", { name: /^Protection/ })).toBeNull();
expect(header.queryByRole("button", { name: /^Pause/ })).toBeNull();
});
test("the sidebar's foot states protection and uptime, and queries/min only on Overview", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
const aside = document.querySelector("aside") as HTMLElement;
const status = within(aside).getByLabelText("Status");
await waitFor(() => expect(within(status).getByText("Active")).toBeTruthy());
// 1 second of uptime from the fixture, aged by however long the test took.
expect(within(status).getByText("Uptime").nextElementSibling?.textContent).toMatch(/^\d+s$/);
// The fixture's overview body: no queries over a 24h window, stated as a rate.
await waitFor(() => expect(within(status).getByText("Queries/min")).toBeTruthy());
expect(within(status).getByText("Queries/min").nextElementSibling?.textContent).toBe("0.0");
});
test("Pause sits at the foot of the sidebar, above the version label", async () => {