- Replace inline onclick on <tr> elements with data-action/data-value attributes
- Add tabindex="0" and role="row" to all clickable rows
- Add delegated click and keydown (Enter/Space) listeners on containers
- Remove window._pktSelect, _pktToggleGroup, _pktSelectHash, _nodeSelect globals
- Convert to local functions referenced by delegated handlers
Affected files: packets.js, nodes.js, analytics.js
(channels.js and observers.js had no interactive <tr> elements)
- nodes.js: escape decoded.text and decoded.name in advertisement list rendering
- packets.js: escape decoded.text in summary display and decoded.name in fieldRow
- max-width 900px → 1600px for analytics page
- Added analytics-grid CSS class for auto-fit columns
- Hash Stats: distribution + timeline side-by-side, adopters + top hops side-by-side
- RF, Topology, Channels already used analytics-row flex layout
- Cards now fill available screen width instead of being squished
Each cell displays the hex prefix (AB, C3, etc). Color meaning:
- Dark: unused prefix (no known nodes)
- Green: 1 node using it (safe, no collision)
- Yellow→Red: 2+ nodes sharing prefix (collision risk)
Legend added below matrix.
Built O(1) prefix index on allNodes (replaces O(n) filter per hop).
Cache disambiguation results by hop sequence key — same path only
resolved once. Subpaths endpoint: ~1s for 19K packets (was 30s+).
Replace naive first-match resolution in /api/analytics/subpaths and
/api/analytics/subpath-detail with shared disambiguateHops() function.
Each path is now resolved with forward+backward nearest-neighbor pass
and distance sanity check, matching live map and resolve-hops logic.
1-byte collision table now shows:
- Max pairwise distance between colliding nodes
- Classification: Local (<50km, true collision), Regional (50-200km,
possible atmospheric), Distant (>200km, internet bridge/MQTT/separate mesh)
- Coordinates for each node
- Legend explaining each classification
- Sorted: distant first (most interesting)
500 limit was cutting off nodes, causing single-candidate false
matches (e.g. Osprey-Base in Seattle winning for prefix 60 when
little russia in SF was beyond the limit). With ~400 nodes having
coords, 2000 ensures all are loaded.
After sequential disambiguation, sanity-check each hop: if it's
>200km (~1.8°) from both neighbors, it's almost certainly a 1-byte
prefix collision with a distant node, not an actual hop. Mark as
unreliable (server) or drop from animation (live map).
Fixes packet 19384 bouncing to Seattle — Spiden Repeater shares
prefix 1D with an unknown local node.
Instead of averaging all hops to a center point, walk the path
sequentially — each ambiguous hop picks the candidate closest to
the previously resolved hop. Forward pass seeds from first known
position, backward pass seeds from observer. This matches physical
reality: packets travel hop-by-hop in RF range.
- ADVERT is payload_type 4 not 1
- Frontend sends observer_id (not just observer_name) since some
observers have null name
- Observer position derived from geographic center of nodes it
commonly receives ADVERTs from
- Last hop in path disambiguated by proximity to observer position
Packets page now passes server-disambiguated full pubkeys.
Map needs bidirectional prefix match since DB nodes may have
truncated (8-char) or full (64-char) keys. Removes redundant
client-side geographic disambiguation since packets page already
resolved via /api/resolve-hops.
- Observer name resolved to lat/lon for geographic context
- Last hop in path sorted by distance to observer (not center)
since it's the node that delivered the packet to the observer
- Packets page now passes observer_name to /api/resolve-hops
- Fixes CroatR1 being picked over Kpa Roof Solar for hop 8A
Was fetching /api/resolve-hops (with geographic disambiguation) then
throwing away the result and still passing raw 1-byte prefixes. Now
passes full pubkeys so the map doesn't re-disambiguate differently.
When the same packet is received multiple times (different hop counts
as it propagates), use the reception with the most hops for display.
Fixes routes showing incomplete paths when the first reception had
fewer hops than later ones.
Applies to both grouped query and individual packet detail endpoint.
When a 1-byte hop prefix matches multiple nodes, pick the one
geographically closest to the center of other known hops in the
path — same logic as /api/resolve-hops but client-side.
Previously just took the first match, which could be a node in
Chicago matching a local Bay Area hop prefix.
No more 5-minute window or 200-packet limit. Fetches up to 10K
packets from the scrub point forward and replays them all.
When replay finishes, resumes live.
- nodeActivity cleared in clearNodeMarkers (removes ghost heatmap)
- When replay finishes, set scrubTs to last packet's timestamp so
updateTimelinePlayhead holds position instead of snapping right
- animatePacket checks for ADVERT type and adds new nodes to map
with marker if they have location data
- Unpause always resumes to live (removed missed packets prompt)
When scrubbing to a past time, only nodes that had advertised before
that time appear on the map. Nodes that hadn't been seen yet are
hidden.
- Added 'before' param to /api/nodes (filters first_seen <= before)
- loadNodes(beforeTs) accepts optional timestamp filter
- clearNodeMarkers() removes all markers and data
- vcrReplayFromTs clears and reloads nodes for replay time
- vcrResumeLive reloads all nodes (no filter)