mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 20:51:37 +00:00
22fe929da2
Implements #1727. ## What this adds **Mobile client-RX coverage** — an opt-in, crowdsourced RF-coverage feature. A roaming MeshCore **companion** radio (driven by the open-source [corescope-rx](https://github.com/efiten/corescope-rx) PWA, GPLv3) reports which nodes it heard directly, tagged with the phone's GPS and the packet's SNR/RSSI. CoreScope ingests these into a new `client_receptions` table and renders per-node **hex coverage** on the Reach page, plus a standalone **Coverage dashboard** (`#/rx-coverage`) with a top-mobile-observers leaderboard. Also includes **`GET /api/nodes/resolve?prefix=<hex>`** — a read-only node-name lookup by pubkey prefix (`{name, pubkey, ambiguous}`), used by the companion app for friendly names. ## Opt-in — default OFF (zero impact on existing deployments) The whole feature is gated behind one config flag, **disabled by default**: ```jsonc "clientRxCoverage": { "enabled": false } ``` When disabled (the default): the ingestor writes **no** `client_receptions`; the three coverage endpoints return a clean **404**; the UI hides the Coverage nav link, the `#/rx-coverage` route, and the Reach-page toggle. `/api/nodes/resolve` is always available (not coverage-specific). ## How it works ``` companion ──BLE 0x88 (snr+rssi+raw)──▶ corescope-rx PWA ──▶ MQTT meshcore/client/{pubkey}/packets │ ingestor (gated) ──▶ client_receptions (GPS + SNR + heard-key) │ server: pure-Go hex grid ──▶ GeoJSON ──▶ Reach hex overlay + Coverage dashboard ``` - **Direct-only capture:** records only what the companion heard itself and directly — a 0-hop advert's pubkey, or `path[last]` (last forwarder) for FLOOD routes; ≥2-byte path-hash required. Upstream hops discarded. - **No new deps:** hexbins are a pure-Go pointy-top grid over Web Mercator (`cmd/server/hexgrid.go`) computed at query time (`CGO_ENABLED=0` / `modernc.org/sqlite` friendly); frontend uses the existing Leaflet. - **Trust:** companion pubkey = identity; an EMQX ACL binds each client to publish only to its own `meshcore/client/{pubkey}/packets` topic. Payload contract in `docs/client-rx-coverage.md`. ## How to enable / try it 1. In `config.json`, set `"clientRxCoverage": { "enabled": true }` and restart server + ingestor. 2. Point an EMQX (or any broker) listener so a client can publish to `meshcore/client/<pubkey>/packets`; the ingestor already subscribes under `meshcore/#`. 3. Run the [corescope-rx](https://github.com/efiten/corescope-rx) PWA on an Android phone paired (BLE) to a MeshCore companion — it captures heard nodes + GPS and publishes. 4. View results: per-node Reach page → toggle **coverage**, or the **Coverage** dashboard at `#/rx-coverage`. ## What's where - **Ingestor:** `cmd/ingestor/client_reception.go` (ingest), `db.go` (`client_receptions` + `client_observers` schema), `main.go` (gated dispatch), `config.go` (flag). - **Server:** `cmd/server/rx_coverage.go` + `rx_dashboard.go` (endpoints, self-guard 404 when off), `hexgrid.go` (pure-Go grid), `node_resolve.go` (resolve), `routes.go` / `types.go` / `config.go` (wiring + flag + `/api/config/client` field). - **Frontend:** `public/rx-coverage.js` (dashboard), `node-reach-coverage.js` + `.css` (overlay), `node-reach.js` (Reach toggle, flag-gated), `roles.js` (reads the flag, hides nav when off). - **Docs:** `docs/client-rx-coverage.md`. ## Testing - Go: `cd cmd/server && go test ./...` and `cd cmd/ingestor && go test ./...` — green, including new gate tests (`coverage_gate_test.go` in both: off → no rows / 404, on → works) and the rx-coverage / resolve / hexgrid suites. - JS: `node test-coverage-gate.js`, `node test-node-reach-coverage.js` (wired into CI). The Playwright `test-node-reach-coverage-e2e.js` is wired into the e2e job and **skips when `clientRxCoverage` is disabled**, so it's safe under the default-off config. ## Notes for reviewers - The four new routes are registered in `cmd/server/openapi_known_gaps.json` (the existing OpenAPI-completeness ratchet), matching how other not-yet-spec'd routes are tracked. Happy to write full OpenAPI spec entries instead if you prefer. - Commits are split per layer (ingestor / server endpoints / resolve / frontend / CI) for review. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Erwin Fiten <e.fiten@opteco.be>
42 lines
2.6 KiB
CSS
42 lines
2.6 KiB
CSS
/* Client-RX coverage styles (fork-only feature). Kept in a DEDICATED file —
|
||
separate from node-reach.css — so upstream's periodic node-reach.css rewrites
|
||
don't drop the coverage tier colours + leaderboard layout (as happened in the
|
||
v3.9.0 merge). Consumed by rx-coverage.js (standalone Coverage dashboard) and,
|
||
once re-grafted, the per-node Reach page coverage overlay. */
|
||
|
||
/* RX coverage hex layer (mobile client receptions) */
|
||
:root {
|
||
--nq-cov-strong: #2ecc71; /* SF8: SNR ≥ −5 dB (good margin) */
|
||
--nq-cov-mid: #e67e22; /* SF8: −9..−5 dB (near the limit) */
|
||
--nq-cov-weak: #e74c3c; /* SF8: < −9 dB (poor, packet loss likely) */
|
||
--nq-cov-grey: #95a5a6; /* heard, no SNR metric */
|
||
}
|
||
/* Dark-theme variants: the saturated mid-luminance defaults glare on dark
|
||
basemaps. Mirrors the rest of the dashboard's theme-aware tokens (#polish). */
|
||
[data-theme="dark"] {
|
||
--nq-cov-strong: #3fb950;
|
||
--nq-cov-mid: #d29922;
|
||
--nq-cov-weak: #f85149;
|
||
--nq-cov-grey: #8b949e;
|
||
}
|
||
.nq-cov-legend { display:flex; gap:12px; align-items:center; font-size:11px; color:var(--text-muted, #57606a); margin:4px 0 10px; }
|
||
/* Toggled by node-reach.js applyCoverage via class, not inline style, so CSS
|
||
(print rules, themes) can still override visibility (#19). */
|
||
.nq-cov-legend.is-hidden { display:none !important; }
|
||
.nq-cov-legend i { width:12px; height:12px; border-radius:2px; display:inline-block; margin-right:4px; vertical-align:middle; }
|
||
|
||
/* Mobile RX coverage hub — leaderboard */
|
||
.rxb { display:flex; flex-direction:column; gap:3px; }
|
||
.rxb-row { display:flex; align-items:center; gap:10px; padding:7px 10px; background:var(--section-bg, #f6f8fa); border:1px solid var(--border, #d0d7de); border-radius:6px; font-size:13px; cursor:pointer; }
|
||
.rxb-row.rxb-head { font-size:10px; text-transform:uppercase; color:var(--text-muted, #57606a); cursor:default; }
|
||
.rxb-row.sel { outline:2px solid var(--accent, #2ecc71); }
|
||
/* Visible keyboard focus for the now-focusable (#polish) leaderboard rows. */
|
||
.rxb-row[data-rx]:focus-visible { outline:2px solid var(--link, #0969da); outline-offset:1px; }
|
||
.rxb-rank { width:24px; text-align:right; color:var(--text-muted, #57606a); }
|
||
.rxb-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||
.rxb-rec, .rxb-nodes, .rxb-cells, .rxb-score { width:56px; text-align:right; font-variant-numeric:tabular-nums; }
|
||
/* Sortable column headers in the leaderboard head row. */
|
||
.rxb-head .rxb-sort { cursor:pointer; user-select:none; }
|
||
.rxb-head .rxb-sort:hover { color:var(--link, #0969da); }
|
||
.rxb-head .rxb-sort:focus-visible { outline:2px solid var(--link, #0969da); outline-offset:1px; }
|