mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-04 18:51:47 +00:00
master
2813 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
096e16409c |
fix(#1741): wrap test-DB insert loops in a single transaction (#1819)
## Fixes #1741 `TestBoundedLoad_OldestLoadedSet` (and any test building a 5000-row fixture) hung/timed out, blocking reliable `go test ./cmd/server` and CI. ## Root cause The four test-DB builders in `cmd/server/bounded_load_test.go` (`createTestDBAt`, `createTestDBWithObs`, `createTestDBWithAgedPackets`) inserted rows in a loop with no `BEGIN`/`COMMIT`. With the pure-Go `modernc.org/sqlite` driver every `Exec` auto-commits → one fsync per row → ~2N fsyncs for N transmissions (tx + obs). At `numTx=5000` that's ~10k fsyncs and the fixture blows past the test timeout. Sibling tests with `numTx<=3000` happened to stay under the timeout, so only the 5000-row cases visibly hung. ## Fix Wrap each insert loop in a single `BEGIN`/`COMMIT` so the whole fixture build becomes one commit. Fixtures now finish in well under a second regardless of `numTx`; the tests' actual assertions (`oldestLoaded` set, newest-first ordering, bounded load) are exercised instead of the timeout masking them. Also made the prepared-statement `Exec` calls check their error (previously discarded) so a failed insert surfaces instead of silently leaving the DB short. No production code changed — test infrastructure only. ## Verified - `TestBoundedLoad_OldestLoadedSet`: **0.18s** (was: 30s timeout / FAIL). - Full `TestBoundedLoad*` + retention group: passes in ~1.2s. - `go test ./...` in `cmd/server`: exit 0 (no longer blocks on this test). Co-authored-by: Waydroid Builder <build@waydroid.local> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6a32ec2b2d |
fix(#1729): preserve firmware-default Public channel (0x11) in analytics (#1817)
## Fixes #1729 The firmware-default **Public** channel (channel-hash byte `0x11` = 17) was rendered as an opaque **"Encrypted (0x11)"** row at the bottom of the analytics Channels tab, despite the key being well-known and builtin. ## Root cause `computeAnalyticsChannels` applied the #978 rainbow-table validation (`SHA256(SHA256("#name")[:16])[0]`, the **hashtag** hash scheme) to every decoded channel name. The Public channel is a **PSK** channel whose hash byte is key-derived (`SHA256(key)[0]` = 17), not hashtag-derived (`186` for `#Public`). So the ingestor-decoded name `"Public"` failed the hashtag check and was discarded, the row forced to `encrypted=true, name="ch17"`. ## Fix Trust the ingestor's `decryptionStatus`. The ingestor already persists `decryptionStatus:"decrypted"` when it decoded a packet with a real key (PSK), and `"no_key"` / `"decryption_failed"` otherwise. When the packet is `decrypted`, skip the hashtag hash check and keep the name — it came from a key-based decryption, not a rainbow-table lookup. The #978 mismatch rejection still applies to non-decrypted packets, so rainbow-table collisions are still caught. Frontend needs no change: `encrypted=false, name="Public"` lands in the "Network" group (top), not "Encrypted". ## Tests - `makeGrpTx` gains `makeGrpTxWithStatus` companion to set `decryptionStatus`. - `TestComputeAnalyticsChannels_PublicChannelPreserved`: hash 17 / "Public" / `decrypted` → name stays `"Public"`, `encrypted=false`. - `TestComputeAnalyticsChannels_UndecryptedNameStillValidated`: a non-`decrypted` name failing the hashtag check is still downgraded to `ch17` (#978 regression guard). All channel-analytics tests pass; `go build ./...` clean. Co-authored-by: Waydroid Builder <build@waydroid.local> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
750b8742a7 |
fix(staging-compose): decouple in-container mosquitto from standalone broker (#1813)
Red commit: `3898dbc5` (verified locally — CI run URL pending)
## Problem
A standalone `mqtt-broker` container (`eclipse-mosquitto:2`) was
provisioned out-of-band on the staging VM. It now owns MQTT, is attached
to external docker network `meshcore-net`, and binds host port `8883`.
The current `docker-compose.staging.yml` still:
- Publishes `1883:1883` on the host (dead weight; conflicts the moment
the broker moves to that port).
- Defaults `DISABLE_MOSQUITTO=false`, so the in-container mosquitto
burns RAM and briefly contests the `mqtt-broker` docker DNS name on cold
start.
- Doesn't join `meshcore-net`, so the ingestor can't resolve
`mqtt-broker:1883` via docker DNS without manual surgery.
## Fix (`docker-compose.staging.yml` only)
1. Remove the `1883:1883` host port publish from `staging-go`.
2. Flip `DISABLE_MOSQUITTO` default from `false` to `true`. Operators
can opt back in with the env var.
3. Attach `staging-go` to both `default` and `meshcore-net`; declare
`meshcore-net` as `external: true` so the file never tries to
create/destroy operator state.
Healthcheck and Caddy/443 plumbing untouched (out of scope).
## Test added (TDD framing: Option A — Go shape-asserts)
`cmd/server/staging_compose_broker_test.go:1` adds four regex-based
assertions on the compose file shape:
- staging-go does **not** bind port `1883` in ANY form (quoted/unquoted
short form, or long-form `target: 1883` / `published: 1883`).
- `DISABLE_MOSQUITTO` uses the interpolated default form
`${DISABLE_MOSQUITTO:-true}` (preserves operator override). Bare literal
`true`, or a later `=false` override in the same env block, is rejected.
- Top-level `networks:` declares `meshcore-net` as `external: true`.
- `staging-go` attaches to `meshcore-net` via a real
`services.staging-go.networks:` sub-key (comment-stripped so an
in-comment example can't masquerade).
Regex (not YAML byte-equality) so cosmetic edits don't break the guard.
No new go module deps. Red commit `3898dbc5` fails all 4 assertions on
master. Green commit `38297ff4` makes them pass. Round-1 hardening
commit `9f7155e2` tightens the regexes (per adversarial + kent-beck
must-fixes) and was verified against master's YAML shape — all 4 tests
fail on `origin/master`'s compose, pass on branch, proving the tightened
regexes still gate a real regression.
## Risk
Low, with one intentional semantic change.
- **Semantic change (v3.7+):** `DISABLE_MOSQUITTO` in
`docker-compose.staging.yml` now defaults to `true`. This is a
**deliberate flip** — the standalone `mqtt-broker` container is now
authoritative on the staging host, and running the in-container
mosquitto alongside it wastes RAM and races the docker DNS name
`mqtt-broker` on cold start. Operators who want the pre-v3.7 shape
(in-container mosquitto + host-published `1883`) must explicitly opt
back in via env override AND re-add the `1883:1883` port mapping
(concrete snippet is inline in the compose file and in `DEPLOY.md` under
"Standalone MQTT broker (staging)"). This intent is called out in a
`SEMANTIC CHANGE (v3.7+)` header comment at the top of
`docker-compose.staging.yml`.
- **Deploy prereq:** the external `meshcore-net` docker network MUST
already exist on the host before `docker compose up`. If it doesn't,
compose refuses to start `staging-go`. This is documented inline in the
compose file (with the `docker network create meshcore-net` one-liner)
and in `DEPLOY.md`.
- **Only takes effect where the standalone broker is deployed** — which
it already is on staging today. The legacy `DISABLE_MOSQUITTO=false`
path remains reachable via env override; the ingestor's upstream config
is untouched.
Partial fix — no tracking issue; follow-up to operator-side broker
provisioning.
---------
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
fa15ab0a30 |
fix(#1809): gate background loader on LoadChunked completion (#1811)
Partial fix for #1809. Red commit: |
||
|
|
242c7c609b |
fix(mqtt): escalate persistent paho disconnect + recover from emit panic + expose watchdog tick (#1749) (#1810)
# Partial fix for #1749 — MQTT watchdog escalation + panic recovery +
tick exposure
Red commit:
|
||
|
|
b74a64ccfa |
fix(ui): canonical payload label map across packets/live/packet-filter (#1799) (#1804)
## Summary Replaces the three drifted per-surface payload-type label vocabularies with a single canonical map keyed by firmware enum name. Per the locked triage comment on #1799 ([comment-4823975431](https://github.com/Kpa-clawbot/CoreScope/issues/1799#issuecomment-4823975431)): > Create `public/payload-labels.js` exporting `{GRP_DATA: {short:'Group Data', long:'Group data packet', enumId:6}, ...}`. Migrate `packets.js typeMap`, `packet-filter.js FW_PAYLOAD_TYPES`, `live.js TYPE_COLORS legend` to consume it. E2E that scrapes each surface and asserts label equality. ## Changes - **`public/payload-labels.js`** (new) — canonical map exposed as `window.PayloadLabels` and `window.PayloadLabelsApi`. Keys are firmware enum names; values carry `{short, long, enumId}` plus derived `SHORT_BY_ID` / `FW_PAYLOAD_TYPES` / `TYPE_ALIASES` for legacy callers. - **`public/packets.js`** — `TYPE_NAMES` + `typeMap` now read from `PayloadLabelsApi.SHORT_BY_ID`. Literal kept only as a defensive fallback for the case where the script tag fails to load. - **`public/packet-filter.js`** — `FW_PAYLOAD_TYPES` + `TYPE_ALIASES` now sourced from `PayloadLabelsApi`. Literal fallback retained so `node test-packet-filter.js` still works headlessly. - **`public/live.js`** — legend `<li>` rows are now generated from `window.PayloadLabels` in stable order, killing the third-vocabulary `Message — Group text` / `Direct — Direct message` drift the #1797 review surfaced. - **`public/index.html`** — `<script src="payload-labels.js">` loaded before `roles.js` / `packet-filter.js` / `packets.js`. - **`test-issue-1799-label-vocab-e2e.js`** (new) — Playwright E2E. Scrapes `#liveLegend` rows and the `/packets` type-filter checklist, asserts each label matches `window.PayloadLabels[ENUM].short` for `TXT_MSG`, `GRP_TXT`, `GRP_DATA`. Also verifies `window.PacketFilter` still recognises the enum names. - **`.github/workflows/deploy.yml`** — wired the new E2E into the existing Playwright block. ## TDD trail - Red commit `eb392d4` — adds the failing E2E only (asserts `window.PayloadLabels` exists and labels match; both fail). - Green commit `44e902a` — introduces the canonical map and migrates the three surfaces. ## Verification - `node test-packet-filter.js` — 92/92 pass with the new fallback wiring. - Preflight: `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean. Browser verified: E2E `test-issue-1799-label-vocab-e2e.js` exercises `/live` legend + `/packets` type filter against a Playwright headless Chromium; CI's Playwright block runs it on every push. E2E assertion added: `test-issue-1799-label-vocab-e2e.js:139` — `assert(fromLegend === canon, ...)` and `assert(fromPackets === canon, ...)` per enum. Fixes #1799 --------- Co-authored-by: mc-bot <bot@corescope> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@kpa.com> Co-authored-by: clawbot <bot@clawbot.local> |
||
|
|
4654ce3386 |
feat(analytics): "My Repeaters" favorites monitoring dashboard (#1761)
My Repeaters monitoring dashboard. Closes #1765. --------- Co-authored-by: Waydroid Builder <build@waydroid.local> |
||
|
|
30e4151f7a |
fix: neighbor-graph tab never renders after filtering down (#1758)
The Analytics → Neighbor Graph tab fetches the full (uncapped) graph and, when it exceeds NODE_LIMIT (1000), skips the force simulation with a "use filters to reduce the node count" notice. But filtering never actually re-enabled rendering: - the node-count guard tested _ngState.allNodes (the immutable full fetched set, assigned once in createGraphState and never reassigned) instead of the displayed/filtered _ngState.nodes, so its verdict was fixed at load time; - the entire draw loop lives in startGraphRenderer(), which ran exactly once at load and was never called from applyNGFilters(), so a filter change updated the node/edge arrays and stat cards but never un-hid the canvas or scheduled an animation frame -> the graph stayed blank no matter how few nodes remained. This explains both reported symptoms (selects too many nodes initially AND stays broken once restricted to fewer). Fix: make the render lifecycle filter-aware. - startGraphRenderer() now guards on the displayed set (_ngState.nodes), cancels any running rAF loop before re-deciding, toggles the canvas plus a stable-id "skipped" notice, and restarts cleanly (no double loops). - applyNGFilters() calls startGraphRenderer() so every filter change re-evaluates the guard and (re)starts or stops the loop. - the initial render now goes through applyNGFilters() so the first paint already respects the default filters (observers unchecked, saved min-score) instead of dumping the full fetched graph. Test: `node --check public/analytics.js` passes. Manually: open Analytics → Neighbor Graph on a mesh with >1000 nodes → the "skipped" notice shows; tighten filters (min-score up / roles off) below 1000 → the graph now renders (was blank before); loosen again → notice returns. Frontend-only change (`public/analytics.js`); no backend/API change. --- **TDD note (review round 1):** Single-commit community bug-fix on an existing UI surface (no "net-new UI" exemption). The e2e `test-issue-1758-ng-filter-rerenders-e2e.js` is the red→green gate — it fails on `origin/master` (the renderer kept the node-count guard on the full fetched graph and never un-hid the canvas) and passes with the fix. Per AGENTS.md the separate red/green-commit *form* is a bot rule, not a contributor gate. --------- Co-authored-by: Waydroid Builder <build@waydroid.local> Co-authored-by: Waydroid Builder <claude@michael.arcan.de> |
||
|
|
9ae547ed7b |
test: de-flake distance-202 and anchor-bias tests (deterministic timing) (#1808)
Two server tests flaked intermittently and reddened CI on unrelated (frontend) PRs that merged master: - TestDistanceConcurrentRequestsDuringBuildReturn202 asserted all 10 concurrent requests get 202 'during the build window', but the lazy distance build on the tiny test DB finishes almost instantly, so on a fast machine some requests raced past it and got 200 (~50% flake). Add a nil-by-default distanceBuildHook seam on PacketStore (zero overhead in prod) that the test uses to hold the build open until all requests have been served — making the window guarantee deterministic. - TestHandleNodePaths_AnchorBiasInconsistency_Issue1278 queried /paths right after store.Load(), racing the path-hop index that Load() builds in a background goroutine (#1008); the membership/canonical result was thus non-deterministic (rarer flake, worse under suite load). Wait for PathHopIndexReady() before querying. Both run 30x green and pass -race. No production behavior change (hook is nil). Co-authored-by: Waydroid Builder <claude@michael.arcan.de> |
||
|
|
ec0ebeda2f |
fix(#1793): WebSocket CheckOrigin allowlist (block cross-origin scrapers) (#1795)
## Summary Closes the wide-open `/ws` WebSocket upgrader (`CheckOrigin: return true`) that lets any browser origin scrape live packet data. Replaces it with an explicit allowlist consulted from `cfg.CORSAllowedOrigins`, plus an implicit same-origin allowance and an empty-Origin (non-browser client) allowance. Fixes #1793. ## Rules (`Hub.checkOrigin`) - Empty `Origin` header → **allow** (non-browser clients; per-IP rate/deny gating tracked separately in #1794). - `Origin` host == request `Host` (case-insensitive) → **allow** (same-origin). - `Origin` matches an entry in `cfg.CORSAllowedOrigins` by exact case-insensitive match → **allow**. - `"*"` in `cfg.CORSAllowedOrigins` is **deliberately ignored** for `/ws`. A startup `[ws] WARNING:` is logged once when present. - Anything else → **reject** (gorilla returns 403). ### Deliberate divergence from CORS XHR CORS XHR (`corsMiddleware`) still honors `"*"` for read-only cross-origin GETs. The `/ws` upgrade does NOT, per OWASP's WebSocket Security Cheat Sheet: > Use an allowlist, not a denylist. Avoid wildcards or substring matching. — https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html `"*"` on the WS path would re-open the exact CSWSH/scraping vector this PR closes, so it is rejected with a startup warning rather than silently honored. This intentional asymmetry is documented in the updated `_comment_corsAllowedOrigins` in `config.example.json`. ## TDD red → green - `e5974c6a` **RED** — adds `cmd/server/websocket_checkorigin_test.go` with five cases; `SetAllowedOrigins` introduced as an enforcement stub so the test compiles and fails on the assertion (CI fails on this commit by design). - `a4791dc3` **GREEN** — implements `Hub.checkOrigin`, wires `SetAllowedOrigins` from `main.go`, updates the config example. All tests pass. ## Tests added (`cmd/server/websocket_checkorigin_test.go`) - `TestCheckOriginRejectsForeignOrigin` — foreign Origin → 403 - `TestCheckOriginAllowsEmptyOrigin` — non-browser client → 101 - `TestCheckOriginAllowsSameHost` — same-origin → 101 - `TestCheckOriginAllowsAllowlistedOrigin` — exact allowlist match → 101 - `TestCheckOriginWildcardDoesNotAllowForeignOrigin` — `"*"` in allowlist still rejects foreign origin → 403 ## Files changed - `cmd/server/websocket.go` — `Hub.allowedOrigins`, `SetAllowedOrigins`, `checkOrigin`, wired into `Upgrader.CheckOrigin`. - `cmd/server/main.go` — `hub.SetAllowedOrigins(cfg.CORSAllowedOrigins)` at the single call site. - `cmd/server/websocket_checkorigin_test.go` — new test file. - `config.example.json` — updated `_comment_corsAllowedOrigins` to document `/ws` gating and the `"*"` divergence. ## Out of scope (follow-up) - **#1794** — per-IP rate limit / deny list / connection cap for non-browser clients (which still bypass Origin because they don't send one). Layered defense; not in this PR. ## Verification - `go test ./cmd/server/...` — all server tests pass locally (574s). - Preflight clean (`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`). --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
ae2e3933dd |
feat(server): store memory diagnostics + drop redundant obs.RawHex (#1773)
Drops the redundant per-observation RawHex (~98MB on a live store; reader already falls back to tx.RawHex #881) and adds an opt-in /api/perf?mem=1 memory breakdown (flood-forward share + per-component bytes). Profiled against a live instance. **Savings substantiation:** live-instance profiling shows ~1.66M observations in the store, each previously carrying its own per-observation `raw_hex` (avg ≈118 hex chars ≈59 bytes) that exactly duplicates the parent transmission's `raw_hex`. Dropping the duplicate on every load/ingest path eliminates ≈98 MB of redundant in-memory storage plus ~1.66M string allocations, with no data loss — the read path (`enrichObs`) already falls back to `tx.RawHex` when `obs.RawHex` is empty (verified by the new safety-gate test). The patched build cannot be run against the live instance here; instead the new opt-in `/api/perf?mem=1` diagnostic lets operators measure the real before/after (`trackedMB` and the per-component breakdown) directly after deploy. |
||
|
|
707d70c738 |
fix(packets): clamp .col-details to one line on mobile (#1770 S path) (#1805)
## Summary Partial fix for #1770 (S quick-fix path only; L refactor remains as follow-up). The packets-view virtual-scroller assumes a constant `VSCROLL_ROW_HEIGHT`, but the base rule at `public/style.css` L1097 lets `td.col-details` wrap on narrow viewports (`white-space: normal; word-break: break-word`). Wrapped rows produce variable row heights → visible jitter when scrolling past ~900px on iOS. **Quick-fix (S path):** under the existing `@media (max-width: 640px)` block in `public/style.css`, clamp `.col-details` to a single line: ```css .data-table td.col-details { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ``` Trade-off accepted in triage: Details column truncates on mobile in exchange for smooth scrolling. The base rule keeps wrapping on desktop (≥641px) so nothing changes there. **Out of scope:** the full L-path fix (per-row measurement, `_rowHeightsPx[]`, cumulative offsets, re-measure on hop-resolver finalize) — tracked separately on #1770. ## TDD - **Red commit** `7f58bedc` — adds `test-issue-1770-mobile-row-clamp.js`, a CSS-grep test (same pattern as `test-issue-1364-pill-no-clamp.js`) that walks every `@media (max-width: 640px)` block in `public/style.css` and asserts a `.col-details` rule declares `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`. Verified to FAIL on master (assertion failure, not a parse error) and PASS after the CSS change. - **Green commit** `d46271b8` — applies the 5-line CSS clamp inside the existing mobile breakpoint at L2362. ## Files touched - `public/style.css` (+13) - `test-issue-1770-mobile-row-clamp.js` (+101, new) ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all gates pass (PII, branch scope, red commit, css-vars, css self-fallback, LIKE-on-JSON, sync migration, async-migration, XSS). No warnings. --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
b3189c613a |
fix(#1802): decode CONTROL DISCOVER_REQ/RESP subtype + body fields (#1806)
## Summary Extend CONTROL packet decoding to surface DISCOVER_REQ / DISCOVER_RESP subtype plus body fields in the packet detail view. Previously only the byte0 zero-hop flag was decoded; the body was rendered as opaque hex. ## What changed **Backend** — `cmd/ingestor/decoder.go` `decodeControl()` - New `Payload` fields (all omitempty): `CtrlSubtype`, `CtrlFilter`, `CtrlTag`, `CtrlSince`, `CtrlNodeType`, `CtrlSNR`, `CtrlPubKey`. - Subtype derived from `byte0 & 0xF0`: `0x80` → `DISCOVER_REQ`, `0x90` → `DISCOVER_RESP`, otherwise `UNKNOWN`. - REQ body parsed when `len(buf) >= 6`: `filter:u8 | tag:u32 LE`, plus optional `since:u32 LE` when 4 more bytes remain. - RESP body parsed when `len(buf) >= 6`: `node_type` (low nibble of byte0), `snr:i8`, `tag:u32 LE`, and `pubkey` hex — 32 bytes when full, 8 bytes when prefix-only. - Every field gated on length; short/truncated bodies emit subtype only and never panic. - `CtrlZeroHop` retained for backwards compatibility (rename flagged for follow-up per triage). **Frontend** — `public/packets.js` `getDetailPreview()` - New `decoded.type === 'CONTROL'` branch renders subtype + present body fields (filter / tag / since / node_type / snr / pubkey). Each field shown only when populated, so truncated CONTROL still gets a subtype label. ## Wire format reference - `firmware/src/Mesh.cpp:69` — `CTL_TYPE_NODE_DISCOVER_REQ=0x80`, `CTL_TYPE_NODE_DISCOVER_RESP=0x90`. - `firmware/examples/simple_repeater/MyMesh.cpp:773-820` — body parse / build. ## Tests (red → green, per AGENTS.md STRICT TDD) - `cmd/ingestor/issue1802_test.go` — 6 cases: REQ full body (with since), REQ no-since, RESP 32B pubkey, RESP 8B prefix pubkey, RESP truncated pubkey (no panic, no pubkey emitted), short body (subtype only), unknown subtype. Red commit `43713d3a` → green commit `d4b28180`. Pre-existing CONTROL tests (`TestDecodeControlZeroHop`, `TestDecodeControlMultiHop`) still pass. - `test-packets.js` — 3 cases on `getDetailPreview`: DISCOVER_REQ (filter+tag rendered), DISCOVER_RESP (snr+pubkey rendered), UNKNOWN subtype label. Red commit `be23e349` → green commit `845d6c48`. ## Preflight overrides - `check-branch-clean` (cross-stack): justified — issue #1802 explicitly spans backend decoder (`cmd/ingestor/decoder.go`) and frontend renderer (`public/packets.js`) per triage comment. Tests in both layers. Single-purpose PR. ## Scope discipline Files touched: `cmd/ingestor/decoder.go`, `cmd/ingestor/issue1802_test.go`, `public/packets.js`, `test-packets.js`. No other files. No firmware changes. No `cmd/server/decoder.go` changes. No `CtrlZeroHop` rename (deferred per triage). Fixes #1802 --------- Co-authored-by: clawbot <bot@meshcore.local> |
||
|
|
120ac052d3 |
fix(packets): add Multipart/Control/Raw Custom to type filter checklist (#1798) (#1803)
## Summary Fixes #1798. Extends the Packets-page `typeMap` in `public/packets.js` to include three firmware payload types that were previously missing from the multi-select checklist: - `10` — Multipart - `11` — Control - `15` — Raw Custom Other surfaces (`public/packet-filter.js` `FW_PAYLOAD_TYPES`, `public/live.js` `TYPE_COLORS`, `public/map.js`) already knew about these types; only the Packets-page checklist UI omitted them, forcing operators to hand-type filter expressions to filter on them. ## Red → green - Red commit: `359e3645ac41506e563c19dfbd49983fb4ec9638` — adds E2E that opens `#typeMenu` and asserts each new `data-type-id="10|11|15"` checkbox renders with the exact label. Fails on assertion (DOM selectors return null) against the pre-fix `typeMap`. - Green commit: `f484e8cb88659b14fed7aa7fefcdfb3f0eb6c186` — single-line literal extension; test goes green. ## E2E assertion added `test-e2e-playwright.js:576` — `Packets type filter includes Multipart/Control/Raw Custom (#1798)` (asserts the three new `data-type-id` checkboxes render with their exact labels in the rendered Packets-page checklist DOM). ## Files touched - `public/packets.js` — extend `typeMap` literal - `test-e2e-playwright.js` — new E2E test asserting the three checkboxes render ## Browser verified E2E test scrapes the rendered Packets-page DOM via Playwright; CI runs it against the local Go server fixture in the `e2e-test` job. Fixes #1798 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
3efa37c46c |
feat(server): complete the #672 4-axis repeater usefulness score (#1762)
Adds Coverage (harmonic reach) + Redundancy (Tarjan articulation) axes + composite & grade. Closes #672. **TDD note (BLOCKER-1):** Community PR delivered as a single squashed commit, so there is no separate pre-fix failing-test commit — please accept as a community-PR exemption. The tests are *gating*, not just thorough: each axis test pins a specific topology outcome (coverage on line/star/disconnected/weight-sensitive; redundancy online/triangle/star/bridged-cliques), and an end-to-end `/api/nodes` surface test drives the whole pipeline and asserts the composite diverges from the Traffic axis. Inverting the `1/weight` distance, dropping the NaN/Inf reject, removing the `redundancyMinWeight` floor, or aliasing `usefulness_score` back onto `traffic_share_score` each break a specific assertion. The axis functions are pure (no hidden state), so the suite fully characterises the behavior without the red anchor. Co-authored-by: Waydroid Builder <build@waydroid.local> |
||
|
|
5e096147e5 |
fix(#1800): add routed_through filter, clarify path, fix hex lexer error (#1801)
Fixes #1800. ## Three changes 1. **`routed_through` field** — new `FIELDS` entry. Resolves against `packet.resolved_path` (handles both the JSON-string form from `/api/packets/by-id` and the already-parsed-array form from `/api/packets`). Returns a space-joined lower-case hex string so `contains` / `starts_with` / `==` work the same way they already do for `hash`. 2. **`path` desc clarified + `path_prefixes` alias** — `path` desc now reads `Hop path as 1-byte prefixes joined (e.g. a3→7f). For pubkey search use routed_through.` `path_prefixes` is added as a discoverability alias and resolves to the same value. 3. **Lexer hex-token error** — when the number/duration tokenizer hits an unknown unit AND the slice (extended forward through any remaining `[0-9a-fA-F]`) is pure hex of length ≥ 4, the lexer now returns: ``` Hex value must be quoted: try 'field == "<hex>"' or use the starts_with/contains operator ``` instead of `Invalid duration unit 'f' at position N (expected s/m/h/d/w)`. The duration-unit error is preserved for non-hex cases (`age < 5x` still errors with the original message). ## TDD Red commit `e44ac00a` adds 7 assertions that fail with the unmodified code (proven by stashing the impl and re-running — output: `7 failed`). Green commit `7b623721` makes them pass. Tests added in `test-packet-filter.js` (`#1800: …`): - `routed_through starts_with "2f0b00"` matches packet with JSON-string `resolved_path` - same, against array-form `resolved_path` (handles real `/api/packets` shape) - `routed_through contains "<full-pubkey>"` matches - `routed_through contains "2f0b"` matches - `routed_through starts_with "deadbe"` does NOT match - `path 2f0b001247a047ca` → error contains `Hex value must be quoted` - regression: `path contains "a3"` still matches `path_json=["a3","7f"]` - `routed_through` listed in `FIELDS` - `path_prefixes` alias resolves like `path` `node test-packet-filter.js` → `=== Results: 92 passed, 0 failed ===` Sibling JS tests (`test-packet-filter-ux.js`, `test-packet-filter-time.js`, in-file self-tests) all green. ## Browser verification Browser tool was unavailable this session, so I executed `public/packet-filter.js` in a Node VM context (identical execution) and exercised it against a live `/api/packets?limit=200` response from staging: - `routed_through starts_with "41b1"` returned 1 matching packet (whose `resolved_path[0]` is `41b1eabc3c6e88997242051ee53fa5840761dff02ac5f6d9904f23985395ec31`) - `routed_through starts_with "bccf91"` matched a packet with that hop - `routed_through starts_with "deadbe"` matched nothing (correct) - `PF.suggest('routed_t', 8)` returned `['routed_through']` - `PF.suggest('route', 5)` returned `['route', 'routed_through']` - `PF.compile('path 2f0b001247a047ca').error` is verbatim: `Hex value must be quoted: try 'field == "<hex>"' or use the starts_with/contains operator` - `PF.compile('age < 5x').error` is still `Invalid duration unit 'x' at position 7 (expected s/m/h/d/w)` — duration-unit message preserved for non-hex cases. ## Out of scope (per issue) - No server-side filter pushdown. - No operator-list changes. - No `resolved_path` changes — it already ships on `/api/packets`, `/api/packets/by-id`, `/api/live`. --------- Co-authored-by: meshcore-bot <meshcore-bot@users.noreply.github.com> |
||
|
|
d5ceb27334 |
fix(#1792): decode GRP_DATA channel hash + inner data_type/len/blob in details cell (#1796)
Red commit:
|
||
|
|
770749a8ce |
fix(#1791): add 'Group Data' (payload_type=6) to packets type filter (#1797)
Fixes #1791. ## What Adds `6:'Group Data'` to the `typeMap` in `public/packets.js` so the Packets-view "message type" multi-select shows a Group Data checkbox. The filter pipeline already keys by integer payload_type, so this just registers the missing option. Also aligns the Live-view legend label in `public/live.js` to "Group Data" for cross-view consistency. ## Why Triage (in #1791) confirmed payload_type=6 (GRP_DATA) was the only ordinary type omitted from the static `typeMap`. `packet-filter.js`, `live.js`, `app.js`, and `map.js` all already know about it — only the Packets-page checklist was missing it. ## Test (TDD red → green) Branch history (4 production commits before round-1 review): - `19ed5beb` — **test-only red commit**: adds Playwright E2E that opens the type-filter menu, asserts a `data-type-id="6"` checkbox labeled "Group Data" exists, selects it, and asserts every visible row's type badge reads "Group Data". Also seeds one GRP_DATA packet into the CI fixture (`.github/workflows/deploy.yml`) so the filter has a row to match. - `823a7d8d` — adds the one-line `typeMap` entry. First CI run on this commit failed on an unrelated test (not the #1791 assertion); the #1791 test ran and passed. - `eec2428` — fixture cleanup: `path_json=[]`/`resolved_path=[]` so the seeded GRP_DATA hop-row count matches the raw_hex `path_len=0`. CI green. - `8f85f5f` — labels the type-6 entry "Group Data" (was briefly "Grp Data"). CI green. E2E assertion: `test-e2e-playwright.js` block `Packets type filter includes Group Data (#1791)`. ## Round-1 review follow-ups - `e3651c99` — `public/live.js` legend: `'Grp Data'` → `'Group Data'`. - `4475c2f7` — test cleanup hardening: error string aligned to assertion, duplicated selector extracted, regex tightened to strict equality, `#typeMenu` explicitly closed, `meshcore-time-window` localStorage key cleared, page reloaded so the in-memory `selectedTypes` Set is reset. - `b90bc33f` — `.github/workflows/deploy.yml`: drop self-referential `#1797` citation from fixture comment, switch synthetic fixture id from `-1` to `-1000000` sentinel with explanatory comment. ## Scope Single-line typeMap registration plus its E2E test scaffolding, fixture seed, and the live.js label alignment. --------- Co-authored-by: clawbot <bot@openclaw.dev> Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
17654dd090 |
docs(api): document per-node usefulness metrics in OpenAPI (#1769)
Documented Node schema (the four #672 usefulness axes + composite + A-F grade + relay fields) and response schemas on the node endpoints. Documentation-only; no behaviour change. Pairs with #1762 (documents the metrics it adds). Co-authored-by: Waydroid Builder <build@waydroid.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1adb0116b2 |
fix: don't re-render node dots when scrubbing the Live timeline (#1754)
## What Scrubbing the Live page timeline no longer re-renders all node dots. ## Why `vcrReplayFromTs()` ran `clearNodeMarkers()` (wiping `nodesLayer` and `nodeMarkers`) and then `loadNodes()` rebuilt every marker from scratch. A single scrub click destroyed and recreated the entire node layer; visible flicker plus unnecessary DOM work (even though `addNodeMarker()` already no-ops nodes that still exist). ## How - `vcrReplayFromTs()` now clears only the transient animation/path layers. - The time-scoped branch of `loadNodes()` reconciles against the existing markers: removes only nodes absent at the target time, adds genuinely new ones, leaves shared dots untouched. ## Testing - `test-live.js`: 95/95 pass - `node --check public/live.js` clean |
||
|
|
fc26fb6b3a |
feat(#1751): show transported region scopes in repeater sidebar (#1752)
Closes #1751. |
||
|
|
c03f2ebbcc |
live: cap animation-canvas DPR at 1.5 and redraw at ~60fps (#1737)
The live-map animation overlay re-clears and re-draws its full backing store every animation frame. Two unbounded multipliers make that expensive: 1. devicePixelRatio is uncapped in updateAnimCanvas(). The canvas is already ~1.4x the screen area (20% pad per side), so at DPR 2-3 it allocates and fills 5-12x the screen's pixels per frame. Cap at 1.5 — lines stay crisp, per-frame fill cost drops up to ~4x on hi-DPI displays. 2. renderAnimations() reschedules via rAF with no rate limit, so on 120/144Hz displays it does 2-2.4x the work for no visible gain. Add a ~60fps guard. Progress is time-based (tickDt, itself capped at 32ms), so skipping frames preserves motion exactly. Paused frames fall through to the existing sleep. No behavior change on a standard 60Hz / 1x-DPI display. Existing animation tests (test-live-dt-cap-1524, test-live-anims) unaffected. Co-authored-by: Michael <claude@michael.arcan.de> Co-authored-by: efiten <erwin.fiten@gmail.com> |
||
|
|
9757178aad |
fix(#1789): add Firmware + Client columns to observers table (#1790)
## Summary Adds **Firmware** and **Client** columns to the observers table (`#/observers`). Both values already come back from `/api/observers` (`firmware`, `client_version`) — they were just never rendered. Fleet operators have been asking to sort/scan firmware versions to coordinate upgrades. Closes #1789. ## Changes - `public/observers.js` - Two new `<th data-priority="4" data-sort-key="...">` headers (Firmware, Client). Priority 4 matches Clock Offset / Uptime so `TableResponsive` hides them first on narrow viewports. - Two new `<td class="mono">` cells with `data-value="${escapeHtml(raw)}"` for sort and the rendered text escape-wrapped. - `truncateBuildSuffix()` helper trims the long `" Build: ..."` tail from firmware in the displayed text; the full string is preserved in `title=` for hover. - `test-issue-1789-observer-firmware-cols.js` — TDD red→green static-source regression test (same pattern as `test-observers-headings.js`). - `test-observers-headings.js` — updated expected heading list with the two new columns (existing #1039 invariant test). - `test-all.sh` — wires the new test into CI. ## TDD evidence - Red: `c6c4e594c1084d664730666ba069871ac6d9755c` — test commit fails on assertions (not import errors): 5/6 cases fail because the headers/cells/title attr don't yet exist; the column-count invariant still passes because both thead and tbody are unmodified. - Green: `02a0246a185950c903828ac1225d6795f5a3b2f4` — implementation; all 6 cases pass. ## Browser verified To be verified post-deploy on staging (`http://analyzer-stg.00id.net/#/observers`). No backend changes — purely additive frontend render of fields that are already on the wire. ## Perf No new API calls, no extra fetches, two extra template-literal cells per observer row (~10s of observers in prod). O(n) render unchanged. --------- Co-authored-by: clawbot <bot@meshcore.local> |
||
|
|
f0763aecce |
fix(#1726): clear stale "varies" hash size once a node settles (#1788)
Fixes #1726. ## Problem A MeshCore v1.16.0 repeater configured for 2-byte path hashes (`path.hash.mode=1`) — e.g. `36f6c7c7…` (`DK_3400_RAK_TEST`) — kept showing as **"varies"** / mixed 1-byte + 2-byte for the full 7-day advert window. Per the live data in the issue triage: of the node's ~20 recent adverts, exactly **one** (2026-06-09, across 15 distinct observer paths) was a genuine 1-byte flood advert; every other advert was 2-byte. The flip-flop heuristic in `computeNodeHashSizeInfo` weighs that stale advert equally with recent ones, so an operator who flips `path.hash.mode` mid-flight (or a single old 1-byte advert) stays flagged for the full window with no way to signal "the config is settled now." ## Fix Two coupled changes in `cmd/server/store.go` `computeNodeHashSizeInfo`: 1. **Chronological ordering.** `byPayloadType[4]` iterates in insertion order, not timestamp order, so `HashSize = Seq[last]` could pick the wrong advert under out-of-order MQTT ingest or chunked cold-load (the "carmack" concern from triage). We now collect `(FirstSeen, size)` pairs and **stable-sort by `FirstSeen`**; ties keep insertion order, preserving prior behavior when timestamps are equal. 2. **Recency decay.** After `transitions >= 2` raises the flip-flop flag, clear it when the most recent `hashSizeRecentAgreeCount` (= **3**) non-zero-hop adverts all agree on a single size. A node still flapping (recent adverts disagree) stays flagged. `3` mirrors the existing ≥3-observation threshold used to raise the flag. ## Policy note Triage marked this **needs-operator-input** because the decay is a behavior/policy change. This PR implements the rule the triage proposed ("if the last 3 adverts agree, clear inconsistent"), which matches the reporter's stated expectation. Happy to adjust the threshold or gate it differently per your call. ## Tests `cmd/server/issue1726_hash_decay_test.go`: - `TestIssue1726_SettledNodeNotInconsistent` — reporter's case (`[2,1,2,2,2]` within window) → `Inconsistent=false`, `HashSize=2`. - `TestIssue1726_HashSizeUsesChronologicallyLatest` — out-of-order insertion still reports the chronologically-latest size. - `TestIssue1726_ActiveFlapperStaysInconsistent` — a node whose recent adverts disagree stays flagged. Existing flip-flop / hash-collision tests unchanged and green; full `cmd/server` package suite passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Erwin Fiten <e.fiten@opteco.be> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d437958474 |
fix(map): pin APC (Napa) and STS (Sonoma) observers (#1786) (#1787)
Fixes the map-coordinate gap in #1786. ## Problem Observers tagged with IATA code **APC** (Napa County) or **STS** (Charles M. Schulz–Sonoma County) render with no location and never pin on the map. ## Root cause `iataCoords` in `cmd/server/routes.go` is a hardcoded `IATA -> lat/lon` lookup used purely for placing observer/region markers on the map. It had no entry for APC or STS, so those observers had no coordinates to render with. This is **display-only**. Ingestion is not gated on these codes: `IsObserverIATAAllowed` (`cmd/ingestor/config.go`) short-circuits to `true` when the observer IATA whitelist is empty — which is the staging configuration. The reporter''s "packets disappear entirely" symptom is therefore **not** explained by this code path (likely an upstream `meshcoretomqtt`/broker topic issue; needs operator `mosquitto_sub` confirmation per triage). ## Fix - Add `APC {38.2132, -122.2807}` and `STS {38.509, -122.8128}` to `iataCoords`, matching the airports'' published coordinates. - Add a regression test (`TestIataCoordsIncludesNapaAndSonoma`) asserting both are present with the expected coordinates. ## Verification - `go test ./cmd/server/` — full package passes (`ok`). - `go vet ./cmd/server/` — clean. ## Scope note Checked the repo for other statically-enumerable region codes (`config.example.json` regions: SJC/SFO/OAK/MRY) — all already covered. The broader "are other in-use codes missing" question can only be answered against the live `cfg.Regions` + `db.GetDistinctIATAs()` set, which is operational, not in-tree. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Erwin Fiten <e.fiten@opteco.be> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
55e203a9c8 |
fix(nav): surface Coverage route in mobile nav when enabled (#1783)
Fixes #1782. ## Problem When `clientRxCoverage` is enabled, the **Coverage** route (`#/rx-coverage`) is reachable from the desktop top-nav but **unreachable on mobile** — neither the bottom-nav "More" sheet (phones, ≤768px) nor the edge-swipe drawer (touch tablets, >768px) lists it. ## Root cause `public/roles.js` injects the Coverage link **only into the desktop top-nav** (`.nav-links`), gated on `window.MC_CLIENT_RX_COVERAGE`. The two mobile nav surfaces build their long-tail lists from **independent hardcoded arrays** that omitted `rx-coverage`: - `public/bottom-nav.js` → `MORE_ROUTES` - `public/nav-drawer.js` → `ROUTES` Both even carry `!! MANUAL SYNC REQUIRED !!` comments. Because the link is injected into the DOM (not these arrays) and is config-gated, it never reached mobile. ## Fix Both surfaces now insert the Coverage entry **right after Analytics** (matching the desktop top-nav insertion point) when `window.MC_CLIENT_RX_COVERAGE` is true. The check is evaluated at **lazy build time** (first sheet/drawer open), by which point `MeshConfigReady` has resolved the flag. Default-off behaviour is unchanged, so the default nav still matches the existing nav-overflow tests. ## Testing Adds `test-rx-coverage-mobile-nav-e2e.js`, which: - skips cleanly when Chromium is unavailable (`CHROMIUM_REQUIRE=1` makes it a hard fail) or when `clientRxCoverage` is disabled — mirroring `test-node-reach-coverage-e2e.js`; - at 360px asserts Coverage is present in the bottom-nav More sheet, ordered after Analytics, and that tapping it navigates to `#/rx-coverage`; - at 1024px asserts Coverage is present in the edge-swipe drawer, ordered after Analytics. Verified locally against a server built from this branch with `clientRxCoverage` enabled (migrated `test-fixtures/e2e-fixture.db`): - new test: **3/3 pass**; reverting the two source files makes it **fail 3/3** (true regression test); - existing nav e2e suites still green: `test-nav-drawer-1064-e2e.js` (11/11), `test-bottom-nav-1061-e2e.js` (31/31), `test-nav-more-floor-1139-e2e.js` (10/10). ## Notes - No perf impact: the route list is built once, lazily, on first sheet/drawer open. - The hardcoded `MORE_ROUTES` / `ROUTES` arrays remain the source of truth for the always-on routes; this only conditionally appends the one opt-in route, consistent with how `roles.js` already gates the desktop link. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5c0de8fb41 |
feat(live): optional "Multibyte only" view filter (#1780) (#1781)
Closes #1780. ## What Adds an opt-in **"Multibyte only"** toggle to the live map controls. When ON, packets whose path hash size is `< 2` bytes (single-byte, or unresolvable) are excluded from the entire live view — feed, map polylines/rain, and the packet counter — in both LIVE and REPLAY modes. - **Default OFF** — no behavior change for existing users. - Persisted in `localStorage` under `live-multibyte-only`. - Distinct from the existing global "hide 1-byte path hops" toggle: that filters individual hops within a path at every render site; this filters whole packets, on the live view only. They share no state. ## How - **`public/hop-filter.js`** — new pure, dependency-free classifier `MC_packetHashSize(rawHex, routeType)` returning `1|2|3`, or `0` when unresolvable. Reads the path-length byte from `raw_hex` (`(pathByte >> 6) + 1`), offset `5` for transport routes (route_type 0/3) else `1` — mirroring the existing `getPathLenOffset`/`computeBreakdownRanges` logic in `app.js`. Lives next to the existing `hopByteLen`/`MC_*` family; `app.js` is untouched (no duplication of the byte math). - **`public/live.js`** — `groupIsMultibyte(packets)` consumes that helper; applied at two render-time sites: the top of `renderPacketTree` (above the counter increment, so the counter reflects multibyte-only) and inside the `rebuildFeedList` group loop (so toggling re-filters the buffered feed). Toggle markup + change handler mirror the existing `liveFavoritesToggle` pattern. ## Why read from `raw_hex` and not the path hops The hash size is a property of the whole packet and is present even for zero-hop packets (where there are no hops to inspect), so reading the path-length byte is correct in all cases. Unresolvable size is treated as single-byte (excluded when ON) — we only show packets we can positively confirm are multibyte. ## Performance (hot path) The filter runs in the packet-render hot path, so: classification is **O(1) per packet group** — it reads the first resolvable observation's `raw_hex` (a short hex string, single `parseInt` of one byte) and short-circuits. No per-packet API calls, no allocation in the loop, no added O(n²). When the toggle is OFF (default) the check is a single boolean guard and does nothing else. The buffered-feed re-filter reuses the existing `rebuildFeedList` pass — no extra traversal. ## Tests - **Unit** (`test-live-multibyte-filter.js`, 9 cases): single/2-byte/3-byte classification, transport-route offset, missing/short/garbage `raw_hex` → 0, whitespace tolerance. - **E2E** (`test-live-multibyte-only-e2e.js`, Playwright): toggle present and defaults OFF; ON hides a single-byte packet while a multibyte one renders; OFF restores it; setting persists across reload. Registered in the CI live-E2E block in `deploy.yml`. ## Docs User-guide entry added in `docs/user-guide/live.md`. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
57956712e7 |
fix(#1768): Relay Airtime Share uses LoRa Time-on-Air (preamble-aware) — partial fix (#1776)
Partial fix for #1768 — Relay Airtime Share now uses closed-form LoRa Time-on-Air instead of a payload-bytes-only proxy, removing the ~3-4× bias against small frames (preamble + fixed-symbol intercept). cross-stack: justified — backend score formula needs a frontend caption change (`public/analytics.js` dumbbell preset banner + tooltip) so operators can interpret the assumed PHY block. Both move together or the metric is misleading. ## Red commit `8da57062` — failing test asserts ToA-based score (~83.48 % ADVERT share on the locked acceptance fixture) instead of the byte proxy's 95.24 %. `internal/lora.TimeOnAir` was a zero-returning stub at the red commit; tests failed with assertion errors, not build errors. ## Green commit `dd402edd` — implements `lora.TimeOnAir` (Semtech AN1200.13 / SX126x §6.1.4 closed form, cross-checked against RadioLib), wires `score = TimeOnAir(payloadBytes, preset) × distinctRelays` in `cmd/server/relay_airtime_share.go`, surfaces the preset in the JSON response and analytics caption. ## Config (per AGENTS Config Documentation Rule) New keys under existing `analytics` block: ```json "loraPreset": { "freq": 869600000, "bw": 62.5, "sf": 8, "cr": 5 } ``` Defaults match the deployment's actual `get radio` (869.6 MHz / BW 62.5 kHz / SF 8 / CR 4/5). `CRC=1`, `IH=0`, `DE = (T_sym ≥ 16 ms)`, and the SF-dependent preamble (32 for SF≤8 else 16, per firmware `preambleLengthForSF` / MeshCore PR #1954) are firmware-fixed constants in `internal/lora/toa.go` and intentionally NOT surfaced as config (per re-triage). ## Scope In-scope files (6): - `internal/lora/toa.go` (new package — closed-form ToA) - `internal/lora/toa_test.go` (table-driven preset tests) - `cmd/server/relay_airtime_share.go` (wire ToA into score) - `cmd/server/relay_airtime_share_test.go` (recomputed expected values) - `cmd/server/config.go` + `config.example.json` (preset config keys) - `public/analytics.js` (preset caption on dumbbell chart + tooltip) Plus `cmd/server/go.mod` (replace directive for the new internal module). ## Deferred to v2 (separate issues per re-triage) - Per-observation SF/BW + radio-settings-aware dedup (blocked: ingestor stores SNR/RSSI only, no SF/BW on observations). - CR-per-hop dual-point sensitivity band (CR scales only the payload symbol term `(CR+4)`, not the preamble/header; second-order accuracy gain). - Cross-SF bridge accounting. ## Tests ``` cd internal/lora && go test ./... → PASS cd cmd/server && go test -run RelayAirtime → PASS ``` ## Preflight overrides - `check-branch-clean` (cross-stack): justified above — score formula change requires matching caption update; both files trace to the same issue. --------- Co-authored-by: kpa-clawbot <kpa-clawbot@users.noreply.github.com> Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: bot <bot@meshcore> |
||
|
|
b3b8bec5ec |
feat(filter): expose payload.destHash + payload.srcHash in filter autocomplete (#1774) (#1775)
Resolves #1774. ## What Adds `payload.destHash` and `payload.srcHash` to the filter autocomplete suggestions array in `public/packet-filter.js`. ## Why The decoder already emits `destHash` and `srcHash` JSON keys for `REQ` / `RESPONSE` / `TXT_MSG` / `PATH` / `ANON_REQ` packets (see `cmd/ingestor/decoder.go`), and the generic `payload.*` accessor in the filter language (`packet-filter.js:296-308`) already evaluates these fields correctly — i.e. `payload.destHash == "2f"` has always worked. The gap was purely autocomplete + docs: the two names were missing from the `FIELDS` (SUGGESTIONS) array, so operators discovering filters via the suggestion popup couldn't find them. Per the triage on #1774, the canonical names match the decoder's serialization (`destHash` / `srcHash`), not the reporter's proposed `dest` / `src` — so the filter token agrees with the raw-JSON view and the packet detail's `Dest Hash (1B)` label. ## Tests - Red commit (`test-packet-filter.js`): asserts `filter('payload.destHash == "2f"')` matches a fake REQ packet AND that `FIELDS` contains entries named `payload.destHash` / `payload.srcHash`. Fails on the FIELDS assertion only (filter() already works). - Green commit: adds the two entries. All 83 packet-filter tests pass. ## Scope Single-file change in `public/packet-filter.js` (+2 lines) + 23 lines of test coverage. No decoder changes, no backend changes, no API signature changes. Fixes #1774. --------- Co-authored-by: clawbot <bot@corescope> |
||
|
|
72d451221c |
tone down naive-clock observer notice (#1478 follow-up) (#1759)
Red commit:
|
||
|
|
735d9eb516 |
fix(#1715): dark-theme role swatches via per-theme CSS tokens (#1757)
## Summary Dark-theme variants of the neighbor-graph role swatches (`#ngRoleChecks` labels on `/analytics?tab=neighbor-graph`) still failed WCAG AA after #1720's light-theme fix because the swatches used inline `style="color:#..."` from `customize.js` `DEFAULTS.nodeColors` (palette-700) — bypassing the theme tokens entirely. Measured before: | Role | Color | vs `#1a1a2e` (dark) | |---|---|---| | repeater | `#dc2626` | 3.53:1 ❌ | | companion | `#2563eb` | 3.30:1 ❌ | | observer | `#8b5cf6` | 4.02:1 ❌ | ## Fix - Defines `--role-{repeater,companion,room,sensor,observer}` in `:root` (palette-700, ≥4.5:1 on white) and overrides them in both dark blocks (`[data-theme="dark"]` + the `@media (prefers-color-scheme: dark)` mirror) with palette-400/500 shades that clear AA on `#1a1a2e`. - Refactors the neighbor-graph swatch DOM in `public/analytics.js` from inline `style="color:${hex}"` to class-based `<span class="role-swatch role-swatch--{role}">`, with matching CSS rules that read the tokens. - Removes all 5 `#1715` entries from `tests/a11y-allowlist.yaml` per the issue's acceptance criteria. After: | Role | Light (vs `#fff`) | Dark (vs `#1a1a2e`) | |---|---|---| | repeater | `#dc2626` 4.83:1 | `#ef4444` 4.53:1 | | companion | `#2563eb` 5.17:1 | `#3b82f6` 4.64:1 | | room | `#15803d` 5.02:1 | `#16a34a` 5.18:1 | | sensor | `#b45309` 5.02:1 | `#d97706` 5.35:1 | | observer | `#7c3aed` 5.70:1 | `#a78bfa` 6.27:1 | ## Tests - `test-a11y-1715-dark-role-swatches.js` — CSS-driven WCAG AA probes for the 5 per-theme `--role-*` tokens plus markup invariants (no inline color span; class names present in `#ngRoleChecks` block). - Red commit: `a09ec21c` — fails on assertion with 12 below-threshold/markup probes. - Green commit: `f87dcd64` — all probes PASS. - `tests/a11y-allowlist.yaml` shed all 5 entries; the umbrella `test-a11y-axe-1668.js` (CI) is now the live-browser net for those cells. ## Preflight overrides - `check-xss-sinks.sh` flags `public/analytics.js:2502` (label "observer" appears in an `innerHTML=\`tpl\`` line). The flagged token is a hardcoded literal string — no user-controlled data flows into that template. No template content changed in this PR; the flag is preexisting noise from the heuristic scan and the gate ultimately marks ✅ pass. Fixes #1715 --------- Co-authored-by: clawbot <clawbot@kpa.local> Co-authored-by: clawbot <bot@example.com> |
||
|
|
e465e1c6c6 |
perf(#1740): replace idx_tx_last_seen with partial index WHERE last_seen=0 (#1756)
Fixes #1740. ## What Replace the full `idx_tx_last_seen` with a partial index `WHERE last_seen=0`. Two ordered, sequential migrations in `internal/dbschema/dbschema.go::ensureTransmissionsLastSeenColumn`: - **(a)** `CREATE INDEX IF NOT EXISTS idx_tx_last_seen_zero ON transmissions(id) WHERE last_seen=0` - **(b)** `DROP INDEX IF EXISTS idx_tx_last_seen` — gated after (a) succeeds (sequential `Exec`; (b) never runs if (a) errors) ## Why The only consumer is `chunkedTxLastSeenBackfill`'s `WHERE last_seen=0` scan + `MAX(id)` lookup. The full index covers ALL rows including the long tail where `last_seen != 0` after backfill converges (71K+ rows in prod per carmack's #1740 note). The partial index degenerates to ~the count of un-backfilled rows (0 in steady state, bounded by ingest rate during ops) and stops competing for page cache. ## Migration cost Both migrations are sync and annotated `PREFLIGHT: async=false`: - (a) `CREATE INDEX` on partial subset `WHERE last_seen=0` is bounded by un-backfilled rows — a small superset of the inflight ingest window, not a full table scan. - (b) `DROP INDEX` is a metadata-only schema rewrite in SQLite — no row scan at any size. `dbschema/` has no access to `Store.RunAsyncMigration` (that helper lives in `cmd/ingestor/` and is the wrong layer for the schema source-of-truth per #1321), so sync is the only path here regardless. ## TDD - **RED** `a529f0f4`: `EXPLAIN QUERY PLAN` test asserting the backfill `MAX(id) WHERE last_seen=0` query uses `idx_tx_last_seen_zero` + a second test asserting the legacy `idx_tx_last_seen` is dropped post-Apply. Both failed on assertion (planner picked the full index; partial index didn't exist). - **GREEN** `208fde8a`: add migrations (a) and (b). Both tests pass. ## Files touched - `internal/dbschema/dbschema.go` — swap the index - `internal/dbschema/dbschema_test.go` — `EXPLAIN QUERY PLAN` pin + DROP assertion - `cmd/ingestor/db.go` — comment refresh only (idx_tx_last_seen → idx_tx_last_seen_zero) ## Acceptance - ✅ New partial index created - ✅ Old full index dropped via gated migration (sequential, order preserved) - ✅ Query plan test asserts partial-index usage - ✅ Existing migration tests still green (`internal/dbschema`, `cmd/ingestor` TestIssue1690 + applySchema) --------- Co-authored-by: clawbot <bot@openclaw> |
||
|
|
db5520f70f |
fix(nodes): copy URL buttons produce malformed origin#frag URLs (#1753) (#1755)
## Problem The **Copy URL** and **Copy short URL** buttons on the node detail page produced URLs like: ``` https://analyzer.00id.net#/nodes/abcdef… ``` The `/` between the authority and the fragment is missing. RFC 3986 allows that form, but several mobile browsers (and some link-detection heuristics) reject or mis-parse it. ## Fix Three sites in `public/nodes.js` concatenated `location.origin` with a literal that started with `'#/'`. Prepend `/`: - `public/nodes.js:772` — full Copy URL (full pubkey) - `public/nodes.js:783` — Copy short URL (8-char prefix) - `public/nodes.js:1580` — side-pane Copy URL All three now build `https://analyzer.00id.net/#/nodes/…`, which every browser accepts. ## Tests `test-issue-1753-copy-url-slash.js` — extracts every `location.origin + '<literal>'` site from `public/nodes.js` and asserts the literal starts with `/`. Wired into `.github/workflows/deploy.yml`. - **Red commit** `b4df2786` — test added; CI fails on assertion (3 of 4 cases) because the literals still start with `'#/'`. - **Green commit** `2c59f7c0` — three literals fixed to `'/#/nodes/'`; test passes (4/4). ## Verification ``` $ node test-issue-1753-copy-url-slash.js issue-1753 copy-URL slash regression ✅ found at least 3 location.origin + literal sites in public/nodes.js ✅ public/nodes.js:772 literal starts with "/#/" (got "/#/nodes/") ✅ public/nodes.js:783 literal starts with "/#/" (got "/#/nodes/") ✅ public/nodes.js:1580 literal starts with "/#/" (got "/#/nodes/") 4 passed, 0 failed ``` `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → clean (all gates + warnings green). Fixes #1753 --------- Co-authored-by: openclaw-bot <bot@openclaw.dev> |
||
|
|
f780fe7d0b |
ci: bump Go test timeout 15m -> 20m (server + ingestor) (#1750)
## Problem The server Go suite runs ~13–15m against a **15m** `go test -timeout`, so on slower CI runners it intermittently hits `panic: test timed out after 15m0s` (`cmd/server`, e.g. `db_test.go`) — false-red CI that a plain rerun clears. Observed on PR #1728 (15m25s on the passing attempt — right at the ceiling). ## Fix Bump both `go test` invocations (`cmd/server` and `cmd/ingestor`) from `-timeout 15m` to `-timeout 20m` for headroom. No test or application code changes — CI workflow only. ## Verification Workflow-only change; this PR's own CI is the confirmation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Erwin Fiten <e.fiten@opteco.be> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0765c2cc69 |
fix(#1718): drop prefix-tool a11y allowlist entries — subsumed by #1720 (#1736)
## Summary PR #1720 (merged 2026-06-13) consolidated active button states onto the shared `.btn-active-accent` rule that paints `background: var(--accent-strong)` (`#2563eb`) + `color: var(--text-on-accent)` (`#f9fafb`) = **4.95:1**, WCAG AA pass in both themes. That subsumes the `#ptCheckBtn` / `#ptGenBtn` color-contrast violations issue #1718 tracked, so the allowlist entries are stale. ## Change Drop the two `issue: 1718` entries from `tests/a11y-allowlist.yaml`: ```yaml - route: '/analytics?tab=prefix-tool' selector: '#ptCheckBtn' rule: color-contrast issue: 1718 expires_at: 2026-09-11 - route: '/analytics?tab=prefix-tool' selector: '#ptGenBtn' rule: color-contrast issue: 1718 expires_at: 2026-09-11 ``` No other tabs touched. `#1715` dark-theme work and other `expires_at: 2026-09-11` entries are out of scope — separate issues, separate PRs. No production CSS/JS modified (PR #1720 did the substantive fix). ## Verification The CI a11y gate (`test-a11y-axe-1668.js`) is the authoritative check. It re-renders `/analytics?tab=prefix-tool` in dark+light × desktop+ mobile and asserts zero net violations against the trimmed allowlist. With this PR the entries are gone — if PR #1720's fix were ever reverted, the gate fails immediately with no allowlist masking it. Local repro not attempted: sandbox chromium lacks the `@axe-core/playwright` module (matches the documented limitation in PR #1730 / PR #1723). CI is the source of truth for this gate. ## TDD note Config-change exemption per workspace AGENTS.md: - No test files modified. - No production code modified. - Config-only allowlist trim; CI must stay green without test edits. - The gate itself is the test — dropping the allowlist entries IS the red→green transition (entries gone → axe runs unfiltered → must remain pass because #1720 fixed the root cause). Mirrors the exact pattern accepted in PR #1722 (clock-health), PR #1723 (subpaths), PR #1730 (nodes), and PR #1731 (rf-health) — same allowlist-drop shape, same upstream PR #1720 fix. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — config-only change; PII grep on diff clean. Fixes #1718. Refs PR #1720, PR #1722, PR #1723, PR #1730, PR #1731. Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: efiten <erwin.fiten@gmail.com> |
||
|
|
aadc182d0c |
docs: correct README license label MIT → GPL-3.0-or-later (#1744) (#1746)
Fixes #1744 ## Summary README's License section advertised `MIT`, but the repo's `LICENSE` file is GNU GPL v3 (`GNU GENERAL PUBLIC LICENSE / Version 3, 29 June 2007`). Updated the README label to the SPDX identifier `GPL-3.0-or-later` so it matches the actual license text. ## Change - `README.md`: `MIT` → `GPL-3.0-or-later` (single line) ## TDD exemption Pure docs change, no behavior. Per `AGENTS.md` TDD section: *"Pure docs / pure comments: no test required. Kent Beck gate still runs, rubber-stamps with 'no behavior change' justification."* No production code, no tests touched. ## Out of scope The reporter also flagged two other items; per triage these are explicitly out of scope here: - Root-directory clutter — already tracked by #1385. - Missing "About" page — needs its own issue; not addressed in this PR. Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: efiten <erwin.fiten@gmail.com> |
||
|
|
22fe929da2 |
feat: opt-in mobile client-RX coverage (crowdsourced RF reach) + /api/nodes/resolve (#1728)
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> |
||
|
|
df28efaed9 |
docs(agents): contributor onboarding pack for AI-driven workflows (#1734)
## What Adds `docs/agents/` — an onboarding pack for external contributors using their own AI coding agent (Claude Code, Codex, Cursor, Aider, OpenClaw, etc.). ## Why Maintainers run an agent-driven workflow against this repo. External contributors using agents benefit from the same discipline (TDD red→green, PII preflight, parallel persona polish, three-axis merge readiness) but had nothing portable to point at. This documents the **process** and the **reusable building blocks** in an agent-agnostic way. ## Contents ``` docs/agents/ README.md WORKFLOW.md # pipeline + planning + PII preflight + force-push + worktrees RULES.md # 36 hard-won discipline rules TDD.md # red→green requirement, exemptions SUBAGENT-BRIEF-TEMPLATE.md skills/ # 14 task playbooks (intake, fix, polish, merge-gate, release, ops...) personas/ # 14 review voices (carmack, dijkstra, torvalds, meshcore, taleb, ...) ``` ## Scope Docs-only. No code changes. Existing `AGENTS.md` is unchanged. All committed text uses sanitized placeholders (`<workspace>`, `<repo>`, `YOUR_NAME`, `YOUR_HANDLE`, etc.) — no personal names, phones, IPs, keys, or absolute home/root paths. ## Verification - PII preflight grep on staged diff: only matches are the literal placeholders inside the documented sanitized example (`YOUR_NAME|YOUR_HANDLE|...|api[_-]?key|...`). - Off-topic skill grep on `docs/agents/`: clean (zero hits for the wrong-language/off-topic skill names that were scrubbed from the prior attempt). --------- Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: efiten <erwin.fiten@gmail.com> |
||
|
|
bdf5f647d4 |
fix(ci): freshen all e2e-fixture observation timestamps (unblocks #1630 reach e2e) (#1747)
## Problem
`test-issue-1630-reach-mobile-e2e.js` has been failing on `master` since
~2026-06-16, in its precondition `pickRepeaterWithReach` ("no repeater
with reach links found in fixture") — not in any of its actual
assertions. The same SHA passed on 06-15 and failed on 06-16 with no
code change, i.e. it tracks the wall clock, not the tree.
## Root cause
`tools/freshen-fixture.sh` shifts `nodes`, `transmissions`, `observers`
and `neighbor_edges` timestamps to ~now, but for
`observations.timestamp` it only rewrote rows where `timestamp = 0 OR
timestamp IS NULL`. Real-timestamped observations stayed frozen at
fixture-capture time.
Per-node reach (`/api/nodes/{pk}/reach?days=N` → `scanReachRows`,
`cmd/server/node_reach.go`) windows on `observations.timestamp >=
sinceEpoch`. ~30 days after the fixture was captured, the newest
observation aged out of the 30-day window, so reach returned no links
and the test could find no repeater with reach.
## Fix
Shift all non-zero `observations.timestamp` forward by the same offset
(preserving relative order), mirroring the other tables in the script.
The offset subquery is uncorrelated, so SQLite evaluates `MAX` once on
the pre-update state (same idiom the existing blocks rely on).
## Verification
Ran the updated script against `test-fixtures/e2e-fixture.db`:
```
BEFORE: newest observation 31 days old → outside the 30-day reach window
AFTER : newest observation 0 days old, all 500 observations within 30 days
```
CI Playwright on this PR is the end-to-end confirmation. Scope is the CI
fixture helper only — no application code, schema, or runtime behaviour
changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Erwin Fiten <erwin.fiten@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1476b857d9 |
fix(#1716): drop rf-health a11y allowlist entry — subsumed by #1720 (#1731)
Fixes #1716. PR #1720 (merged 2026-06-13) consolidated `.rf-range-btn.active` — along with `.clock-filter-btn.active`, `.subpath-jump-nav a`, `.node-filter-option.node-filter-active`, `.subpath-selected`, and `.analytics-time-range button.active` — into the shared `.btn-active-accent` rule that paints `background: var(--accent-strong)` (`#2563eb`) + `color: var(--text-on-accent)` (`#f9fafb`) = **4.95:1**, WCAG AA pass in both themes. That makes the `#1716` axe allowlist entry obsolete: the underlying violation no longer reproduces. This PR drops the entry and adds a dedicated per-issue regression gate so a future refactor that only breaks `.rf-range-btn.active` (without touching the other consolidated selectors covered by `#1719`) trips with a clear `#1716` citation. ## Strict TDD red→green ### RED — `bce51a60` (test-only) Adds `test-a11y-1716-rf-range-btn-active.js`, a pure-CSS probe with three assertions: - **A1** — `.rf-range-btn.active` is routed through a rule whose body sets `background: var(--accent-strong)` + `color: var(--text-on-accent)`. - **A2** — the legacy `var(--accent)` + `#fff` pair (2.75:1) does NOT reappear on any block listing `.rf-range-btn.active`. - **A3** — numeric contrast on the resolved tokens is ≥ 4.5:1 in both light and dark themes. Locally verified the test FAILS when the consolidated active-button block is reverted to `background: var(--accent); color: #fff`: ``` PASS A3[light]: 4.95:1 (fg=#f9fafb bg=#2563eb) PASS A3[dark]: 4.95:1 (fg=#f9fafb bg=#2563eb) FAIL A1: .rf-range-btn.active is NOT routed through the consolidated (--accent-strong / --text-on-accent) pair — PR #1720 regression FAIL A2: legacy 2.75:1 pair re-emerged on .rf-range-btn.active (bg=var(--accent) fg=#fff) FAIL: 2 assertion(s) tripped on .rf-range-btn.active (issue #1716) ``` Then restored CSS — test passes green on the consolidated state from master. ### GREEN — `eea79791` Removes from `tests/a11y-allowlist.yaml`: ```yaml - route: '/analytics?tab=rf-health' selector: 'button[data-range="24h"]' rule: color-contrast issue: 1716 expires_at: 2026-09-11 ``` ### CI wiring — `b300ce6d` Hooks the new probe into the same `.github/workflows/deploy.yml` step that already runs `test-a11y-axe-1668-selftest.js` and `test-issue-1705-subpath-contrast.js`, so the gate runs on every PR. ## Gate output (after green) ``` $ node test-a11y-1716-rf-range-btn-active.js PASS A1: .rf-range-btn.active routes to var(--accent-strong) + var(--text-on-accent) PASS A2: no legacy var(--accent) + #fff pair on .rf-range-btn.active PASS A3[light]: 4.95:1 (fg=#f9fafb bg=#2563eb) PASS A3[dark]: 4.95:1 (fg=#f9fafb bg=#2563eb) PASS: .rf-range-btn.active gated by consolidated --accent-strong / --text-on-accent pair (issue #1716) ``` The umbrella `#1719` probe also still passes (`PASS: all 4 root-cause patterns ≥ 4.5:1 in both themes`). ## Scope Only the `rf-health` / `.rf-range-btn.active` line. The sibling allowlist entries for `#1714` (nodes), `#1715` (neighbor-graph), and `#1718` (prefix-tool) are out of scope — separate issues, separate PRs. No production CSS touched (PR #1720 did the substantive fix). ## Files changed - `tests/a11y-allowlist.yaml` (−5 lines: drop `#1716` entry) - `test-a11y-1716-rf-range-btn-active.js` (+177 lines: new regression gate) - `.github/workflows/deploy.yml` (+1 line: wire the new gate) ## Preflight All hard gates clean (PII, branch scope, red commit, CSS-var defined, CSS self-fallback, LIKE-on-JSON, sync migration, async migration, XSS sinks). All warnings clean. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
b695aec4ec |
fix(#1714): drop nodes a11y allowlist entry — subsumed by #1720 (#1730)
## Summary PR #1720 (commit `a344ae0a`, merged 2026-06-13) introduced `--status-green-text=#15803d` (5.02:1 on white) and routed the `/analytics?tab=nodes` stat-card text usage to the new token. The remaining contrast violation that issue #1714 tracked is gone, so the allowlist entry is stale. ## Change Drop the single allowlist entry tagged `issue: 1714` from `tests/a11y-allowlist.yaml`: ```yaml - route: '/analytics?tab=nodes' selector: '.analytics-stat-card:nth-child(1) > div:nth-child(1)' rule: color-contrast issue: 1714 expires_at: 2026-09-11 ``` No other tabs touched (#1715/#1716/#1718 remain — separate issues, separate PRs). ## Verification The CI a11y gate (`test-a11y-axe-1668.js`) is the authoritative check. It re-renders `/analytics?tab=nodes` in dark+light × desktop+mobile and asserts zero net violations against the trimmed allowlist. With this PR, the entry is gone — if PR #1720's fix were ever reverted, the gate fails immediately (nothing left to mask it). Local repro was not attempted: prior PR #1723 (same allowlist-drop shape, same upstream PR #1720) documented sandbox chromium failures (musl/glibc + Vulkan/EGL); CI is the source of truth for this gate. Before (current `master`, with entry present): CI a11y gate green (the allowlist masks the now-fixed selector). After (this PR, entry removed): CI a11y gate must remain green because the underlying contrast was actually fixed by PR #1720. ## TDD note Config-change exemption per workspace AGENTS.md: - No test files modified. - No production code modified. - Config-only allowlist trim; CI must stay green without test edits. - The gate itself is the test — adding a duplicate assertion file would not catch anything CI does not already catch. Mirrors the exact pattern accepted in PR #1723 (subpaths allowlist drop, same upstream PR #1720 fix). ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all hard gates pass, all warnings pass. ("Red commit" gate notes this is a config-change with no test commits, justified above.) Partial fix for #1714 (will switch to `Fixes #1714` once CI a11y gate confirms zero net violations on `/analytics?tab=nodes`). Refs PR #1720, PR #1723. |
||
|
|
92e001c093 |
chore(a11y): drop subsumed subpaths allowlist entries (#1713) (#1723)
## Summary PR #1720 merged (commit `a344ae0a`) and introduced the shared `.btn-active-accent` color-contrast rule, which subsumes the per-link allowlist entries for the subpaths tab pills on `/analytics?tab=subpaths`. This PR removes the 4 now-stale allowlist entries tagged `issue: 1713`: - `a[href$="#sp-pairs"]` - `a[href$="#sp-triples"]` - `a[href$="#sp-quads"]` - `a[href$="#sp-long"]` All four were `rule: color-contrast` exemptions on `/analytics?tab=subpaths`. ## Verification CI a11y gate (`test-a11y-axe-1668.js`) is the authoritative check — it re-renders the route in dark+light × desktop+mobile and asserts zero net violations against the trimmed allowlist. Local repro was attempted but blocked by sandbox chromium issues (musl/glibc relocations on bundled playwright chromium; host chromium hits Vulkan/EGL init failures). Relying on CI a11y job for the green signal. ## TDD note Config exemption per workspace AGENTS.md: - No test files modified. - Config-only allowlist trim; CI must stay green without test edits. Partial fix for #1713 (will switch to `Fixes #1713` once CI a11y gate confirms zero net violations on `/analytics?tab=subpaths`). Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
ae88d38b12 |
chore(a11y): drop subsumed clock-health allowlist entries (#1717) (#1722)
## Summary
Drops the two stale `issue: 1717` entries from
`tests/a11y-allowlist.yaml`. Both were subsumed by PR #1720 (merged at
|
||
|
|
cbe6e94b1a |
fix(#1706): expand axe route coverage to remaining analytics tabs (#1707)
Adds an anti-drift coverage selftest that fails CI if a future analytics tab is added to `public/analytics.js` but not registered in `test-a11y-axe-1668.js` `ROUTES`. Wires it into the `.github/workflows/deploy.yml` axe job alongside the existing reciprocity selftest. ## Relationship to #1706 / #1711 Follow-up to #1706 / #1711 — #1711 already added the 8 missing analytics tabs to `ROUTES` (`subpaths`, `nodes`, `distance`, `neighbor-graph`, `rf-health`, `clock-health`, `scopes`, `prefix-tool`). This PR locks that in: a future tab added to `analytics.js` without a corresponding `ROUTES` entry now breaks CI on this assertion. NOT closing #1706 — that issue is already closed by #1711. ## What the gate does (headline) `test-a11y-axe-routes-coverage.js` scrapes `<button class="tab-btn" data-tab="...">` declarations from `public/analytics.js` and asserts every declared tab is exercised by `test-a11y-axe-1668.js` `ROUTES` as `/analytics?tab=<tab>`. Asymmetry vs the existing selftest, intentional: - `test-a11y-axe-1668-selftest.js` — checks `REGISTERED_ANALYTICS_TABS ⊆ analytics.js` dispatch arms (no dead registrations). - `test-a11y-axe-routes-coverage.js` (new) — checks `analytics.js data-tab buttons ⊆ ROUTES` (no axe-blind tabs). Together they keep the axe matrix honest in both directions. ## Diff scope Only two files vs merge base: - `.github/workflows/deploy.yml` (+1 line — wires the new test into the deploy-job batch) - `test-a11y-axe-routes-coverage.js` (+74 lines, new file) No production code changes, no `ROUTES` changes (those landed in #1711). ## TDD framing Net-new drift gate — no prior assertions to break, no behavior change in shipped UI. Per workspace `AGENTS.md` net-new-test exemption ("net-new UI surfaces … test must land in the SAME PR but doesn't need to be the FIRST commit"), this analogous net-new gate ships green from commit 1. A red→green pair would require a synthetic regression in `analytics.js` or `ROUTES`, which isn't appropriate for an anti-drift guard. ## Local run Local Alpine chromium 136 crashes under Playwright's CDP probe (`posix_fallocate64: symbol not found`) — affects the axe runner generally, not this selftest. The coverage assertion itself is pure Node (file read + regex + set diff) and runs locally clean. Real verdict comes from CI's Playwright-bundled chromium. --------- Co-authored-by: Kpa-clawbot <bot@openclaw.local> |
||
|
|
9b8b613832 |
fix(#1705): WCAG AA contrast on .subpath-selected .hop-prefix (#1712)
## Summary **Regression guard for an already-fixed bug.** The WCAG AA contrast BLOCKER on `.subpath-selected .hop-prefix` called out in #1705 was already resolved by PR #1708 (commit `293efdb6`), which is an ancestor of this branch's base. This PR adds the missing automated test that locks the fix in — it does **not** ship a CSS change. ### Why this is not a TDD red→green sequence Test commit `0d58d1d5` is **green-on-arrival**: the fix it asserts against had already landed days earlier on master. There is no red commit on this branch — running the test at any point on this branch passes. This PR therefore claims the **net-new-test exemption** per `~/.openclaw/workspace-meshcore/AGENTS.md`: bug fixes on EXISTING UI normally require red→green, but where the fix shipped first and the test is a *post-hoc regression guard*, the test lands in the same PR series but does not need to be the first commit. No production behavior is changed by anything in this branch. (The earlier revisions of this body presented a misleading red→green table; that framing was wrong and has been removed.) ## What this PR actually ships Two test files plus CI wiring — all test-only / config-only: 1. `test-issue-1705-subpath-contrast.js` — parses `public/style.css`, extracts `--accent-strong` / `--text-on-accent` per theme, sRGB-composites any `rgba()` foreground against the resolved background, asserts WCAG AA ≥4.5:1 on `.subpath-selected .hop-prefix` in both themes. 2. `test-issue-1705-subpath-contrast-e2e.js` — Playwright/headless-Chromium variant that loads the real `public/style.css` into a DOM mirroring `analytics.js` `renderSubpathsTable`, then asserts `getComputedStyle` contrast on a live `tr.subpath-selected > .hop-prefix`. Catches specificity/cascade regressions the static parser cannot. 3. `.github/workflows/deploy.yml` PR test stage wiring — runs both tests on every PR. ## The bug (historical, already fixed) For context: `.subpath-selected .hop-prefix` measured ~1.87:1 in dark theme (`rgba(255,255,255,0.6)` composited against `var(--accent)` = `#4a9eff`). #1708 (`293efdb6`) swapped `.subpath-selected` background to `var(--accent-strong)` (`#2563eb`) and color to `var(--text-on-accent)` (`#f9fafb`), yielding **4.95:1** in both themes. The child `.hop-prefix` color was set to `inherit` so the prefix cannot be decoratively muted below AA again. ## Parser test — hardening (review r1) Round-1 review pass surfaced 7 must-fix items on the parser test; all addressed: | # | Concern | Fix | |---|---------|-----| | MF3 | Parser asserted declared cascade, not computed style | Added the Playwright E2E variant; `getComputedStyle` resolves specificity natively | | MF4 | CSS comment cited "4.83:1+" while measured value is 4.95:1 | Comment updated to `#f9fafb on #2563eb = 4.95:1` | | MF5 | `extractBlockTokens` silently returned `{}` when the regex didn't match | Throws with selector label; defensive assertion verifies `:root` declares the three tokens | | MF6 | `'inherit'` on the child color fell back to `#ffffff` silently | Now throws; callers either declare the parent color or use the Playwright variant where `inherit` resolves natively | | MF7 | `extractDecl` picked the last regex match across rules with no specificity model | Now throws on N>1 distinct values; warns on N>1 identical values | ## E2E test — hardening (polish v2) - M2: removed dead `<link rel="stylesheet" href="file://...">` element from the test HTML template — `setContent` runs against `about:blank` origin and cannot fetch `file://`, so the link was dead. CSS is injected inline via `page.evaluate` (unchanged). Removed the now-unused `cssHref` declaration. - M3: fixed misleading class comment — the production table uses `.analytics-table` (see `analytics.js` `renderSubpathsTable`), so the fixture's `class="analytics-table subpaths-table"` was misleading. Stripped `.subpaths-table` from the fixture and corrected the comment to cite the real production class. ## Acceptance criteria - [x] `.subpath-selected .hop-prefix` ≥4.5:1 in both themes — verified by both tests (parser + E2E) - [x] Regression test added covering the selected state, wired into PR CI Partial fix for #1705 — leaves the issue open for the audit-probe alpha-compositing work (private tooling, out of scope here). ## Local test results - `node test-issue-1705-subpath-contrast.js` (parser): **PASS** — `light 4.95:1 / dark 4.95:1` - `node test-issue-1705-subpath-contrast-e2e.js` (Playwright): **SKIP** in dev sandbox (chromium relocation error — musl/arm sandbox-in-sandbox); CI installs the Playwright-bundled binary via `npx playwright install chromium` and runs with `CHROMIUM_REQUIRE=1` --------- Co-authored-by: CoreScope Bot <bot@corescope.local> Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
a344ae0a12 |
fix(#1719): contrast root causes — active-btn / skew-badge / role-swatch / status-green (#1720)
## Summary Fixes the four recurring color-contrast root causes #1719 identifies behind ~320 axe violations on PR #1707's expanded gate. All fixes are token-based; no hardcoded hex introduced. ## TDD - **Red:** `151db732` — `test-a11y-1719-contrast-root-causes-e2e.js` asserts WCAG AA on all 4 patterns; failed with 12 sub-threshold probes. - **Green:** `dd26554e` — fixes below; test now reports 11/11 PASS. ## Patterns + measured contrast (before → after) | # | Surface | Before | After | Note | |---|---|---|---|---| | P1 | `.rf-range-btn.active` / `.clock-filter-btn.active` / `.subpath-jump-nav a` / `#ptCheckBtn` / `#ptGenBtn` | `#fff` on `--accent` (#4a9eff) = **2.75:1** | `--text-on-accent` on `--accent-strong` = **4.95:1** | Consolidated into ONE grouped `.btn-active-accent, ...` rule; inline buttons now use the shared class | | P2 | `.skew-badge--no_clock` (dark theme) | `#fff` on `--text-muted` (#d1d5db) = **1.47:1** | `#fff` on `--skew-badge-no-clock-bg` (#4b5563) = **7.56:1** | New dedicated token, both themes | | P3 | Neighbor-graph role swatches, light theme on white | room 3.30:1 / sensor 3.19:1 / observer 4.23:1 | room **5.02** / sensor **5.02** / observer **5.70** | `customize.js` defaults bumped to palette-{green/amber/purple}-700 | | P4 | `.analytics-stat-card` text in `--status-green` on white | **2.28:1** | new `--status-green-text` = #15803d → **5.02:1** | `--status-green` background token unchanged (still #22c55e); inline text usages routed to the new token | ## Why this unblocks #1707 #1707's 320 axe color-contrast hits decompose into: - 137× single-rule `.skew-badge--no_clock` → P2. - ~N×4 active-button surfaces (rf-health / clock-health / subpaths / prefix-tool) → P1. - Role-swatch text on `/#/analytics?tab=neighbor-graph` (light) → P3. - `.analytics-stat-card` text on `/#/analytics?tab=nodes` (light) → P4. After this merges, the next CI run on #1707 should see the expanded gate go green (or down to a small ≤5 residual the operator can triage separately per the issue's acceptance criteria). ## Local axe gate `BASE_URL=… node test-a11y-axe-1668.js` was **NOT** run locally — the sandbox's bundled chromium fails to boot Playwright (known issue). CI on this PR runs the same gate against the staging fixture; relying on that. The dedicated test `test-a11y-1719-contrast-root-causes-e2e.js` is CSS+JS-parse-driven (no browser) and runs in <100ms — it's the regression net for these 4 patterns specifically. ``` $ node test-a11y-1719-contrast-root-causes-e2e.js PASS [P1] theme=light .rf-range-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=light .clock-filter-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=light .subpath-jump-nav a fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .rf-range-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .clock-filter-btn.active fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P1] theme=dark .subpath-jump-nav a fg=#f9fafb bg=#2563eb ratio=4.95:1 PASS [P2] theme=light .skew-badge--no_clock fg=#fff bg=#4b5563 ratio=7.56:1 PASS [P2] theme=dark .skew-badge--no_clock fg=#fff bg=#4b5563 ratio=7.56:1 PASS [P3] theme=light nodeColors.room on white fg=#15803d bg=#ffffff ratio=5.02:1 PASS [P3] theme=light nodeColors.sensor on white fg=#b45309 bg=#ffffff ratio=5.02:1 PASS [P3] theme=light nodeColors.observer on white fg=#7c3aed bg=#ffffff ratio=5.70:1 PASS [P4] theme=light .analytics-stat-card text color (--status-green-text) fg=#15803d bg=#ffffff ratio=5.02:1 PASS [P4] theme=dark .analytics-stat-card text color (--status-green-text) fg=#22c55e bg=#232340 ratio=6.65:1 PASS: all 4 root-cause patterns ≥ 4.5:1 in both themes (issue #1719) ``` ## Out-of-scope (intentional) - Other text-on-light `var(--status-green)` usages in `nodes.js` (Critical/Valuable labels): different surface, not the analytics-stat-card pattern #1719 calls out. Tracked under analytics audit umbrella. - Hardcoded `var(--status-green, #2ecc71)` fallback in `nodes.js` lines 648/666: same scope deferral. - Allowlist entries: none added per the issue's acceptance criteria. Fixes #1719. --------- Co-authored-by: clawbot <clawbot@kpa.local> Co-authored-by: Kpa-clawbot <bot@openclaw.local> |
||
|
|
4d2033da0f |
fix(#1709): restore Live map viewport from lat/lon/zoom hash params (#1721)
## Summary Fixes #1709 — implements deep-link viewport restoration on the Live page so `#/live?lat=43.0731&lon=-89.4012&zoom=12` (and the `node=` combo) center+zoom the map identically to how `/#/map?lat=...&lon=...&zoom=...` already worked. ## Approach Extracted a shared `parseViewportHash(hashOrSearch, opts)` helper in `public/app.js` (next to existing `getHashParams()`) and wired it into BOTH call sites — Live and Map — so the parse/validate logic is DRY and unit-testable. ### `parseViewportHash` contract - Accepts a full hash (`#/live?lat=...`) OR a bare query string (`lat=...&lon=...`). - Returns `{lat, lon, zoom}` only if BOTH `lat` and `lon` parse to finite numbers within bounds (`lat ∈ [-90, 90]`, `lon ∈ [-180, 180]`). Partial lat-only or lon-only is rejected — the issue explicitly forbids partial application of a center. - `zoom` defaults to 12 when missing, must be numeric when present, and is clamped to `[minZoom, maxZoom]` (defaults `[1, 20]` — sensible Leaflet fallback when the tile-provider config isn't supplied). - Returns `null` for any null/empty/invalid input. ### Precedence chain (Live) 1. **URL hash `lat`/`lon`/`zoom`** — highest priority. Applied BEFORE the initial `setView()` so the very first render lands at the requested viewport (no visible recenter from default → URL), AND in the localStorage-restore block so URL overrides `live-map-view`. 2. `live-map-view` localStorage (existing fallback, preserved). 3. `/api/config/map` defaults (existing default, preserved). ### Node-filter URL preservation The existing node-filter URL update logic at `public/live.js:1634` / `1650` already seeds `params` from `getHashParams()`, so unrelated keys (including `lat`/`lon`/`zoom`) already survive node filter changes. Added two source-grep regression tests to guard against future regressions (catches the anti-pattern `const params = new URLSearchParams(); params.set('node', ...)` which would silently clobber the viewport). ## Files changed - `public/app.js` — `+47/-0` — new `parseViewportHash()` helper + window expose. - `public/map.js` — `+8/-4` — replaces inline `parseFloat`/`parseInt` block with helper call. - `public/live.js` — `+22/-3` — applies helper at init (`setView` line) AND in the localStorage-restore block so URL overrides both fallbacks. - `test-frontend-helpers.js` — `+105/-0` — 14 `parseViewportHash` unit tests + 2 live.js source-grep regression tests for the node-filter URL flow. ## TDD red→green - **Red commit** `e6baf935` (FIRST commit on branch): adds tests + a stub `parseViewportHash` returning `null`. 10 of the 14 unit tests fail on assertion (not import error); 2 live.js source-grep tests already pass against current master (regression guards). - **Green commit** `43b3cb5f`: implements the helper + wires both call sites. All 16 new tests pass. ## Test output (`node test-frontend-helpers.js`, last 15 lines) ``` ✅ #825: deep link to unencrypted #channel falls through to REST and renders messages ✅ deriveKey: SHA256("#test")[:16] matches known value ✅ deriveKey: returns 16 bytes ✅ #815 preserved: deep link to #channel with stored key triggers decrypt path (no lock) ✅ invalidateApiCache causes api to re-fetch after cache bust ✅ computeChannelHash: SHA256(key)[0] ✅ verifyMAC: valid MAC passes ✅ verifyMAC: invalid MAC fails ✅ invalidateApiCache with no prefix busts all entries ✅ invalidateApiCache with prefix only busts matching ════════════════════════════════════════ Frontend helpers: 625 passed, 2 failed ════════════════════════════════════════ ``` The 2 failures (`favStar returns filled star for favorite`, `favStar returns empty star for non-favorite`) are **pre-existing on master** and unrelated to this PR — confirmed by running on `origin/master` before any changes. ## Acceptance criteria (issue #1709) 1. ✅ `#/live?lat=43.0731&lon=-89.4012&zoom=12` centers Live map at that lat/lon/zoom. 2. ✅ `#/live?node=ABC123&lat=43.0731&lon=-89.4012&zoom=12` applies BOTH node filter AND viewport (`getHashParams().get('node')` already feeds `setNodeFilter`; helper independently parses lat/lon/zoom). 3. ✅ URL viewport params override `live-map-view` localStorage (URL check runs first AND overrides the savedView branch). 4. ✅ Invalid viewport params ignored safely (`parseViewportHash` returns `null` on any out-of-range / NaN input). 5. ✅ Missing `lat` or `lon` does NOT partially apply a center (helper requires both). 6. ✅ Live node-filter URL update preserves unrelated params — existing `getHashParams()` seeding + new regression tests. 7. ✅ No backend endpoint changes (`grep -l '\.go$' diff` → empty). ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → **clean** (all 12 gates pass, no warnings). --------- Co-authored-by: Kpa-clawbot <bot@kpabap.dev> Co-authored-by: Kpa-clawbot <bot@openclaw.local> |
||
|
|
69fba8032d |
test(a11y): expand axe CI gate to all 14 analytics tabs + prefix-tool (#1706) (#1711)
## Summary Closes #1706. Expands the axe-core a11y CI gate (`test-a11y-axe-1668.js`) to cover the 7 missing analytics tabs plus the `prefix-tool` utility surface. Before this PR, only 7 of 14 analytics tabs were gated; the other 7 could regress on contrast/aria without CI noticing. ## Changes **`test-a11y-axe-1668.js`** — adds 8 hash-routes to `ROUTES`: - `/analytics?tab=subpaths` - `/analytics?tab=nodes` - `/analytics?tab=distance` - `/analytics?tab=neighbor-graph` - `/analytics?tab=rf-health` - `/analytics?tab=clock-health` - `/analytics?tab=scopes` - `/analytics?tab=prefix-tool` The `prefix-tool` route was verified by greppping `public/analytics.js`: the dispatch arm is `case 'prefix-tool':` (line 292), the nav button uses `data-tab="prefix-tool"` (line 132), and existing UI cross-links use `#/analytics?tab=prefix-tool` (lines 1467, 1469, 1795, 3064). It lives in the analytics tab strip, not a separate `/tools/...` route. `REGISTERED_ANALYTICS_TABS` already lists all 15 tabs (the selftest's reciprocity check kept the constant honest), so no sibling slug-list at line ~82 needed updating beyond the existing list. **`test-a11y-axe-1668-selftest.js`** — adds a meta-assertion that loops over `REGISTERED_ANALYTICS_TABS` and asserts each `tab` has a matching `/analytics?tab=<tab>` entry in `ROUTES`. This locks the gate forever: any new analytics tab added to `analytics.js` will fail the selftest unless its route is also gated. ## TDD shape (red → green) - **Red commit** `a1d9aa8e` — adds the meta-assertion to the selftest. Selftest fails with `#1706: ROUTES missing analytics tab coverage for "/analytics?tab=subpaths"` (asserted, not a build error). - **Green commit** `501d9572` — adds the 8 missing entries to `ROUTES`. Selftest passes: `routes=24 themes=2 allowlist=0`. Cell count: 24 routes × 2 themes × 2 viewports = **96 cells** (was 64). ## Allowlist / tracking issues None yet. The 7 new analytics tabs and `prefix-tool` have not yet been exercised by the full axe browser run in CI — that happens when this PR runs. Per the M5 policy embedded in the gate's header, if any of the new routes fails axe on first run, a follow-up tracking issue + per-policy allowlist entry will be filed (the violations will NOT be silenced in this PR, and the routes will NOT be removed). I'll watch CI and report back. ## Out of scope Per the issue: - No keyboard-nav / focus-visible coverage (separate gap). - No modal / slideover open-state scans (separate gap). - No tufte / density work. - No source-code modifications to any analytics tab (contrast/aria fixes happen in follow-up PRs, not here). ## Preflight Ran `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all hard gates pass, no warnings. ## Verification ``` $ node test-a11y-axe-1668-selftest.js PASS: a11y-axe-1668 selftest — routes=24 themes=2 allowlist=0 ``` --------- Co-authored-by: clawbot <bot@kpa-clawbot.local> Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
293efdb647 |
fix(#1705): subpath-selected hop-prefix contrast BLOCKER (dark, 1.87:1 → ≥4.5:1) (#1708)
## Summary Fixes the BLOCKER half of #1705: `.subpath-selected .hop-prefix` contrast in `public/style.css`. | | Before | After | |---|---|---| | background | `var(--accent)` = `#4a9eff` | `var(--accent-strong)` = `#2563eb` | | color (primary) | `#fff` | `var(--text-on-accent)` = `#f9fafb` | | color (hop-prefix) | `rgba(255,255,255,0.6)` | `var(--text-on-accent)` | | measured contrast (hop-prefix) | **1.87:1** (composite over `--accent`, dark) | **4.95:1** (light + dark) | Pure token swap onto the existing `--accent-strong` / `--text-on-accent` pair already used by `.badge-selected`, `.filter-bar .btn.active`, `.dropdown-item:hover` etc. No new hex literals. Light and dark themes both pass WCAG AA body text (≥4.5:1). ## TDD trail - Red: `033f8e4c` — `test-a11y-1705-subpath-hop-prefix-e2e.js`. Parses `public/style.css`, resolves the relevant tokens per theme, composites the alpha-bearing text over the rendered background, asserts WCAG contrast ≥ 4.5:1. Failed with `ratio=1.87:1` on both themes — the exact value cited in #1705. - Green: `db6b9dd0` — CSS fix. Test now reports `composite=#f9fafb, ratio=4.95:1` on both themes. Why a dedicated test (not just `test-a11y-axe-1668.js`): `.subpath-selected` is a click-state class, so the umbrella axe gate never sees it during initial-paint scans. This is the canonical "state-only" a11y regression class — the umbrella gate is structurally blind to it. ## Out of scope (documented in #1705 for separate follow-up) - The **a11y audit probe correctness fix** (alpha-composite + parent-bg walk) lives in workspace tooling (`workspace-meshcore/a11y-audit/audit.py`), not in this repo. The probe-correctness write-up is captured in #1705 itself; this PR is exclusively the CSS BLOCKER + regression test. - "Other rgba-based dark-mode contrast surfaces" — per the issue's Out-of-scope section, those get filed separately if discovered. ## Local verification ``` $ node test-a11y-1705-subpath-hop-prefix-e2e.js PASS theme=dark bg=#2563eb text=#f9fafb ratio=4.95:1 PASS theme=light bg=#2563eb text=#f9fafb ratio=4.95:1 ``` The full `test-a11y-axe-1668.js` gate could not be exercised on this sandbox (Chromium SIGTRAPs against the host kernel — unrelated to this change). CI runs it on Ubuntu where the umbrella ruleset already enforces the 0-violation policy. ## Browser verified CSS-only change in a CSS-variable swap. Computed values are deterministic from the stylesheet and asserted by the new test; no JS / DOM / render-path is touched. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all gates clean. Fixes #1705 --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: Kpa-clawbot <bot@clawbot> |
||
|
|
97833c523b |
fix(post-packets): use v3 observations schema (closes #1196) (#1704)
## Summary `POST /api/packets` is broken on every v3-schema install — which is the default since #1289. The handler issues two writes against legacy v2 column names and silently swallows the observation insert's error, returning `200 OK` with `id>0` while persisting zero observation rows. ## Root cause `cmd/server/routes.go:1225-1235` (pre-fix) used the v2 schema shape: ```go INSERT INTO transmissions (... path_json ...) // path_json removed in v3 INSERT INTO observations (transmission_id, observer_id, observer_name, snr, rssi, timestamp) // v2 columns // timestamp written as RFC3339 text; v3 wants unix INTEGER // second Exec's error was discarded ``` v3 schema (`cmd/ingestor/db.go:289-304`): `observations.observer_idx INTEGER` (FK `observers.rowid`), `observations.timestamp INTEGER` (unix epoch), `path_json` lives here not on `transmissions`. Reporter [@EldoonNemar](https://github.com/EldoonNemar) called this out precisely in #1196 — both the schema mismatch and the divergence between the test harness (which uses the v3 shape) and the handler (v2 shape). ## Fix `cmd/server/routes.go`: - `transmissions` insert: drop `path_json` column. - Observer resolution: `INSERT OR IGNORE INTO observers (id, name, ...)` then `SELECT rowid` — mirrors the ingestor resolver at `cmd/ingestor/db.go:778,906`. - `observations` insert: write `observer_idx INTEGER` + `timestamp = time.Now().Unix()`; `path_json` moved here. - **Propagate both insert errors** (transmission + observation) as `500` instead of swallowing them. ## TDD | Step | Commit | Result | | ----- | ------- | ------ | | RED | `46d25389` | Test fails on master: `id=0` because the transmissions insert references a column not present in v3. | | GREEN | `dae57d67` | Test passes; round-trip persists the observation with `observer_idx` resolved from the seeded `obs1` row and a unix-epoch `timestamp`. | Local repro: ``` # RED on the test commit alone: $ go test -run TestPostPacketPersistsV3Schema -count=1 . --- FAIL: TestPostPacketPersistsV3Schema (0.03s) routes_test.go:4755: expected transmission id > 0, got 0 (body: {"id":0,"decoded":{...}}) FAIL # GREEN on HEAD: $ go test -run TestPostPacketPersistsV3Schema -count=1 . ok github.com/corescope/server 0.037s ``` ## Scope Two files, both in `cmd/server/`: - `cmd/server/routes.go` (+38/-12) — handler rewrite - `cmd/server/routes_test.go` (+66) — round-trip regression test No public API signature changes. No DB schema changes (consumes the existing v3 schema correctly). Closes #1196 |