Commit Graph
2977 Commits
Author SHA1 Message Date
dborup 07a2e1f2cb Merge branch 'areas-meshguide-sync' 2026-07-24 20:14:38 +02:00
dborup fecb53dd00 feat: pong reply reports how far the packet spread geographically
Adds a "spread up to Nkm" part to the ping-bot's reply, computed as the
farthest any hearing station's own GPS fix was from whoever heard it
first -- the same signal View Path's map shows, surfaced directly in
the chat bubble so it's visible without opening the map. Deliberately
skips View Path's neighbor-centroid position fallback (too expensive
to run per-ping across a page of messages); a station without its own
GPS fix just doesn't contribute, so the part is silently omitted when
fewer than two stations have one.

"heard by N observers" already reported total station breadth per
transmission, so no separate count was added.
2026-07-24 20:06:40 +02:00
dborup fa99231cdb Merge branch 'areas-meshguide-sync' 2026-07-24 19:50:34 +02:00
dborup ae16d23263 fix: View Path tooltip renders as a tall single-column rectangle
The tooltip's containing pane has no intrinsic width, so a plain
width:auto block collapsed to min-content (one word per line) and
never grew toward max-width at all. width:max-content makes it size
to its content up to max-width, producing a normal wrapped box.

Verified live on stg by patching the CSS in-page and re-triggering a
tooltip before shipping.
2026-07-24 19:44:58 +02:00
dborup d1888618ce fix: View Path tooltip still wrapped too narrow at larger font sizes
max-width was a fixed 220px; at bigger font/zoom settings that left
room for barely one word per line. Use 28ch instead so the box scales
with the actual font size.
2026-07-24 19:36:07 +02:00
dborupandClaude Sonnet 5 4b4dc9f27f revert: remove View Path bridge-repeater highlighting
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>
2026-07-24 19:27:56 +02:00
dborupandClaude Sonnet 5 6718c246b9 fix: View Path tooltips wrap instead of stretching across the whole map
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>
2026-07-24 19:19:57 +02:00
dborupandClaude Sonnet 5 d0205b0edd feat: View Path markers are clickable, navigate to node detail
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>
2026-07-24 18:34:50 +02:00
dborupandClaude Sonnet 5 3c8ec35ea2 feat: View Path shows distance from 'first' in km
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>
2026-07-24 18:23:05 +02:00
dborupandClaude Sonnet 5 575743efda feat: View Path highlights confirmed bridge repeaters
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>
2026-07-24 18:16:15 +02:00
dborupandClaude Sonnet 5 d5214006d6 feat: View Path shows a role icon for observers with a known node role
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>
2026-07-24 18:03:25 +02:00
dborupandClaude Sonnet 5 4ecb9c3902 feat: scale View Path approximate markers by neighbor confidence
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>
2026-07-24 17:44:19 +02:00
dborupandClaude Sonnet 5 8e61a78d32 feat: View Path shows how long after 'first' each station heard the packet
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>
2026-07-24 17:38:15 +02:00
dborupandClaude Sonnet 5 cc66581a73 Merge areas-meshguide-sync into master: View Path approximate-position markers, made visible and weighted by all neighbors
- Approximate (neighbor-borrowed) markers on the View Path map are now
  actually visible -- larger radius, thicker dashed stroke, faint fill
  instead of a near-invisible hollow outline.
- The approximate position itself is now a count-weighted centroid of
  EVERY positioned neighbor, not just the single strongest one, so it
  no longer sits exactly on top of one specific neighbor's dot when
  more than one positioned neighbor is known.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 17:28:58 +02:00
dborupandClaude Sonnet 5 fb69681de7 feat: View Path approximates unpositioned nodes from a weighted centroid of neighbors, not just one
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>
2026-07-24 17:19:47 +02:00
dborupandClaude Sonnet 5 09b84f372b fix: make approximate View Path markers actually visible
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>
2026-07-24 17:10:45 +02:00
dborupandClaude Sonnet 5 5e6e6252b4 feat: View Path falls back to the nearest positioned neighbor for unpositioned hops/observers
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>
2026-07-24 17:02:38 +02:00
dborupandClaude Sonnet 5 f244f27a63 fix: View Path treats (0,0) node coordinates as no fix, not the Gulf of Guinea
GetPacketPath's node-position lookups (relay-hop points and observer
positioning) read raw lat/lon straight off the nodes row, so a node
with (0,0) stored literally instead of NULL -- MeshCore's "never
actually reported a GPS position" case in practice -- rendered as a
real point off the coast of Ghana instead of no position at all.

Excludes (0,0) the same way GetNodesForScopeAdoption and
geofilter.PassesFilter already do; the node's name is kept, just not
the bogus position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:43:53 +02:00
dborupandClaude Sonnet 5 03fc104d86 Merge areas-meshguide-sync into master: View Path origin landmark + large-mesh edge safety gate
- GetPacketPath now also returns `first`: the single earliest-arriving
  observation across every station (usually 0 hops, close to the
  sender), drawn as a green landmark ring on the View Path map --
  Branches alone had no natural "where did this start" anchor.
- The ingestor's interior neighbor-edge creation (added earlier today)
  now excludes 1-byte hop prefixes, per upstream review on PR #1852
  from a 1000+ node deployment: a "currently unique" 1-byte prefix can
  silently become wrong once an unknown node sharing that byte joins,
  with no secondary signal to catch it. 2+ byte prefixes are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:33:15 +02:00
dborupandClaude Sonnet 5 32d5f373a0 fix: exclude 1-byte hops from interior neighbor-edge creation
Review on upstream PR #1852 (SaarMesh, 1071 nodes, 18.9M hop
occurrences) flagged a large-mesh false-edge risk in 7fe3dd9's
interior-hop edges: resolvePrefix only requires a hop to be unique
among nodes known TODAY. At 1 byte that "uniqueness" is fragile --
SaarMesh measured ~1.2% of their nodes as currently-unique on 1 byte,
each one a silent miscall waiting for an unknown/new node sharing that
byte to appear. Unlike the two endpoint edges, an interior edge has no
ADVERT/ANON_REQ to double-check it against, so there's no way to catch
the mistake once made. 2+ byte prefixes don't have this problem
(SaarMesh: 99.4% of nodes uniquely resolvable there).

Adds minInteriorEdgeHashBytes=2, matching the conservative default
already chosen for the upstream #1824 pathTrust.minHashBytesForMapping
config -- inlined here rather than depending on that config landing
first, since it isn't wired into any call site yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 15:57:27 +02:00
dborupandClaude Sonnet 5 a4f927c23a feat: View Path marks the earliest-arriving observation as a landmark
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>
2026-07-24 15:47:52 +02:00
dborupandClaude Sonnet 5 a50e199f6d Merge areas-meshguide-sync into master: View Path flood spread + observer positioning + interior-hop resolution
- GetPacketPath now returns every station that observed a packet
  (one branch per observer, deepest-first) instead of only the single
  farthest-traveled route, so the "View path" map shows the full
  flood spread.
- Observer positions now prefer the station's own self-advertised GPS
  (falling back to a node-name match, then the IATA table) instead of
  relying on the IATA table alone.
- The ingestor's neighbor_edges builder now also records adjacency
  between consecutive hops within a path, not just the two endpoints
  -- this is what resolvePathWithContext needs to resolve past hop 0,
  and previously that data was never written at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:54:22 +02:00
dborupandClaude Sonnet 5 7fe3dd99fe fix: neighbor_edges now records interior hop-to-hop adjacency, not just the two endpoints
resolvePathWithContext (path_resolver.go) anchors each hop's
disambiguation on the previously-resolved hop via a neighbor_edges
adjacency lookup. But buildAndPersistNeighborEdges only ever wrote two
edge shapes -- originator<->hop0 (ADVERTs only) and observer<->lastHop
-- never anything between consecutive hops in the middle of a path.
So the anchor lookup for hop 1+ was always querying a table that
structurally could never contain that pair (unless it coincidentally
matched some other packet's endpoints), which is why a resolved_path
would reliably stop after hop 0 regardless of how many hops the
packet actually traveled.

Now also emits an edge for each consecutive pair of hops within the
path itself (when both resolve unambiguously), independent of
isAdvert/from_pubkey since it's relational between the hops, not tied
to origin/observer identity. This feeds the exact adjacency data the
anchor resolver already knows how to consume -- no changes needed
there. Coverage improves gradually rather than all at once, since an
interior pair must itself already be unambiguous to seed an edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:02:06 +02:00
dborupandClaude Sonnet 5 e38651fad0 fix: View Path falls back to a node name match when an observer's id isn't its pubkey
Some bridge-type observers (seen in the wild on openHop-Repeater
firmware) publish their MQTT status keyed by device name instead of
their mesh pubkey, so observers.id never matches nodes.public_key for
them even though the same physical device has a real, positioned node
row under its actual pubkey. The pubkey-based GPS lookup added in
94be933 silently missed these.

Now falls back to matching the observer's display name against nodes
when the pubkey lookup finds nothing, skipping the match entirely if
more than one positioned node shares that name rather than guessing
which one is the real observer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 11:57:40 +02:00
dborupandClaude Sonnet 5 94be933ed5 fix: View Path positions observers from their own GPS, not just the IATA table
GetPacketPath only ever tried the hardcoded iataCoords table to place
an observer. Any station whose configured IATA code isn't a real
airport (a custom/regional code, or a typo) fell out of the map
entirely -- even when the station is itself a mesh node that has
self-advertised a real GPS position, the same source /api/observers
and the Wardriving tab already treat as authoritative.

Now checks the observer's own node-table position first (folded into
the existing bulk pubkey lookup, no extra query) and only falls back
to the IATA table when it has none.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 11:35:28 +02:00
dborupandClaude Sonnet 5 2f686d40c6 feat: View Path map shows every station that heard the packet, not just the deepest one
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>
2026-07-24 11:14:22 +02:00
dborupandClaude Sonnet 5 9ac4bdac81 Merge areas-meshguide-sync into master: ping-bot keywords + scope/area cleanup
Adds "/ping" as an additional trigger keyword alongside "ping", and drops
scope/area from the pong reply since both are already shown on the
triggering message's own meta line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:55:25 +02:00
dborupandClaude Sonnet 5 461fbaad1b feat: remove scope/area from the ping-bot reply
Both are already shown on the triggering message's own meta line right
above it, so repeating them in the pong reply was redundant. Drops the
handler-level appendAreaToBotReply pass (routes.go) entirely along with
its now-unused scope plumbing through pendingPing (db.go) and the
client-side pingBotReply (channels.js).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:42:29 +02:00
dborup 59ea92f117 Merge areas-meshguide-sync into master: ping-bot reply + view path
Adds a CoreScope-only "ping" bot to the Channels view: a channel message
matching a trigger word ("ping" or "/ping") gets a synthesized "pong"
reply showing hop count, relay path (repeater names, resolved once per
page for every ping on it), region scope, area, SNR, and how many
distinct observers heard it (reporting the deepest/farthest-along
observation across all of them, not just the first). Deliberately never
transmitted back onto the mesh -- CoreScope has no publish path to a
MeshCore broker/radio -- and clearly marked "Not sent to the mesh" so
it's never mistaken for a real bot reply.

A new general-purpose GET /api/packets/{hash}/path endpoint resolves any
packet's relay path geographically (node lat/lon + observer position),
backing a "View path" map link on the reply.

Also includes: MeshCore ADVERT Feat1/Feat2 capability-byte persistence
was added and then reverted after confirming live traffic never sets
those flags (columns remain in the schema, unused).
2026-07-23 16:02:52 +02:00
dborupandClaude Sonnet 5 653949479a feat: ping-bot also triggers on "/ping", not just bare "ping"
Trigger check moved from a single string comparison to a small
pingTriggerWords set (mirrored by hand in db.go and channels.js), so
adding more trigger words later is a one-line change in each. Still an
exact match after the existing @mention-stripping -- "/pingx" etc. don't
match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 15:52:08 +02:00
dborupandClaude Sonnet 5 5842d85abd feat: "View path" map for the ping-bot reply
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>
2026-07-23 15:38:52 +02:00
dborupandClaude Sonnet 5 f0f28b5c32 fix: ping-bot reply says "observers" not "stations"
Matches the term used everywhere else in CoreScope (/api/observers,
observer names, etc.) instead of an inconsistent synonym.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 15:17:10 +02:00
dborupandClaude Sonnet 5 3b5bc17180 feat: ping-bot reply reports full reach (deepest hop + breadth), not just first observation
GetChannelMessages previously used "first observation wins" for a ping's
botReply -- if the same flood was heard by multiple stations, only
whichever one happened to be scanned first shaped the hop count, SNR, and
relay path. That understates reach: different stations legitimately hear
the same flood at different hop depths depending on which relay leg
reached them.

Now tracks, across every observation of the ping: the DEEPEST (max-hop)
observation's hops/SNR/relay path, and every DISTINCT station that heard
it. The reply shows the deepest leg's path and reports "heard by N
stations" once more than one did (falling back to the single station's
name when there's exactly one, same as before).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 15:09:19 +02:00
dborupandClaude Sonnet 5 3c091900e2 rename: ping-bot sender MeshviewBot -> CoreScopeBot
Matches the product name (CoreScope) rather than the meshview.dk domain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 14:55:17 +02:00
dborupandClaude Sonnet 5 b3d7efb511 feat: ping-bot reply shows relay path, scope, and area
REST-loaded channel history (GetChannelMessages) now resolves the ping
message's relay path to node names ("via RepeaterA → RepeaterB"), bulk-
resolving every referenced pubkey across the page in one query rather
than per-message. Falls back to the raw pubkey when a hop's node isn't
known. Also includes the message's region scope and (via a handler-level
pass, since area resolution needs server config db.go doesn't have)
its resolved area.

The client-side pingBotReply (WebSocket live-push + PSK-channel decrypt
paths) gains scope/area too, since both are already present in that data.
Relay-path names are REST-only for now: the live WS broadcast doesn't
carry a resolved_path, only historical/REST-loaded messages do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 14:39:13 +02:00
dborupandClaude Sonnet 5 79c734a26e fix: ping-bot reply missing on live/WS-pushed and PSK-decrypted messages
The ping->pong synthesis only ran in GetChannelMessages (server-side,
REST-loaded history). Messages that arrive while a channel is already
open -- via the WebSocket live-push path, or the client-side decrypt path
for user-added PSK channels -- are built directly from the broadcast
payload and never round-trip through that endpoint, so typing "ping" in
an already-open channel showed nothing until a manual reload.

Adds a client-side pingBotReply() mirroring the Go version's trigger rule
and reply format, wired into both paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 13:59:44 +02:00
dborupandClaude Sonnet 5 8144b46136 feat: CoreScope-only "ping" bot reply in Channels view
A channel message whose text is exactly "ping" (mention-prefix like
"@MeshviewBot ping" stripped first) now gets a synthesized "pong" reply
showing hop count, SNR, and hearing observer -- computed at read time
from that message's own already-stored data, no new table.

Deliberately NOT transmitted back onto the mesh: CoreScope has no publish
path to a MeshCore broker/radio (confirmed: it only ever subscribes to
MQTT, never publishes). The reply is visible only in CoreScope's own
Channels view, rendered as a visually distinct dashed-border bubble with
an explicit "Not sent to the mesh" caveat so it's never mistaken for a
real bot reply the sender's own radio received.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 13:47:28 +02:00
dborup f8ec91319f Revert "feat: persist ADVERT Feat1/Feat2 capability bytes per node"
This reverts commit 10f9f22148.
2026-07-23 11:35:55 +02:00
dborupandClaude Sonnet 5 10f9f22148 feat: persist ADVERT Feat1/Feat2 capability bytes per node
MeshCore firmware sends two capability bytes on ADVERT packets (wire bits
per AdvertDataHelpers.h) whenever the HasFeat1/HasFeat2 flags are set.
CoreScope already decoded them per-packet but discarded them rather than
storing a per-node value.

- New feat1/feat2 columns on nodes/inactive_nodes (internal/dbschema,
  idempotent ALTER like the existing multibyte_sup/multibyte_evidence
  columns).
- Ingestor's UpdateNodeTelemetry now writes feat1/feat2 alongside
  battery_mv/temperature_c in the same COALESCE-based UPDATE, from the
  same ADVERT payload.
- Server exposes them on /api/nodes and /api/nodes/{pubkey} (nullable,
  same shape as battery_mv/temperature_c).
- Node detail page shows them as raw hex (0x....) in the Overview panel
  when present -- undecoded, since CoreScope doesn't know the individual
  bit meanings, but no longer silently discarded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:26:06 +02:00
dborup 5b155d10f9 Merge areas-meshguide-sync into master: network-wide hop-depth analytics
Extends the #1812 per-node relay hop-count work with a network-wide view
via /api/analytics/hop-depth (GetHopDepthAnalytics):

- Scopes tab: new "Hop Depth" sub-tab with a scoped-vs-unscoped
  containment comparison, a P95-derived "suggested flood.max.unscoped"
  callout, and a median-hop-over-time trend chart.
- Foreign Traffic tab: min/median/max hop-depth columns on the existing
  unscoped-relay table, plus a "Bridge" badge for repeaters confirmed
  relaying for 2+ regions (a high hop reading there is expected, not a
  containment leak).
2026-07-23 10:23:02 +02:00
dborupandClaude Sonnet 5 1acaabe10a refactor: move median-hop time-series trend to the top of the Hop Depth tab
Was rendered last, below the stat cards and bar chart -- moved above them
since it's the most at-a-glance signal (is containment trending better or
worse) and shouldn't require scrolling past a static snapshot to see.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 10:05:21 +02:00
dborupandClaude Sonnet 5 e23e632d54 feat: scoped-vs-unscoped median hop time-series trend on Hop Depth tab
GetHopDepthAnalytics now buckets its scoped/unscoped hop-index tallies by
time (same 5min/1h/6h bucketing as GetScopeStats' TimeSeries) and computes
a per-bucket median for each series, exposed as a new TimeSeries field.
Median is a *int, nil when a bucket had no traffic of that kind -- 0 is a
valid median hop, so absence has to stay distinguishable from zero.

Frontend renders it as a two-line SVG trend chart on the Scopes > Hop
Depth sub-tab (same visual language as Overview's scoped/unscoped
time-series), answering "is containment getting better or worse over the
window" rather than just a single window-wide snapshot. Each series'
polyline is built from contiguous non-null segments so a gap renders as
a visible break instead of silently interpolating through missing data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:50:51 +02:00
dborupandClaude Sonnet 5 6c9de12542 feat: flag bridge repeaters on the Foreign Traffic unscoped-relay table
A bridge repeater (confirmed relaying for 2+ regions, ScopeStatsResponse.
bridgeRepeaters) is supposed to see traffic that already traveled far from
another region's scope -- a high hop-depth reading there is expected, not
a containment leak. Adds a small "Bridge" badge next to those repeaters'
names so the min/median/max hop columns added earlier can be read in that
context, and a plain regional repeater with the same pattern stands out
as the one actually worth investigating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:39:28 +02:00
dborupandClaude Sonnet 5 6eb094ba00 feat: suggest a flood.max.unscoped value from unscoped hop-depth P95
Adds a third stat card to the Scopes > Hop Depth section: the P95 of the
unscoped hop-depth histogram, framed as a concrete flood.max.unscoped
starting point instead of leaving the operator to eyeball the bar chart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:24:52 +02:00
dborupandClaude Sonnet 5 6a215002c1 refactor: move Flood Containment hop-depth section to its own Scopes sub-tab
Was crowding the Overview panel; gets its own "Hop Depth" sub-tab next to
Overview/Regions/Hygiene, with its own 1h/24h/7d window picker wired to
the same selectedWindow/load() as the rest of the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:10:27 +02:00
dborupandClaude Sonnet 5 2d30e49247 feat: network-wide hop-depth analytics (Scopes + Foreign Traffic tabs)
Extends the #1812 per-node relay hop-count work with a network-wide view:
GET /api/analytics/hop-depth answers (1) whether scoped traffic actually
travels fewer hops than unscoped before hitting a repeater's flood.max cap
(Scopes tab Overview: new "Flood Containment" comparison), and (2) which
repeaters relay unscoped traffic that already traveled far vs merely
locally (Foreign Traffic tab: min/median/max hop columns joined onto the
existing unscoped-relay table by public key).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 08:48:11 +02:00
dborup 1eb7388194 Merge areas-meshguide-sync into master: relay hop-count analytics (#1812)
New GET /api/nodes/{pubkey}/hop_analytics endpoint + "Relay Hop-Count"
chart on the node analytics page, closing upstream
Kpa-clawbot/CoreScope#1812. No meshguide.dk-specific content in this
range (git kept the prior merges' exclusion of ops/meshguide-sync/
with no manual step).
2026-07-23 07:30:37 +02:00
dborup 902d0cac2f feat: add relay hop-count analytics for tuning flood.max (closes upstream #1812)
New GET /api/nodes/{pubkey}/hop_analytics?days= returns, for each recent
transmission that passed through this node as a relay, the node's own
0-based index within the packet's resolved path -- the value MeshCore
firmware compares against flood_max/flood_max_advert/flood_max_unscoped
in allowPacketForward (Packet.cpp path_len = header & 63; MyMesh.cpp
getPathHashCount() >= flood_max). Deliberately distinct from the
existing hopDistribution field on /analytics, which measures path
length to whichever observer reported the packet -- a different,
unrelated number, confirmed via the upstream issue's multi-round
firmware-source discussion.

Candidate-finding reuses the same byPathHop-index + resolved_path
confirmation approach as handleNodePaths (routes.go), written fresh
rather than refactored out of that function to avoid any regression
risk in its older, correctness-critical prefix-collision handling
(#929, #1197, #1278, #1352). Only transmissions with a canonical
resolved_path contribute a data point -- no resolved_path means no
reliable hop index, so it's skipped rather than guessed.

Frontend: new "Relay Hop-Count" card on the node analytics page --
histogram (Chart.js) + a hand-drawn boxplot canvas above it sharing the
same x-axis (no Chart.js boxplot plugin is loaded), filterable by chip
matching the firmware knob names (flood default, flood_advert,
flood_unscoped, direct).
2026-07-23 07:19:10 +02:00
dborup 9f5d594cf5 Merge areas-meshguide-sync into master: split geo-filter/selected-area map overlays
The old single "Mesh live area" checkbox actually showed the
homeArea-linked geo_filter boundary, confusing paired with the
already-existing "Area: X" dropdown that filters map nodes -- picking
an area there drew nothing. Now two independent checkboxes on both
Map and Live: "Geo filter boundary" (unchanged) and "Selected area
outline" (new, draws whichever area is picked in the dropdown).

No meshguide.dk-specific content in this range (git kept the prior
merges' exclusion of ops/meshguide-sync/ with no manual step).
2026-07-23 06:13:41 +02:00
dborup 4d46dba4b2 feat: split "Mesh live area" into a geo-filter toggle and a selected-area outline
The old single "Mesh live area" checkbox actually showed the
homeArea-linked geo_filter boundary, which was confusing paired with
the already-existing "Area: X" dropdown (AreaFilter) that filters
which nodes show on the map -- picking an area there drew nothing.

Now two independent checkboxes on both Map and Live:
- "Geo filter boundary" -- unchanged behavior, the geo_filter/homeArea
  polygon (with buffer zone if configured).
- "Selected area outline" -- new, draws whichever area is currently
  picked via the Area dropdown (falls back to a box when the area has
  no polygon), checked by default whenever an area is selected.
2026-07-22 18:44:59 +02:00