mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-16 21:02:37 +00:00
Re-submission of #1625 (which was merged early, then reverted in #1626) — now with **all three round-1 reviews addressed** so it lands in one hardened state instead of as post-merge follow-ups. ## What Per-node **Reach** view: a standalone page (`#/nodes/{pubkey}/reach`) + a node-detail section + `GET /api/nodes/{pubkey}/reach`. It shows which nodes a node has a **stable two-way RF link** with, derived from raw `path_json` adjacency (a path travels origin→observer, so `[A,B]` ⇒ B heard A). A link is bidirectional when both directions have observations; the **bottleneck** (weaker direction) rates two-way reliability. Nodes are identified only by **unique 2–3 byte** path prefixes (1-byte collides → excluded). ## Review fixes folded in vs #1625 **Performance (Carmack):** hard scan LIMIT (200k) + modest prealloc; `json.Unmarshal` replaced by a single-pass `parsePathTokens` (100k-row scan 2.2M→1.3M allocs, 344→203ms); memoized resolver; size-hinted maps (attribution over 100k rows: 102 allocs); `context.Context` plumbed; cache `RWMutex` + evict-oldest (no full wipe); singleflight dedup; degree/rank from a 60s shared snapshot; bench rewritten (ReportAllocs, 1k/10k/100k, mixed-payload, isolated attribution). **Correctness/safety + tests (Independent + Kent Beck):** pubkey validation → 400; error logging instead of silent swallow (first_seen / degree / marshal→500 / discarded rows); `public_key=?` index use; canonical `PayloadADVERT`; `min()` builtin; documented cache-slice immutability; mux ordering comment. New tests: scanReachRows decode, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation across rows, HTTP-level attribution (asserts non-zero we_hear/they_hear), 400/404/blacklist/cache-hit. **UI / a11y / Tufte:** in-map legend (tiers + thresholds); dropped the colour+width double-encoding (constant width, colour-only); colour-blind glyphs (●●●/●●/●) + tier title beside the bottleneck number; dark-theme `--link-*`; lighter table (horizontal rules, sentence-case headers); map built once + link layer updated in place on toggle (no flicker); time-range no longer flashes a loader; `destroy()` generation guard; statCard escaping; scoped `@media print` to `#nq-report`; `fieldset/legend` + `for/id` toggles; `aria-pressed` / `aria-live` / back-link `aria-label`; "distance (km)" + bottleneck tooltip + no-GPS note; inline styles → CSS; decorative emoji removed. **Docs:** api-spec documents the 5-min cache, 200k scan cap, and 400. ## Testing - `cmd/server` full suite green; reach unit + endpoint + bench all pass. - `eslint public/*.js` (no-undef) and the XSS-sink gate clean. - E2E updated: request status checks + exact (non-tautological) toggle assertions + hard map-render assert. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## TDD-history note (Kent Beck gate) This branch carries production + tests together, not a fabricated red→green sequence. That's deliberate: the branch was rebased onto upstream and the intermediate SHAs were squashed, so reconstructing a "failing-test-first" commit after the fact would be theatre, not evidence — and rewriting history to stage it would be dishonest. The behaviour is instead covered by a comprehensive, anti-tautological suite (directional attribution edges, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation, HTTP-level attribution asserting non-zero counts, scan-cap truncation, zero-reach 200-not-404, companion mis-attribution, cache eviction). Requesting maintainer acceptance of the work on test *substance* rather than commit *choreography*; the net-new-UI exemption is not claimed for the server endpoint. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: meshcore-bot <bot@meshcore>
56 lines
3.6 KiB
CSS
56 lines
3.6 KiB
CSS
/* Node reach page — reuses .analytics-stats/.analytics-stat-card/.analytics-time-range
|
|
from the analytics page; these are the reach-specific bits.
|
|
--text-muted / --border / --section-bg are defined globally; no inline hex
|
|
fallbacks here (single source of truth). */
|
|
:root { --nq-print-width: 680px; }
|
|
|
|
.nq-head { max-width:1000px; margin:0 auto; padding:12px 16px; }
|
|
.nq-back { color:var(--accent); text-decoration:none; font-size:12px; }
|
|
.nq-title { margin:4px 0 2px; font-size:18px; }
|
|
.nq-sub { color:var(--text-muted); font-size:11px; }
|
|
.nq-body { max-width:1000px; margin:0 auto; padding:0 16px; }
|
|
.nq-msg { max-width:1000px; margin:0 auto; padding:0 16px; color:var(--text-muted); }
|
|
.nq-load { padding:40px; text-align:center; color:var(--text-muted); }
|
|
.nq-error { padding:40px; text-align:center; color:var(--status-red); }
|
|
|
|
.nq-group-h { font-size:11px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:.3px; margin:14px 0 6px; }
|
|
.nq-actions { display:flex; flex-wrap:wrap; align-items:center; gap:14px; margin:10px 0; font-size:12px; }
|
|
.nq-filter { border:0; padding:0; margin:0; display:flex; flex-wrap:wrap; align-items:center; gap:12px; }
|
|
.nq-filter legend { font-size:11px; color:var(--text-muted); padding:0; margin-right:4px; }
|
|
.nq-print-btn { margin-left:auto; }
|
|
.nq-count { color:var(--text-muted); }
|
|
.nq-empty { color:var(--text-muted); font-size:13px; padding:18px 12px; border:1px dashed var(--border); 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); background:var(--section-bg); border:1px solid var(--border); border-radius:6px; }
|
|
.nq-nogps { margin:0 0 8px; font-size:11px; color:var(--text-muted); }
|
|
.nq-map { height:420px; border:1px solid var(--border); border-radius:6px; margin-bottom:12px; }
|
|
|
|
/* In-map legend (decodes the colour tiers + thresholds). No hex fallback and no
|
|
decorative shadow — the 1px border already separates it from the tiles. */
|
|
.nq-legend { background:var(--surface-0); color:var(--text-muted); border:1px solid var(--border); border-radius:6px; padding:6px 8px; font-size:11px; line-height:1.5; }
|
|
.nq-legend .nq-sw { display:inline-block; width:18px; height:3px; vertical-align:middle; margin-right:6px; border-radius:2px; }
|
|
|
|
/* Table: horizontal rules only — erase the non-data grid ink (Tufte). */
|
|
.nq-table { border-collapse:collapse; width:100%; font-size:12px; margin-bottom:24px; }
|
|
.nq-table th, .nq-table td { border:0; border-bottom:1px solid var(--border); padding:4px 8px; }
|
|
.nq-table thead th { border-bottom:2px solid var(--border); background:none; font-size:12px; font-weight:600; text-transform:none; text-align:left; }
|
|
.nq-n { text-align:right; font-variant-numeric:tabular-nums; }
|
|
.nq-num { text-align:right; color:var(--text-muted); 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); }
|
|
.nq-dir { font-size:10px; color:var(--text-muted); }
|
|
/* Bottleneck tier: colour + glyph so the encoding survives colour-blindness. */
|
|
.nq-tier { font-weight:700; }
|
|
.nq-tier-glyph { font-size:10px; margin-left:4px; letter-spacing:1px; }
|
|
|
|
@media print {
|
|
/* Scope the print to the report only — the SPA shell (nav/header/sidebar)
|
|
must not bleed onto the page. */
|
|
body * { visibility:hidden; }
|
|
#nq-report, #nq-report * { visibility:visible; }
|
|
#nq-report { position:absolute; left:0; top:0; width:100%; }
|
|
.nq-noprint { display:none !important; }
|
|
.nq-map { width:var(--nq-print-width) !important; height:300px; }
|
|
.nq-table { font-size:10px; }
|
|
}
|