map: one marker per site, node_ref labels, measured coverage only - #358
Conversation
…bump) Bumps retina-analytics to the evidence-only polygon: under FOV_MODE=off the published empirical_coverage.polygon is what the node has been SEEN to detect, with the declared beam wedge no longer clipping it away. test_public_location's fixture spread 30 calibration points along a single line, which opened one bin — enough for the old beam-clipped sector, not an area under the new rule. Six adjacent bearings give it a lobe with both a measured arc and the RX apex its displacement assertions read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every public payload that names a node named it by node_id: the map's node popups, the aircraft detail panel, the illuminator popup. A node_id comes off the board and is the name its owner chose — a run of them is a naming convention — so printing it hands a stranger a correlation key nobody agreed to publish. core.nodes.Node already carries the intended public handle, node_ref, but only nodes that registered through /v1/nodes have a row: the blah2 bridge's receivers and everything on the plain TCP protocol have none, which on the test deployment is all seven live nodes. services/node_ref.public_node_ref returns the stored ref when there is one and otherwise derives HMAC-SHA256(fuzz salt, "node_ref|" + node_id) rendered in mint_node_ref's own base36 alphabet, to the same 15-char shape — so registered and legacy nodes are not tellable apart by form. The salt is already this deployment's anonymity key, secret and stable across restarts, and the domain prefix keeps this frame clear of the location HMAC's. There is no fallback to the raw id: a lookup that fails derives, because "publish the id when the database is down" makes the leak conditional on exactly the moment nobody is watching. Wired into both /api/radar/analytics variants, /api/radar/nodes and the fresh per-node analytics route. No node API route or model changes, so contracts/nodes-v1.openapi.yaml does not move. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three display bugs, one theme: the map was showing configuration as if it were measurement, and machine names as if they were public handles. One marker per receive SITE. Co-located receivers are published at exactly equal coordinates so the pair leaks one sample of its position instead of two; the map drew a marker and an uncertainty disc per node, so the two glyphs stacked (only the top one could be clicked) and the doubled fill made a shared site read as better located than a lone node. A site now carries one disc, a count badge on the glyph, and a popup block per node with its own Show/Hide control — a marker click at a shared site has no single node to mean. Single-node sites behave as before. Nodes are named by node_ref. Node popups, the detail panel's claiming node and detecting-node list, and the illuminator popup all printed the raw node_id, which is the name the operator gave the machine. They print nodeLabel() now; node_id stays the join key throughout. No theoretical beam is drawn anywhere. The dashed Yagi sector was a declared azimuth and width — configuration nobody surveyed — drawn as a detection area, and the node with the LEAST evidence got the boldest wedge (full-strength cone when it had no measured polygon at all). The coverage layer, the selected-node overlay and the contributing-node highlights now draw the empirical polygon or nothing, node popups quote measured reach instead of declared range and beam, and the gap diagnostic tests membership with point-in-polygon against the measured coverage rather than isInBeam against the declared wedge. geo.ts keeps yagiSectorPositions and its tests; nothing draws with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pipeline.md §7 gains what FOV_MODE=off actually publishes now (an evidence-only polygon: own-count open bins, ≤2-bin holes bridged, no theoretical clip) and the public node handle every payload carries. arc-display.md said selecting an arc track draws the detecting node's beam wedge; it draws the measured polygon, or nothing. architecture.md gets a pointer to services/node_ref.py beside the other services. Also corrects CALIBRATION_SCHEMA in the same paragraph: 6 since the bin_pos_ts bump, not 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With the fuzz off, public_node_summaries returns the analytics manager's own cached summary dicts; adding a key to those grew the manager's cache from the publisher. Same payload, new dicts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Carries the site markers, node_ref labels and measured-only coverage over main's map palette/theme refactor: the site icon is built from the active palette like nodeIcon() now is, the coverage layer takes its colour from the palette, and the toolbar's regrouped overflow menu keeps the renamed "Coverage gaps" button. Submodule bumped to retina-analytics feat/evidence-only-coverage merged with its main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
main's identity guard scans every tracked file for real node ids; the module's own explanation named one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…ixtures main's identity guard bans real node ids in tracked files; the fixtures named two co-located real nodes. Same scenarios, invented ids. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…f snapshot on a failed read, memoise site grouping - /api/radar/analytics built an owner's private node summary fresh without the node_ref the cached listing carries, so the owner's own node was the one unnamed marker on their map. - node_ref._snapshot cleared the registered-ref cache before the fallible read; a transient database failure renamed every registered node to a derived ref until the next good refresh. Fetch first, swap second. - NodeMarkersLayer re-grouped the whole fleet by site on every selection change; the grouping is now memoised on the node list alone. - test_publication's owner-copy check compared uptime_s byte-for-byte across two summaries taken at different instants; compared with a tolerance now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 17m 18s —— View job Claude finished @jehanazad's task in 6m — code reviewTodo list
SummaryI re-ran a full review of this sync against Findings worth addressing1. "status": info.get("status"),
"node_ref": public_node_ref(nid),
"name": info.get("config", {}).get("name", nid),This is the exact block the PR edited to add 2. A shared synthetic site never shows the count badge — if (site.isSynth) {
return (
<React.Fragment key={`site-${site.key}`}>
{disc}
<CircleMarker ... radius={5} ...>{popup}</CircleMarker>
</React.Fragment>
);
}
Minor / optional
Discarded during verificationTwo findings from the initial pass didn't hold up and I'm dropping them rather than reporting noise:
No other issues found in the rest of the diff ( |
Resolves 21 conflicting files against 53 commits on main, including the two pieces already split out of this PR (#351 identity scrub, #355 receiver-geometry withholding), #358 site markers / empirical coverage, #348 location privacy, #350/#353/#354 owner contact and retnode links, and the #343/#346/#347/#357 palette work. Rules applied: main's version is the base for the add/add files (public_geometry.py, test_no_real_identities.py); the PR's ref-keying is layered on top of main's rework everywhere else; substitution still runs last on every publication path and fails closed for unregistered real nodes. Follow-on edits outside the conflicted set, needed to compile or to keep main's tests honest under ref addressing: frontend nodeSites.ts (+test) now reads is_synthetic; backend test_node_ref.py and test_public_geometry.py address routes by ref / synthetic id; analytics.py's owner-private listing merge keys on the owner identity. Verified: backend 3495 passed / 2 skipped, ruff clean; frontend tsc, lint, vite build, vitest 232 passed; dashboard tsc, eslint, vitest 53 passed, build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Depends on offworldlabs/retina-analytics#28 — the submodule bump in the first commit points at that branch's commit, so merge that first (CI checks out submodules recursively and needs the commit reachable on the remote; it is, on the feature branch).
Summary
Three map fixes requested 2026-09-06: co-located nodes were unclickable behind each other, node popups and panels printed raw
node_ids, and the "detection area" was the declared beam (theoretical cone for nodes with no data, and a beam-clipped pie slice for nodes with data). Now: one marker per receive site with a count badge and a per-node popup, every node named by a publicnode_ref, and the only detection area ever drawn is the measured (ADS-B-calibrated) polygon, unclipped.Changes
Backend — coverage polygon (submodule bump, retina-analytics#28)
manager.get_node_summaryunderFOV_MODE=offpublishesto_polygon(evidence_only=True)instead of clipping tobeam_azimuth_deg/beam_width_deg. radar3 on test has evidence in all 72 bearing bins (reach 17–65 km) and was served a 120° slice.Backend —
node_reffor every nodeservices/node_ref.py:public_node_ref(node_id)returns the registry'sNode.node_refwhen the node registered through/v1/nodes, else"nde" + 12base36 chars derived by HMAC-SHA256 over the node fuzz salt with anode_ref|domain prefix — same shape asmint_node_ref, stable across restarts, never the raw id. Thenodestable on the test droplet is empty (blah2 bridge and plain-TCP nodes never register), so without the derivation no live node would have a public name./api/radar/analytics(both variants, as copies of the summaries),/api/radar/analytics/{node_id}and/api/radar/nodes. The node API contract is untouched.Frontend
map/nodeSites.ts:groupNodesBySite(exact published-coordinate equality, matching hownode_sites.pyco-locates),nodeLabel(node_ref, else "unlisted node" — never the id),polygonMaxReachKm.NodeMarkersLayerrenders sites: one uncertainty disc per site, a count badge on shared sites (nodeSiteIcon), popup lists each node withnode_refand a measured-coverage line; shared sites get per-node Show/Hide buttons, single-node sites keep click-to-select. Beam/range lines removed from popups.CoverageLayer, selected-node overlay, contributing-node highlights. Nodes without a polygon draw no area.InBeamDiagnostictests membership withpointInPolygonagainst the empirical polygon (new helper ingeo.ts), skips nodes without one; toolbar button renamed "Coverage gaps".AircraftDetailPanel"Claiming node" and detecting-node list print labels via anodeLabelForprop; illuminator popup lists labels.RadarNodegainsnode_ref; thebeam_*/max_range_kmfields stay, commented as arc-rebuild parity only (bistaticArc.tsmirrors the backend arc geometry).Docs —
pipeline.md§7 (evidence-only publication,node_ref),architecture.md,arc-display.md;CALIBRATION_SCHEMAnote corrected 5→6.Test coverage
tests/test_node_ref.py(10: shape, determinism, id/salt sensitivity, registry row wins, no raw-id fallback),test_analytics_refresh.py(+node_ref in both payload variants and the nodes block),test_public_location.pyfixture spread over six bearings so its 30 points still form an area. Full suite: 2953 passed, 1 skipped, coverage 85% (gate 55%).nodeSites.test.ts(grouping, labels, reach),geo.test.ts(+pointInPolygon inside/outside/north-wrap);tsc, eslint,vite build, vitest 184 passed. Run twice independently (implementer and reviewer).--all-files: all hooks pass.nde…labels everywhere, radar3's all-round polygon, no cones on the five unmeasured nodes.Review notes
node_idandbeam_*. Anonymity here is display-level:node_idremains the join key in the aircraft feed (node_id,contributing_node_ids,detecting_nodes) and analytics keys. Replacing the keys on the wire is a larger change across feeds, dashboard and e2e; flagged as follow-up.tasks/solver.py~L2408), arc clipping to the wedge (track_gates._build_single_node_arc,bistaticArc.ts),_refresh_missed_detections, association grid, and the diagnostic/api/test/node/{id}/detection-range.FOV_MODE=activeis the existing evidence-based path for the gates; these need their own measurement before changing.NODE_FUZZ_SALTrenames every unregistered node — the same effect rotation already has on positions. e2enodes.spec.tsassertsbeam_*fields indetection_area; they are still served, so no change there.🤖 Generated with Claude Code