diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a96a5..8288be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to nxdns are recorded here. The format follows [Keep a Chang Sections are written by hand. Nothing here is generated from commit messages: the point of the file is to say what changed for an operator, which a commit subject rarely does. +## [0.0.15] - 2026-08-31 + +The Activity page's history filters become a toolbar you can actually use. Filters apply as you set them, the client field stops asking you to remember IP addresses, and the whole admin picks up one coherent icon set. + +### Changed + +- **History filters apply live from a toolbar.** The form-and-Apply-button row is gone. Domain text filters as you type (debounced), result and time are one-click controls, and a custom date range applies as one change. Every filter state is a bookmarkable URL. +- **The client filter is a picker, not a text field.** It lists the clients the server knows — named, sorted, multi-select — instead of asking for an exact address. Selected clients show as removable chips (first three, then a count), and a query can filter on up to 32 clients at once; the API accepts the same list. +- **One icon set.** Interface glyphs (dropdown carets, checkbox ticks, the search magnifier, status marks, back arrows) are now Phosphor icons instead of a mix of text characters and hand-drawn shapes. +- **The admin bundle budget rises from 800,000 to 900,000 bytes.** The client picker, the accessible menu and dialog primitives behind it, and the Phosphor icon components are the arrivals that spend it; the built assets sit at about 840,000 bytes. + +### Added + +- **Accessibility pass over the admin.** Focus-visible rings on every control, labels wired to their inputs, keyboard-reachable menus and dialogs, and visible text where information previously lived only in hover titles. + ## [0.0.14] - 2026-08-28 Schema changes stop costing you your query history. querylog.db is now version-stamped and migrated in place; the server refuses to start rather than ever reset a healthy file, and the release tooling refuses to ship a schema change that is neither migratable nor explicitly disclosed with recovery steps. Three releases (0.0.6, 0.0.9, 0.0.12) each discarded the log on upgrade; this ends that. diff --git a/admin/package-lock.json b/admin/package-lock.json index 75029fb..89288df 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -8,7 +8,7 @@ "name": "nxdns-admin", "version": "0.0.0", "dependencies": { - "@phosphor-icons/react": "^2.1.10", + "@phosphor-icons/react": "2.1.10", "@stylexjs/stylex": "0.19.0", "@tanstack/react-query": "5.101.4", "@tanstack/react-router": "1.170.18", diff --git a/admin/package.json b/admin/package.json index 6757e25..a8729b8 100644 --- a/admin/package.json +++ b/admin/package.json @@ -25,7 +25,7 @@ "trailingComma": "all" }, "dependencies": { - "@phosphor-icons/react": "^2.1.10", + "@phosphor-icons/react": "2.1.10", "@stylexjs/stylex": "0.19.0", "@tanstack/react-query": "5.101.4", "@tanstack/react-router": "1.170.18", diff --git a/admin/src/features/activity/ActivityDetailPage.test.tsx b/admin/src/features/activity/ActivityDetailPage.test.tsx index 5b7b862..cca4c1f 100644 --- a/admin/src/features/activity/ActivityDetailPage.test.tsx +++ b/admin/src/features/activity/ActivityDetailPage.test.tsx @@ -252,7 +252,7 @@ test("the back link restores the investigation the reader came from", async () = renderDetail(19, "?mode=history&domain=shop&since=1600000000&blocked=true"); await screen.findByRole("heading", { name: "shop.example" }); - expect(hrefSearch(screen.getByRole("link", { name: "← Activity" }))).toEqual({ + expect(hrefSearch(within(screen.getByRole("main")).getByRole("link", { name: "Activity" }))).toEqual({ mode: "history", domain: "shop", since: "1600000000", @@ -307,7 +307,7 @@ test("a row retention has pruned explains the 404 and keeps the way back to the await screen.findByRole("alert"); expect(screen.getByText(/no such query/)).toBeTruthy(); - expect(hrefSearch(screen.getByRole("link", { name: "← Activity" }))).toEqual({ + expect(hrefSearch(within(screen.getByRole("main")).getByRole("link", { name: "Activity" }))).toEqual({ mode: "history", domain: "gone", }); diff --git a/admin/src/features/activity/ActivityDetailPage.tsx b/admin/src/features/activity/ActivityDetailPage.tsx index 817d5dd..241eb4c 100644 --- a/admin/src/features/activity/ActivityDetailPage.tsx +++ b/admin/src/features/activity/ActivityDetailPage.tsx @@ -1,3 +1,4 @@ +import { ArrowLeft } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import { Link, useParams, useSearch } from "@tanstack/react-router"; import * as stylex from "@stylexjs/stylex"; @@ -12,11 +13,17 @@ import type { ActivitySearch } from "./search"; const styles = stylex.create({ back: { + display: "inline-flex", + alignItems: "center", + gap: "0.25rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.primaryOnSurface, textDecorationLine: "none", }, + backIcon: { + display: "inline-flex", + }, loading: { marginTop: "1rem", color: colors.textMuted, @@ -32,7 +39,10 @@ const styles = stylex.create({ function BackLink({ origin }: { origin: ActivitySearch }) { return ( - ← Activity + + Activity ); } diff --git a/admin/src/features/activity/ActivityFilters.test.tsx b/admin/src/features/activity/ActivityFilters.test.tsx index e7c74d6..66e2877 100644 --- a/admin/src/features/activity/ActivityFilters.test.tsx +++ b/admin/src/features/activity/ActivityFilters.test.tsx @@ -157,7 +157,7 @@ function clientChips(): string[] { return within(screen.getByRole("group", { name: "Clients" })) .getAllByRole("button") .slice(1) - .map((chip) => chip.textContent?.replace("\u00d7", "") ?? ""); + .map((chip) => chip.textContent ?? ""); } /** @@ -368,7 +368,7 @@ test("the domain field is search-shaped, unspellchecked, and labelled without a expect(domain.getAttribute("autocomplete")).toBe("off"); expect(domain.getAttribute("placeholder")).toBe("Filter domains…"); // The magnifier is decoration over the field, never a second thing to read. - expect(document.querySelector("svg[aria-hidden='true']")).toBeTruthy(); + expect(domain.parentElement?.querySelector("[aria-hidden='true'] svg")).toBeTruthy(); }); test("a bound change does not reset the domain draft that is still being typed", () => { @@ -553,7 +553,7 @@ test("the menu ticks the clients the url names, not the ones last picked here", const ticked = screen .getAllByRole("menuitemcheckbox") .filter((item) => item.getAttribute("aria-checked") === "true"); - expect(ticked.map((item) => item.textContent?.replace("✓", ""))).toEqual(["laptop.lan — 192.0.2.11"]); + expect(ticked.map((item) => item.textContent)).toEqual(["laptop.lan — 192.0.2.11"]); }); test("a rename moves the label and leaves the filter on the address", async () => { diff --git a/admin/src/features/activity/ActivityFilters.tsx b/admin/src/features/activity/ActivityFilters.tsx index 9e00932..2c520f9 100644 --- a/admin/src/features/activity/ActivityFilters.tsx +++ b/admin/src/features/activity/ActivityFilters.tsx @@ -29,6 +29,7 @@ * would promise filtering that is not happening. */ +import { MagnifyingGlass } from "@phosphor-icons/react"; import { useCallback, useEffect, useState, type FormEvent, type KeyboardEvent } from "react"; import * as stylex from "@stylexjs/stylex"; import { Button, Menu, MenuItem, MenuTrigger, Popover, Radio, RadioGroup } from "react-aria-components"; @@ -76,6 +77,7 @@ const styles = stylex.create({ display: "flex", }, searchIcon: { + display: "inline-flex", position: "absolute", insetInlineStart: "0.5rem", top: "50%", @@ -502,19 +504,9 @@ export default function ActivityFilters({ applied, onApply, onClear }: Props) {