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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user