Files
meshcore-analyzer/public/node-reach.css
T
efiten 47f85f6c4c 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>
2026-06-08 13:11:06 +02:00

25 lines
1.7 KiB
CSS

/* Node reach page — reuses .analytics-stats/.analytics-stat-card/.analytics-time-range
from the analytics page; these are the reach-specific bits. */
.nq-group-h { font-size:11px; font-weight:600; color:var(--text-muted, #57606a); text-transform:uppercase; letter-spacing:.3px; margin:14px 0 6px; }
.nq-actions { display:flex; align-items:center; gap:14px; margin:10px 0; font-size:12px; }
.nq-count { color:var(--text-muted, #57606a); }
.nq-empty { color:var(--text-muted, #57606a); font-size:13px; padding:18px 12px; border:1px dashed var(--border, #d0d7de); border-radius:6px; margin:6px 0 24px; }
.nq-note { margin:6px 0 0; padding:6px 10px; font-size:11px; line-height:1.4; color:var(--text-muted, #57606a); background:var(--section-bg, #f6f8fa); border:1px solid var(--border, #d0d7de); border-radius:6px; }
.nq-map { height:420px; border:1px solid var(--border, #d0d7de); border-radius:6px; margin-bottom:12px; }
.nq-table { border-collapse:collapse; width:100%; font-size:12px; margin-bottom:24px; }
.nq-table th, .nq-table td { border:1px solid var(--border, #d0d7de); padding:3px 7px; }
.nq-table th { background:var(--section-bg, #f6f8fa); font-size:10px; text-transform:uppercase; }
.nq-n { text-align:right; font-variant-numeric:tabular-nums; }
.nq-num { text-align:right; color:var(--text-muted, #8c959f); width:26px; }
.nq-link { color:var(--accent); text-decoration:none; font-weight:600; }
.nq-link:hover { text-decoration:underline; }
.nq-oneway { color:var(--text-muted, #8c959f); }
.nq-dir { font-size:10px; color:var(--text-muted, #8c959f); }
@media print {
.nq-noprint { display:none !important; }
.nq-map { width:680px !important; height:300px; }
.nq-table { font-size:10px; }
}