mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-03 14:59:31 +00:00
## 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>
48 lines
2.1 KiB
JavaScript
48 lines
2.1 KiB
JavaScript
/* window.NodeReachMap.render(containerId, node, links, colorFn) — focused
|
|
Leaflet map of a node and its bidirectional links, coloured by bottleneck.
|
|
Returns the Leaflet map instance (with _nqBounds) so the caller can resize
|
|
for printing. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
function cssColor(varExpr) {
|
|
var name = varExpr.replace('var(', '').replace(')', '').trim();
|
|
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
return v || '#888';
|
|
}
|
|
|
|
function render(containerId, node, links, colorFn) {
|
|
var c = document.getElementById(containerId);
|
|
if (!c || typeof L === 'undefined') return null;
|
|
var map = L.map(containerId, { zoomControl: true, attributionControl: false })
|
|
.setView([node.lat, node.lon], 11);
|
|
if (typeof window._applyTilesToNodeMap === 'function') {
|
|
window._applyTilesToNodeMap(map);
|
|
} else {
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
|
|
}
|
|
var pts = [[node.lat, node.lon]];
|
|
links.forEach(function (l) {
|
|
if (l.lat == null || l.lon == null) return;
|
|
pts.push([l.lat, l.lon]);
|
|
var col = cssColor(colorFn(l.bottleneck));
|
|
L.polyline([[node.lat, node.lon], [l.lat, l.lon]], {
|
|
color: col,
|
|
weight: Math.max(1.5, Math.min(7, 1.2 + 1.6 * Math.log10(l.bottleneck + 1))),
|
|
opacity: 0.85
|
|
}).addTo(map).bindPopup(escapeHtml(l.name) + '<br>we ' + l.we_hear + ' / they ' + l.they_hear);
|
|
// Neighbour dot: filled with the link colour (was white/invisible before).
|
|
L.circleMarker([l.lat, l.lon], { radius: 5, color: '#ffffff', weight: 1, fillColor: col, fillOpacity: 1 })
|
|
.addTo(map).bindTooltip(escapeHtml(l.name));
|
|
});
|
|
// The node itself: a default Leaflet pin marker — always clearly visible.
|
|
L.marker([node.lat, node.lon]).addTo(map).bindPopup(escapeHtml(node.name));
|
|
try { map.fitBounds(pts, { padding: [30, 30] }); } catch (e) {}
|
|
map._nqBounds = pts;
|
|
setTimeout(function () { map.invalidateSize(); }, 120);
|
|
return map;
|
|
}
|
|
|
|
window.NodeReachMap = { render: render };
|
|
})();
|