Files
meshcore-analyzer/docs
efitenandClaude Opus 5 52b9474d7a feat(map): filter repeaters by region name (#1862) (#2022)
Fixes #1862

## What

Adds a **Region Scope** picker to the map controls: pick `#be` and the
map keeps the nodes that declare `#be` or were seen carrying `#be`
traffic. It combines with the #2006 scope-state filter and persists in
localStorage the same way. While a region is picked, a small "Region:
#be · reset" chip sits on the map itself, so the filter stays visible
when the controls panel is collapsed (the default on phones) and can be
cleared from there.

## API

`/api/nodes` and `/api/nodes/{pubkey}` gain two fields on repeater/room
rows:

- `declared_regions`: named regions from the node's newest
declared-regions answer, split by the same function the Scope Audit now
uses for `declaredRegions` (`splitDeclaredRegions`,
`cmd/server/scope_config_state.go`), so both pages list a repeater under
the same names. `[]` means it answered and named no region. Absent means
it never answered, other roles, no declared-regions source, or the
declared-regions lookup failed.
- `declared_regions_truncated`: present, and `true`, only when that
answer was flagged as truncated, so the list is partial. Never `false`:
the `nodes.configured_scope` source does not record truncation, so
absence does not mean the list is complete.

The observed side reuses `transported_scopes`. Documented in
`docs/api-spec.md` and the served OpenAPI spec.

### Why no `?hashRegion=` query parameter
The observed side lives in the in-memory store. Filtering it after the
SQL `LIMIT`/`OFFSET` would corrupt `total` and paging, and the map pages
through `/api/nodes`. Same reasoning as the Data path section of #2001.

## Map behaviour

- Filtering is client-side over the nodes `fetchAllNodes` already
loaded: no new request. One pass over the loaded nodes to build the
picker counts, one Set lookup per node per render. The marker filter is
`nodePassesMapFilters` (`public/map.js:219`) and the observer stand-down
`observerLayerShown` (`:212`), both exported and tested.
- The picker and hint count only nodes with a map position, the same
test the marker filter applies first, so a count never promises markers
the map cannot draw.
- The observer layer stands down while a region is picked, for the same
reason it does for the scope-state filter.
- The popup lists declared and observed regions separately. A truncated
declared answer carries the same `truncated` badge the Scope Audit
shows.
- Absence is not read as a finding: the hint under the picker says a
node left off the map is not proof it lacks the region.

## Tests

- Go: `node_declared_regions_api_test.go` covers `declared_regions` on
list and detail endpoints, the no-source case, agreement with
`/api/scope-audit`, `declared_regions_truncated` (truncated,
truncated-empty, complete, configured_scope-only, newer untruncated
answer, companion) and the OpenAPI schema.
- JS: `test-issue-1862-map-region-filter.js` (30 tests) covers the pure
pieces (evidence, counts, options, hint, popup rows,
`nodePassesMapFilters`, `observerLayerShown`) and, at page level, runs
the registered map page through `init()` and `loadNodes()` in a vm
sandbox: picker built from loaded nodes, markers filtered by a stored
region, observer pins standing down, popup rows, the change handler
persisting, and the chip showing, resetting and rendering its text as
text.
- Mutation-checked: removing the region check, the observer stand-down,
the picker build in `loadNodes`, the popup rows, the persist on change,
the chip reset, or the truncated flag (Go or JS) each fails a test.
`test-issue-2001-map-scope-state.js` still passes.
- `go test ./...` in `cmd/server` passes; `check-css-vars` and
`check-xss-sinks --diff` are clean.

## Staging validation
Build `c646310f`, Chrome, no console errors:
- Picking `#be`: chip "Region: #be · reset" at the top of the map, hint
"221 nodes with a map position have evidence for #be: 129 declare it,
191 seen carrying its traffic. Absence here is not proof: ...".
- Clicking reset: picker back to "All regions", stored choice cleared,
chip hidden.
- First version, same instance: `declared_regions` on 136 of 500
`/api/nodes` rows; returning to "All regions" blocked the main thread
5.4 s against 3.2 s for the existing Status filter returning to "All".
The review measured the region filter's own added work at about 0.08 ms
per render plus popup rows that are already built for every marker, so
most of that time is the existing full re-render.

## Not verified
- The chip placement at phone width, and on narrow desktop widths where
it may sit under the expanded controls panel.
- The truncated badge with real truncated answers (staging has none
today).
- `test-map-clustering.js` has one failing test on `upstream/master`
too; untouched here.
- Marker badges from the original issue body are not implemented; the
popup rows are the per-node display.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 21:09:51 +02:00
..