mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-29 00:18:22 +00:00
feat(nodes): per-node Reach page + GET /api/nodes/{pubkey}/reach (directional link quality) (#1625)
## What
Adds a per-node **Reach** view that answers "how well does this specific
node hear, and get heard by, its neighbours?" — both as a standalone
page (`#/nodes/{pubkey}/reach`) and as a section on the node detail
page.
New endpoint: **`GET /api/nodes/{pubkey}/reach`**.
## What it measures
For the target node it derives, from raw `path_json` adjacency (a path
travels origin→observer, so in `[A,B]` B received A directly):
- **Directional link counts** per neighbour: `we_hear` (how often we
received them) vs `they_hear` (how often they received us).
- **Bidirectional / bottleneck**: a link is two-way stable when both
directions > 0; the weaker direction is the bottleneck and rates real
two-way reliability.
- **Importance**: neighbour degree + rank, relay-observation volume,
bidirectional-link count, direct-observer count.
- **Direct observers**: who received the node at 0 hops, with SNR.
Reliability rule: a neighbour is only attributed when its pubkey
**prefix is unique** at the path's byte length (collisions are skipped,
never misattributed).
## UI
- Standalone Reach page + node-detail section.
- Reusable bidirectional link map (OSM) with links coloured by
bottleneck.
- Incoming/outgoing toggles to isolate each direction.
## Naming note (deliberate, no collision)
This is distinct from the existing **per-observer reachability** in
topology analytics (`ReachNode` / `ObserverReach` / `perObserverReach`).
This PR adds its own `NodeReach*` response structs in a new
`node_reach.go` and a new `/api/nodes/{pubkey}/reach` route — there are
no symbol or route collisions (verified: `go build ./...` clean). Happy
to rename to disambiguate further (e.g. "Link Quality") if you'd prefer
to reserve "Reach" for the per-observer feature.
## Testing
- `cmd/server`: endpoint shape/404/limit-clamp + unit tests for token
derivation and directional attribution, plus a scan benchmark — all
pass.
- Frontend: helper tests + Reach-page E2E (`test-node-reach-e2e.js`),
standalone route + incoming/outgoing toggles.
- `go build ./...` and `eslint public/*.js` (no-undef) clean.
## Docs
Design spec, implementation plan, and the `GET
/api/nodes/{pubkey}/reach` API contract are included under `docs/`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
efd6464204
commit
47f85f6c4c
@@ -23,6 +23,7 @@
|
||||
- [GET /api/nodes/:pubkey/health](#get-apinodespubkeyhealth)
|
||||
- [GET /api/nodes/:pubkey/paths](#get-apinodespubkeypaths)
|
||||
- [GET /api/nodes/:pubkey/analytics](#get-apinodespubkeyanalytics)
|
||||
- [GET /api/nodes/:pubkey/reach](#get-apinodespubkeyreach)
|
||||
- [GET /api/packets](#get-apipackets)
|
||||
- [GET /api/packets/timestamps](#get-apipacketstimestamps)
|
||||
- [GET /api/packets/:id](#get-apipacketsid)
|
||||
@@ -672,6 +673,63 @@ Per-node analytics over a time range.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/nodes/:pubkey/reach
|
||||
|
||||
Per-node RF reach report (two-way link quality). Computes **directional** link counts from raw
|
||||
path adjacency (a flood path is recorded origin→observer, so in `[A,B]` B received
|
||||
A directly). A link is **bidirectional** when both directions have observations;
|
||||
the **bottleneck** (weaker direction) rates two-way stability. Read-only; bounded
|
||||
to a recent window. Identifies nodes only by **unique 2–3 byte** path prefixes
|
||||
(1-byte prefixes collide and are excluded).
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|--------|--------|---------|--------------------------------------|
|
||||
| `days` | number | `7` | Lookback window, clamped 1–30 |
|
||||
|
||||
### Response `200`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"node": { "pubkey": string, "name": string, "role": string,
|
||||
"lat": number | null, "lon": number | null, "first_seen": string (ISO) },
|
||||
"window": { "days": number, "since": string (ISO) },
|
||||
"reliable_tokens": [string], // uppercase hex prefixes unique to this node ([] if unidentifiable)
|
||||
"importance": {
|
||||
"neighbor_degree": number, // all-time, from neighbor_edges
|
||||
"degree_rank": number, // 1-based rank among nodes with edges
|
||||
"nodes_with_edges": number,
|
||||
"relay_observations": number, // windowed obs with this node anywhere in path
|
||||
"bidirectional_links":number,
|
||||
"direct_observers": number
|
||||
},
|
||||
"direct_observers": [
|
||||
{ "pubkey": string, "name": string, "count": number,
|
||||
"avg_snr": number | null, "lat": number | null, "lon": number | null,
|
||||
"distance_km": number | null }
|
||||
],
|
||||
"links": [
|
||||
{ "pubkey": string, "name": string, "role": string,
|
||||
"lat": number | null, "lon": number | null,
|
||||
"we_hear": number, "they_hear": number,
|
||||
"bottleneck": number, "bidir": boolean,
|
||||
"distance_km": number | null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`reliable_tokens: []` means the node has no unique 1–3 byte prefix and cannot be
|
||||
reliably identified in paths; `links`/`direct_observers` will be empty.
|
||||
|
||||
### Response `404`
|
||||
|
||||
```json
|
||||
{ "error": "Not found" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /api/packets
|
||||
|
||||
Paginated packet (transmission) list with filtering.
|
||||
|
||||
Reference in New Issue
Block a user