mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-25 09:13:36 +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>
56 lines
1.6 KiB
Go
56 lines
1.6 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// benchReachDB builds an in-memory DB with nObs observations whose path
|
|
// contains the "01FA" token, for benchmarking scanReachRows.
|
|
func benchReachDB(b *testing.B, nObs int) *DB {
|
|
b.Helper()
|
|
conn, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
schema := []string{
|
|
`CREATE TABLE transmissions (id INTEGER PRIMARY KEY, hash TEXT, first_seen TEXT, payload_type INTEGER, from_pubkey TEXT)`,
|
|
`CREATE TABLE observers (id TEXT PRIMARY KEY, name TEXT)`,
|
|
`CREATE TABLE observations (id INTEGER PRIMARY KEY, transmission_id INTEGER, observer_idx INTEGER, snr REAL, path_json TEXT, timestamp INTEGER)`,
|
|
`CREATE INDEX idx_obs_ts ON observations(timestamp)`,
|
|
}
|
|
for _, s := range schema {
|
|
if _, err := conn.Exec(s); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
tx, _ := conn.Begin()
|
|
tx.Exec(`INSERT INTO observers (id, name) VALUES ('OBS', 'o')`)
|
|
for i := 0; i < nObs; i++ {
|
|
tx.Exec(`INSERT INTO transmissions (id, hash, first_seen, payload_type, from_pubkey) VALUES (?,?,?,5,'')`,
|
|
i, fmt.Sprintf("h%d", i), "2026-06-07T00:00:00Z")
|
|
tx.Exec(`INSERT INTO observations (id, transmission_id, observer_idx, snr, path_json, timestamp) VALUES (?,?,1,-7.0,?,?)`,
|
|
i, i, `["AA","01FA","BB"]`, 1000)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
return &DB{conn: conn}
|
|
}
|
|
|
|
func BenchmarkNodeReachScan(b *testing.B) {
|
|
db := benchReachDB(b, 5000)
|
|
srv := &Server{db: db}
|
|
tokens := map[string]bool{"01FA": true}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rows := srv.scanReachRows(tokens, 0)
|
|
if len(rows) == 0 {
|
|
b.Fatal("expected rows")
|
|
}
|
|
}
|
|
}
|