The "relays for 2+ distinct region scopes" definition doesn't hold up
in practice: most nodes end up carrying both a broad national/regional
scope (e.g. #dk, #eu) and several local ones, so the vast majority of
repeaters would eventually qualify as "bridge" -- the flag stops
meaning anything useful.
Removes IsBridge from PacketPathPoint/PacketPathObserver, the
markBridgeRepeaters handler step, the purple-outline styling, and the
dedicated test file -- keeps PublicKey on PacketPathObserver, since
the click-to-node-detail feature depends on it independently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tooltip text grew long once role, approx/confidence, bridge, and
distance/timing info were all combined into one line. Leaflet's
default tooltip is white-space:nowrap, so a long one stretched into a
single unreadable line spanning the map instead of wrapping.
Adds a packet-path-tooltip CSS class (white-space:normal, max-width)
passed to every bindTooltip call in this component.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hop points already carried publicKey; PacketPathObserver got it in the
bridge-highlighting commit. Clicking any marker with one now closes
the modal and navigates to #/nodes/{pubkey} -- the same hash route the
rest of the app already links to (see e.g. public/channels.js). A
marker with no publicKey (e.g. a bridge-type observer keyed by device
name, not pubkey) gets no click handler and stays inert. Tooltip gets
a "click for node detail" hint when applicable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds DistanceFromFirstKm to PacketPathBranch: the great-circle
distance (haversine) between a branch's own Observer and First's
Observer. Zero for First itself. Deliberately omitted when either
side is positioned via Approx (a neighbor-centroid estimate) -- a
distance computed against a guess isn't a real measurement worth
surfacing.
Rendered in the observer tooltip as "42.3 km away" alongside the
elapsed-time label, giving a concrete sense of how far the flood
physically reached, not just how many hops or how long it took.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds IsBridge to PacketPathPoint/PacketPathObserver and PublicKey to
PacketPathObserver (needed to look bridges up, and useful on its own
for future features). Set by a new markBridgeRepeaters handler-level
step in routes.go, not GetPacketPath itself, since the underlying data
(TransportedScopes per repeater) lives in the in-memory store, not
SQL -- same "relays for 2+ distinct regions" definition and data
source as the Foreign Traffic tab's existing Bridge badge
(ScopeStatsResponse.bridgeRepeaters), just applied to whichever
handful of pubkeys one packet's path touches.
The map gives a bridge repeater a bold purple outline (on top of
whatever primary/approx styling already applies) plus a tooltip note,
so the mesh's backbone nodes stand out regardless of which branch
they're in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PacketPathPoint already carried Role; PacketPathObserver now does too
(from the observer's own nodes row, when it's known as a mesh node
itself and not just an MQTT/API listener). Rendered as a small icon
prefix in the tooltip (📡 repeater, 🏠 room, 📱 client, 🌡️ sensor) --
markers stay plain circleMarker dots throughout, since a role-specific
shape would clash with the color/dash coding already carrying primary,
approx, and observer meaning.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nearestPositionedNeighbor now also returns how many positioned
neighbors fed the weighted centroid and the widest distance between
any two of them (0 with a single contributor). Exposed via new
ApproxNeighborCount/ApproxSpreadKm fields on PacketPathPoint and
PacketPathObserver.
The map scales the approximate marker's size/opacity accordingly: one
neighbor (or several that disagree widely) renders as a bigger,
fainter ring; several agreeing neighbors render tighter and more
solid. The tooltip also states the contributor count.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds SecondsAfterFirst to PacketPathBranch: the gap between the
earliest-arriving observation (First) and this branch's own deepest
observation. Zero for First itself. Rendered in the observer's tooltip
as "+4.7s" (or "Nm Ss" for longer gaps, "first to arrive" for zero) --
gives a sense of the propagation order across the flood, not just
depth/breadth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nearestPositionedNeighbor picked exactly one neighbor (the strongest by
edge count) and used its exact coordinates -- so the approximate marker
always landed precisely on top of that neighbor's own dot. Since each
neighbor's own position is a real, precise fix, and only the
unpositioned node's position relative to them is unknown, more
positioned neighbors should narrow the estimate rather than being
ignored in favor of just the top one.
Now sums every positioned neighbor's coordinates weighted by
neighbor_edges.count and returns the centroid. With exactly one
positioned neighbor this is unchanged (reduces to that neighbor's exact
position); with several, the marker settles somewhere among them
instead of overlapping a single specific node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A plain hollow (fillOpacity:0) dashed ring at normal marker size blended
into the map tiles and was easy to miss entirely. Bumped it to a larger
radius (+4px), thicker dashed stroke, and a faint fill so it reads as a
soft, deliberately-imprecise blob instead of vanishing against the
background.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A hop or observer with no position of its own (no self-advertised GPS,
no name match, no IATA) previously stayed unplotted entirely. Adds a
last-resort fallback: look up its strongest neighbor_edges neighbor
(ranked by observation count) and, if that neighbor has a real
position, borrow it as an approximate stand-in -- flagged via a new
`approx` field so callers never mistake it for a real fix.
The View Path map renders approximate markers hollow and dashed
instead of solid, with a status-line callout ("N approximate (via
nearest neighbor)"), and the "N hops without a known position" count
drops accordingly for whichever ones now have a stand-in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetPacketPath's branches are sorted deepest-first, so the map had no
natural "where did this start" anchor. Adds a `first` field: the
single earliest-arriving observation across every station regardless
of observer or hop depth (the same "first observation wins" pick
already used for a ping message's own meta line), positioned the same
way as any other observer (own GPS, then name match, then IATA).
The map draws it as a green ring on top of everything else, since it
usually coincides with one of the already-plotted branch dots, plus a
status-line callout -- an approximate landmark for where the message
entered the visible mesh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetPacketPath used to resolve only the single farthest-traveled
observation and discard the rest. It now returns one branch per
distinct station (kept at that station's own deepest observation),
each with its own hop count and, where resolvable, relay chain --
so the map visualizes the packet's full flood spread instead of one
route. Stations whose path never resolved, or who heard the packet
directly (0 hops), still contribute a branch via their own position,
rather than being silently dropped.
The "View path" link in the ping-bot reply no longer requires hops > 0
to show, since even an all-direct ping still has observer spread worth
plotting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New GET /api/packets/{hash}/path resolves a packet's DEEPEST observation
(same "farthest leg is more informative" reasoning as the ping-bot reply
itself) to a geographic point sequence: each relay's name/role/lat/lon in
path order, plus the hearing observer's position (from its configured
IATA code, like the Wardriving tab). Hops that have never advertised a
GPS position come back with null lat/lon rather than being dropped, so
the frontend can draw a gap instead of guessing.
Frontend: public/packet-path-map.js is a small on-demand Leaflet modal
(reuses node-reach-map.js's tile/marker conventions, but draws an ordered
chain instead of a star) opened via a new "View path" link on the
ping-bot reply -- shown only when there's an actual multi-hop route and
packet hash to look up. Kept general (keyed by packet hash, not
ping-specific) since any packet with a resolved path could use it later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>