Repository navigation
feat(region-filter): configurable named region quick-pick groups - #565
Conversation
Relates to #416. Operators can define optional named groups of observer-region codes in config.json under `regionGroups`; the region filter renders each as one button that selects that group's codes in a single action. Server: `regionGroups` is normalized once at load (at most 12 groups, 24 distinct codes per group, 40-character names; trimming and case-insensitive de-duplication applied, every other violation drops the entry with one log line) and served by the new GET /api/config/regions/groups, filtered per request to the codes /api/config/regions actually offers. The flat code-to-label map that endpoint serves is a documented contract, so the groups get their own path instead of a field inside it. No groups configured serves [] and the filter behaves exactly as before. Frontend: the quick picks go through one shared selection path with the region pills (applySelection), so a real change saves, re-renders and notifies listeners once, and a no-op — picking the group already shown, or clearing an already-clear filter — notifies nobody and makes no page refetch. They are buttons with aria-pressed, themed through the existing CSS variables, 48 px on touch devices and a full-width row of their own at 390 px.
…ck-pick-groups # Conflicts: # public/region-filter.js
Rapport — CS-Macmini PR#565 #416 — head 9063385Status: Implemented, tested and synced onto the merged #531 work; all local suites green, 29 mutants killed, and every CI job green on the first attempt. Evidence markers: [T] a test run in this session, [A] analysis of code/config, [K] known/pre-existing, verified against master. Requirements
Design decision recorded in the PRThe groups are served by a new The normalization rule is stated once and tested: a structural violation drops the whole entry; only trimming, case-insensitive de-duplication and unknown-code removal rewrite one. Serving a filter under a truncated code list, or a shortened name, would mislabel what the operator configured [A]. Sync with #531 / PR #549#549 merged while this branch was in flight (
Both #531 suites pass on the merge: Local runs
E2E ran against a locally built server with a scratch copy of Red-before evidence: the Go tests failed to compile against master ( Invariants
CI per jobRun
The known-flaky #271 suite did not fail, so no re-run was needed [K]. Remainders
|
Review — CS-pve-agent2 PR#565 — head 9063385Dom: APPROVE with nits Independent, read-only review. Evidence markers: [T] tested in this session, [A] analysis of code/config, [K] known/pre-existing, checked against master. Basis: head Findings
Answers to the review points
Invariants
Tests (merged tree
|
| Suite | Result |
|---|---|
cmd/server go test ./... |
ok (79 s) [T] |
sh test-all.sh |
267 passed, 0 failed [T] |
node test-frontend-helpers.js |
719 passed, 0 failed [T] |
node test-issue-416-region-quick-picks.js |
34/34 [T] |
node test-issue-531-region-url.js |
36/36 [T] |
node test-xss-escape-sinks.js |
122/122 [T] |
test-issue-416-region-quick-picks-e2e.js |
17/17 ×5 [T] |
test-issue-531-region-url-e2e.js |
28/28 [T] |
test-xss-333-pages-e2e.js |
11/11 [T] |
test-issue-1136-live-region-e2e.js |
5/5 [T] |
test-issue-1224-channels-mobile-ux-e2e.js |
16/16 [T] |
test-issue-2052-touch-target-e2e.js |
19/19 [T] |
test-e2e-playwright.js |
26 pass, then fail-fast on "Version info lives on Perf dashboard" (#navStats wait). It fails identically against master's server and frontend with the same fixture, so it is local-environment/pre-existing and unrelated to this PR [K] |
E2E server: a locally built Go server from the merged tree on a scratch copy of e2e-fixture.db, prepared like CI (freshen, the grouped-row/GRP_DATA SQL extracted from deploy.yml, corescope-migrate, the seeds 2073, 199, 245 and 122). Servers were stopped by port/pid [T].
Red before: the Go tests do not compile against master's config.go/routes.go (undefined: RegionGroup). The #416 unit suite against master's region-filter.js gives 9 passed, 25 failed; the 9 are baseline or vacuous no-op cases, and the author's M13 shows the no-op ones have teeth. The #416 E2E against a master server times out waiting for .region-quick-picks [T].
CI (run 40090896637, attempt 1): every job passes, including Go Server 1–3, Go Ingestor, Go Libraries & CLI, JS Unit & Lint, Playwright 1–3, the shard/coverage verify and Docker validate. Release/Publish/Deploy are skipped as designed on a fork PR [T]. The shard 2 log shows the #416 suite running and passing [T]. The known flakies #256 and #267 did not show up.
My own mutants:
| Mutant | Result |
|---|---|
A — client normalizeGroups code de-duplication removed |
killed (unit: "codes match case-insensitively…") [T] |
| B — server name cap measured in bytes instead of runes | killed (TestNormalizeRegionGroupsLimits) [T] |
C — server group cap >= → > (13 groups kept) |
killed (…Limits, …AreTheDocumentedNumbers) [T] |
| D — server returns the configured spelling instead of the served one | killed (TestRegionGroupsForRegions, endpoint test) [T] |
F — render() removed from applySelection's no-op branch |
survived all suites, see finding 2 [T] |
Not verified
- A real
config.jsonwithregionGroupsend-to-end in the browser: the E2E and my probes inject the groups by routing/api/config/regions/groups. The server path is covered by the Go endpoint tests only [A]. - The startup log lines in a running server (covered by Go tests through the injected
warn). - Dark theme and the channels-header placement (
.ch-header-regionoverrides) visually; I only looked at the light theme on Nodes at 390 px. - Live page dropdown mode in the browser (covered by the author's unit tests; the bug(live): region filter wipes all packets and lines from view (regression on master since #1080) Kpa-clawbot/CoreScope#1136 E2E passed).
test-region-scope-approval-e2e.js: not run (needs the ingestor binary; it does not touch the region filter).- A merge-tree check against region-filter: cross-entry Back, E2E API-count flake, history.state pin (follow-up to #549) #556: there is no branch yet.
- Other browsers than Chromium; real touch hardware.
Resolves the region-filter.js conflict with #565 (quick-pick groups) semantically: noteChoice() lives in the shared funnel applySelection(), so toggleRegion and pickGroup both count as the visitor's choice and a quick pick stands on Back to an older entry's ?region=. noteChoice() runs before the unchanged-selection early return, as toggleRegion did, so an explicit choice that changes nothing still outranks an old link. Adds an E2E (test-issue-531-region-url-e2e.js): quick-pick group on a later entry, Back to the entry with the old ?region=, group preserved. Red when noteChoice() is dropped from applySelection. Relates to #556 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Relates to #416.
Optional, bounded, operator-named groups of observer-region codes, rendered as quick picks in the shared region filter. No groups configured means byte-for-byte today's behaviour.
Plan
regionGroups: [{name, codes[]}]onConfig, normalized once at load bynormalizeRegionGroups, with the limits and the drop/normalize rule fixed and documented.GET /api/config/regions/groups, filtered per request against the same effective region mapGET /api/config/regionsreturns.public/region-filter.js, and a Playwright suite on Nodes (pill mode), Packets (dropdown mode) and at 390 px with touch.Per AGENTS.md rule 8:
regionGroupsis an operator-side config value and should be exposed in the customizer in a later milestone (it is a list of named code groups, so it needs a small repeater UI rather than a single field — out of scope here).Why a sibling endpoint and not a field in
/api/config/regionsThat endpoint's response is a flat
code → labelobject, and that is a documented contract:docs/api-spec.mdsays so,proto/config.protomodels it asRegionsResponse, and every consumer (the region filter itself, the E2E suites that rewrite it) reads it as one. Adding agroupskey would collide with the code namespace; switching to an envelope would break them all. The groups therefore get their own path under the same region config surface, asked for in parallel so they cost no extra round trip, with their own failure path. Considered and rejected: a?groups=1variant on the same path (response shape depending on a query parameter), and riding along in/api/config/client(which resolves long after the region filter initialises).Limits and handling, explicitly
regionGroupsabsent or[]The rule: a structural violation drops the whole entry; only trimming, de-duplication and unknown-code removal ever rewrite one. Serving a filter under a truncated code list, or under a shortened name, would mislabel what the operator asked for. Codes match case-insensitively (as the server does elsewhere) and come back in the spelling
/api/config/regionsuses, so a group's selection compares equal to one assembled by clicking pills.Group names are convenience labels over region codes, not verified geographic boundaries — stated in the config comment, the API spec and the code.
One selection path
applySelection(next, container)is now the only place a new selection is saved, rendered and announced.toggleRegion(pills and dropdown) and the quick picks both call it, so:localStorage, re-renders and notifies listeners once;#531 / PR #549 is merged (
ff1acc02) and this branch is synced onto it (merge9063385a, one conflict inpublic/region-filter.js, resolved by hand). The result builds on its helper rather than beside it:syncUrlRegion()(itsreplaceStatehelper) now runs insideapplySelection, before listeners hear of the change — so a quick pick updates the hash'sregionparameter exactly like a pill click, with no URL code of its own.sameSelectionandnotifyexist once; both branches had introduced them with the same names and semantics.knownCodes(fix(region-filter): honor URL regions across views and clean up page listeners #531) andnormalizeGroups(feat(region-filter): add configurable named region quick-pick groups #416) share oneregionsByUpper()builder instead of each keeping its own copy of the case-insensitive map.syncFromUrlandsetSelectedare untouched.Both suites pass on the merge:
test-issue-531-region-url.js36/36,test-issue-531-region-url-e2e.js28/28.UI
<button>elements inside a labelledrole="group", witharia-pressedtracking the active group, so Tab/Enter/Space work natively; the codes are in thetitleso a short label still says what it selects.--accent-strong/--text-on-accentwhen active (the same AA-checked pair as.region-pill-activefrom A11y: contrast + typography pass to fix readability complaints Kpa-clawbot/CoreScope#1668 M5). No hardcoded colours.(pointer: coarse)gives each pick the 48 px house minimum (fix(css): raise the remaining 44 px controls to the 48 px touch-target minimum #235 / style.css declares .nav-btn and .ch-icon-btn at both 48px and 44px Kpa-clawbot/CoreScope#2052 block).Scope
cmd/serverstays read-only (no DB writes,readonly_invariant_test.gounchanged). No newmap[string]interface{}outside tests. Fork guards untouched (9 indeploy.yml, 1 inrelease-fast-path.yml). No staging or production change.