mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 11:21:27 +00:00
fe997fefb2b083062ee252f957b147cd6a3de106
892 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2d654bf61 |
fix(#1415, #1458): packets layout + mobile chrome + semantic-first detail (#1459)
## Closes #1415 — packets cross-viewport jank ## Closes #1458 — Tufte mobile-packets P0 findings (folded into same branch) Single PR covers both issues — they touch the same files (`public/packets.js`, `public/style.css`) and a split would invite merge thrash. ### #1415 — column priority + chrome compaction Locked column-priority tiers (operator spec): | Tier | Viewport | Columns | |---|---|---| | 1 | always (mobile through desktop) | expand · time · type · details | | 2 | tablet+ (>768px) | path | | 3 | desktop only (>1024px) | hash · observer · rpt | Enforced via existing `data-priority` system in `TableResponsive.apply` (priorities 3 → hide ≤1024, 5 → hide ≤768). CSS: - `.col-expand` pinned to `width/min-width/max-width: 32px` at every viewport — kills the 50–180px dead column that pushed every data column right. - `.col-details` capped at `max-width: 480px` so wide viewports stop wasting hundreds of px on the last column. - `@media (max-width: 480px)` hides page-header BYOP, shrinks the h2, and tightens row padding → pre-table chrome drops from ~280px to ~140px. ### #1458 — Tufte mobile P0 findings **P0-A: semantic-first detail panel.** Was: `"Packet Byte Breakdown (134 bytes)"` title + giant neon hex grid above the meaningful fields. Now: type badge + decoded summary + hop count + `src → dst` lead the panel, followed by the existing `.detail-meta` dl (reordered: Payload Type → Path → Timestamp → Observer). **P0-B: raw-bytes disclosure.** Hex legend / hex dump / field table wrapped in `<details class="detail-technical">`. Disclosure copy reads "Show raw bytes". Collapsed by default on phones (`window.innerWidth ≤ 480`), expanded on tablet+. **P0-C: mobile filter-zone collapse.** The always-on filter-expression input above `.filter-bar` is now wrapped with `.pkt-filter-expr` and hidden under the `@media (max-width: 480px)` block. Reveals when the existing "Filters ▾" toggle adds `.filters-expanded` to the sibling `.filter-bar` (CSS `:has()` selector — one tap reveals both chrome rows together). ### TDD `test-issue-1415-packets-layout.js` — pure source-grep, no browser: - col-expand class on first `<th>` + `<td>` + CSS 32px pin - locked column-priority tier values per column - `.col-details` max-width ≤ 480px - mobile @media block: hides BYOP, hides `.pkt-filter-expr` (revealed by `.filters-expanded`) - detail-meta order: Payload Type before Observer - `<details class="detail-technical">` wrapper exists with "Show raw bytes" summary - detail-title leads with a type badge; `.detail-srcdst` emitted - old "Packet Byte Breakdown (N bytes)" title literal removed Red commit `d4372d82` (8 assertion failures, no compile errors), green commit `4fab9dbd` (#1415 work), follow-up commit `a5218035` (#1458 work) keeps everything green. 26 assertions, 0 failed. --------- Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
d24246395d |
fix(#1456): rename Usefulness → Traffic share + add traffic_share_score field (#1457)
## Summary Rename the "Usefulness" UI label to "Traffic share", add hover tooltips for both Traffic share and Bridge score, and introduce a new `traffic_share_score` field on `/api/nodes` (alongside the legacy `usefulness_score`, kept for API back-compat). Closes #1456. ## Why The "Usefulness" label implied a composite score that doesn't exist yet — only the Traffic-share axis (axis 1 of 4 from #672) and the Bridge axis (axis 2 of 4 from #1275) are wired today. A node with low traffic but critical structural position read as "not useful" — exactly wrong. Neither score had a tooltip explaining what it measured. ## Changes ### Frontend (`public/nodes.js`) - Visible label `Usefulness` → `Traffic share` (with ⓘ glyph) - Tooltip explains traffic-share semantics, cross-references Bridge for structural importance, points at #672 for the 4-axis roadmap - Bridge row gets a parallel ⓘ glyph and a tooltip naming "betweenness centrality" + the "quiet but irreplaceable chokepoint" interpretation - Prefers new `traffic_share_score` with graceful fallback to legacy `usefulness_score` ### Backend (`cmd/server/routes.go`) - `/api/nodes` and `/api/nodes/{pubkey}` now emit BOTH `usefulness_score` (kept for API compat) AND `traffic_share_score` (new canonical name), populated with the same value - Inline comment documents the deprecation path: when the #672 composite ships, `usefulness_score` becomes the composite and `traffic_share_score` keeps the per-axis value ## Tests - `test-issue-1456-score-labels.js` — file-grep pins on `nodes.js` (label, tooltip fragments, percent formatting, dual-field read with fallback) - `cmd/server/traffic_share_score_test.go` — `/api/nodes` + `/api/nodes/{pk}` responses contain both fields with equal values TDD: red commit (`8bd235a0`) added failing tests; green commit (`c4d3aee5`) implemented. `go test ./cmd/server/...` passes (47s). ## Out of scope - Renaming the backend field (would break consumers) - Wiring axes 3 (Coverage) and 4 (Redundancy) — tracked in #672 - Changing the score calculation --------- Co-authored-by: clawbot <bot@openclaw.local> |
||
|
|
d00ba91b1a |
feat(#1454): customizer toggle for show encrypted channels (#1455)
## Summary Adds a customizer checkbox that toggles `localStorage["channels-show-encrypted"]` — the read-gate that controls whether `/api/channels` is fetched with `?includeEncrypted=true`. Today operators can only flip that gate from DevTools; this PR gives them the obvious affordance. Default behavior is unchanged: key remains unset → server filters encrypted entries → ~19 channels rendered. Toggle ON sets the key to `"true"` → fetch grows to ~265 with `Encrypted (0xAB)` entries. ## Behavior - **Display tab → new "Channels" subsection → "Show encrypted channels" checkbox.** - ON writes `localStorage["channels-show-encrypted"] = "true"`. - OFF *removes* the key (never writes `"false"`) so the read-gate cleanly returns false and the customizer match-default detection still works. - Toggling dispatches `mc-channels-show-encrypted-changed`; `channels.js` listens and re-fetches via `loadChannels()` — no page reload. - Tooltip / hint copy: "Encrypted channels appear as 'Encrypted (0xAB)' with no name. Operators usually leave this off." ## TDD `test-issue-1454-channels-toggle.js` — source-grep invariants: - Red commit `feb9dcee`: assertions on customizer + listener — failed (production code not yet present). - Green commit `d8742f2c`: production patch — passes. Read-gate at `public/channels.js:1564` is left untouched; the test asserts it. ## Out of scope - Migration of legacy localStorage values into customizer overrides (no override store needed — we keep using the raw localStorage key as the single source of truth). - Per-region toggle. - Decryption key UI. Closes #1454 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
7abe2dd56b |
fix(#1065): remove stray CSS-eater text that killed .gesture-hint parent rule (#1453)
After #1452 merged with width:fit-content + max-width on .gesture-hint, CDP showed the rule was still missing from CSSOM. Tracked it down to line 4024 of style.css which had a raw '(feat(#1062): green — implement gesture system)' string OUTSIDE any comment, after the #1062 closing marker. The parser ate forward through the .gesture-hint parent rule. One-character fix removes the parenthesized commit fragment. Verified via CDP: rule now appears in CSSOM and width:fit-content takes effect. Final follow-up to #1452. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
58282c91d8 |
fix(#1065): gesture hints touch-gate + width:fit-content + CSS-parse safety (#1452)
## Summary Three follow-up fixes for #1065 gesture-hint discoverability: 1. **Touch-capability gate.** New `hasTouchCapability()` helper probes `'ontouchstart' in window`, `navigator.maxTouchPoints`, and `(pointer: coarse)`. Every `HINTS[*].relevant()` predicate now returns `false` immediately on mouse-only viewports, so desktop browsers no longer get "swipe a row left" tips. 2. **`width: fit-content` on the pill wrap.** The `.gesture-hint` block previously had no explicit width and defaulted to block-level full-width. Combined with `translateX(-50%)` on `.gesture-hint-bottom` this rendered as a 100vw-wide bar centered with a negative-X transform, i.e. pushed off-screen-left on narrow viewports (384px wrap on 390px viewport). 3. **CSS-parse safety.** Moved the in-body comment (which contained an em-dash) outside the rule block. An earlier attempt to add `width: fit-content` together with an in-body em-dash comment caused the parent `.gesture-hint` rule to vanish from the CSSOM in Chrome (children `.gesture-hint-*` remained). Putting the comment above the block sidesteps the parser bug. ## Test `test-issue-1065-gesture-hints-gates.js` — pure source-file assertions, no browser required. Red commit first (7 fails), green commit second (10/10 pass). Wired into `test-all.sh`. ## Verification After hot-deploy on staging: - Desktop (no touch): `document.querySelectorAll('.gesture-hint').length` === 0 - Mobile emulated (touch): hint rendered, `getBoundingClientRect().x >= 0`, `width <= 360`, `width < viewport_width` - CSSOM: parent `.gesture-hint` rule present with `width: fit-content` + `max-width: 360px` --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
b5a1642024 |
fix(#1450): preserve custom logo aspect ratio (svg/img CSS split) (#1451)
## Summary
Custom navbar logos via `branding.logoUrl` were rendered squished. The
CSS rule `.brand-logo { width: 125px }` was pinned to the default
inline-SVG wordmark's viewBox aspect (~3.08:1), and when customize-v2
swapped the inline `<svg>` for an `<img>`, that `<img>` inherited the
same fixed 125px width — stretching every non-3.08:1 image into a pill.
## Root cause
- `public/style.css:520` — `.brand-logo { width: 125px }` applied
regardless of element type.
- `public/customize-v2.js:75-77` — `_setBrandLogoUrl` additionally
hardcoded `width="125" height="36"` attributes on the created `<img>`,
overriding any CSS aspect rescue.
- Mobile media query (`style.css:1729`) had the same issue with `width:
112px`.
## Fix
Split the CSS rule by element type:
- `svg.brand-logo` — keeps 125×36 pin for the default wordmark (no
regression).
- `img.brand-logo` — `width: auto`, `max-width: 200px`, `object-fit:
contain` so the operator image's natural aspect is preserved with a sane
cap so very-wide logos can't blow nav layout.
- Mobile `@media` mirrors the split (svg 112×32 pinned, img auto width
with 180px cap).
- Drop the hardcoded `width=125`/`height=36` attrs from the `<img>`
created in `customize-v2 _setBrandLogoUrl`.
## TDD
Red commit `a20b7d7`: 4 assertions, all fail on master.
Green commit `533f464`: same 4 assertions, all pass.
```
✓ img.brand-logo CSS rule exists and uses width:auto (not pinned)
✓ svg.brand-logo CSS rule still pins width:125px (no default regression)
✓ mobile media-query splits the .brand-logo rule into svg/img variants
✓ customize-v2 _setBrandLogoUrl does NOT hardcode width/height attrs on the IMG
```
## Verification plan post-merge
Hot-deploy to staging and CDP-verify:
1. Default SVG wordmark still renders at 125×36 (no default regression).
2. Square 100×100 data-URI logo renders as ~36×36 (was 125×36 pill).
3. Tall 100×300 data-URI logo renders as ~12×36 (was 125×36 pill).
Closes #1450
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
8987dd4163 |
fix(#1446): clearOverride also reverts root --mc-role-* when preset active (#1449)
Last loose end from #1446: clearOverride was leaving the root-level inline --mc-role-{role} stuck at the previous user-pick value. Body cascade still wins for descendants, so visible UI was correct, but introspection (getComputedStyle on documentElement) reported the stale color. One-line additive fix: also call root.removeProperty when preset is active + no user override. Verified by CDP scenario-4 chain (clearOverride → expect revert to preset). Closes the final loose end from #1446 / #1438 chain. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
e4b703b6a5 |
fix(#1446): customize-v2 user override beats active CB preset (followup to #1447) (#1448)
## Summary
Follow-up to #1447 (merged commit
|
||
|
|
ddf14d1954 |
feat(#1446): CB preset is an end-user opt-in (closes #1446, fixes #1444 cascade) (#1447)
## Summary Reframes the CB-preset feature as an **end-user opt-in** layered above operator config — not the canonical color source for the app. Implements the cascade defined in #1446's acceptance test and fixes the #1444 cascade trap as a side effect. **Cascade (top wins):** ``` user per-role override > active CB preset > server config.nodeColors > built-in :root defaults ``` Red commit: |
||
|
|
604c3552c7 |
fix(#1438): customizer per-role override writes --mc-role-{role} on reload (#1443)
## Summary Closes the final gap left by #1439 (marker SVG `fill="var(--mc-role-X)"` migration) and #1441 (body.style write in `setRoleColorOverride`). Both prior PRs made marker SVGs read from `--mc-role-{role}` CSS vars, and made the LIVE customizer pick path write that var via `setRoleColorOverride`. But the second leg of the round-trip was still broken: **On page reload**, `customize-v2.js applyCSS()` replays `userOverrides.nodeColors` from localStorage and writes only `--node-{role}` (the legacy var). `setRoleColorOverride` is **not** replayed. Result: marker fills revert to the active preset's colors even though the operator's custom hex is still in localStorage. ## Fix Extend the per-role loop in `applyCSS` to write **both** `--node-{role}` (legacy compat) and `--mc-role-{role}` (the var marker SVGs now read). ```js for (var role in nc) { root.setProperty('--node-' + role, nc[role]); root.setProperty('--mc-role-' + role, nc[role]); // NEW } ``` `public/customize.js` `setRoleColorOverride` path: already correct in `roles.js` (#1441 wrote the body.style hop with the explicit #1438 comment). No change needed there — the gap was specifically the reload-time replay in customize-v2. ## Test New `test-issue-1438-customizer-mcrole.js` — source-invariant assertions on the loop body. Red commit fails on the `--mc-role-` assertion; green commit passes 4/4. Added to `test-all.sh`. ## Verification plan Post-merge hot-deploy + CDP verify on `analyzer-stg.00id.net`: 1. `setOverride('nodeColors','repeater','#ff00ff')` → `applyCSS(computeEffective())` 2. Assert `getComputedStyle(documentElement).getPropertyValue('--mc-role-repeater') === '#ff00ff'` 3. Sample a repeater marker SVG, assert `getComputedStyle(...).fill === 'rgb(255, 0, 255)'` 4. Screenshot Closes #1438. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
074e3d6bed |
fix(#1438): write customizer override to body.style too (follow-up to #1439) (#1441)
## Summary Follow-up to #1439. Empirical CDP verification on staging caught a residual bug: the customizer per-role override updated `documentElement.style` (where the override helper writes) but mounted SVG markers and other CSS-var consumers kept showing the active preset colour. ## Root cause `cb-presets.js` ships stylesheet rules of the form: ```css body[data-cb-preset="deut"] { --mc-role-companion: #648FFF; ... } ``` This selector beats inheritance from `:root.style` (which is where #1439's `setRoleColorOverride` wrote). Body inline style beats both. ## Fix `setRoleColorOverride` now writes the override to BOTH `documentElement.style` and `document.body.style`. The first-override snapshot is captured per target so clear-override still restores the active preset value (#1412 contract preserved). ## Verification - `test-issue-1438-marker-css-vars.js` extended with assertion E2 (helper touches `document.body` / `body.style`) - `test-issue-1412-customizer-no-override.js` — 13/13 still pass (clear-override-restores-preset) - `test-issue-1407-cb-preset-propagation.js` — 61/61 still pass - Staging CDP verified: `applyPreset('deut')` + `setRoleColorOverride('companion', '#ff00ff')` repaints all 55 mounted companion markers to magenta without reload. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean. Fixes the residual case left after #1439. Co-authored-by: OpenClaw Bot <bot@openclaw> |
||
|
|
94f004909c |
fix(#1438): migrate marker fills to CSS vars + write --mc-role-* in customizer (#1439)
## Summary Fixes #1438. Map + Live node markers and customizer per-role overrides did not honor CB-preset switches because: - SVG markers baked `ROLE_COLORS[role]` hex into `fill=` attribute at marker creation. Existing markers were stale until full page reload after `MeshCorePresets.applyPreset(...)`. - `setRoleColorOverride` only mutated the JS `_roleOverrides` map; the `--mc-role-{role}` CSS var (source of truth for cluster pills, route lines, all CSS-var-driven surfaces) was never updated, so operator picks were invisible to those surfaces. ## Fix shape Empirically verified in headless chromium: CSS-var-on-SVG-fill **does** repaint mounted elements when the variable value changes. Pure CSS-var migration is sufficient — no `cb-preset-changed` listener needed on the marker layers. - **`public/roles.js makeRoleMarkerSVG`** — default fill is now `var(--mc-role-{role})`; callers passing an explicit colour (matrix mode, stale dim) still win. - **`public/map.js makeMarkerIcon` + observer star overlay** — same migration to `var(--mc-role-{role})` / `var(--mc-role-observer)`. - **`public/live.js addNodeMarker`** — passes `null` to `makeRoleMarkerSVG` so the var path is used; inline fallback SVG also uses the var. - **`public/roles.js setRoleColorOverride`** — now writes `--mc-role-{role}` on `documentElement.style`. On clear, restores the preset value captured at first-override time, preserving #1412's contract ("clearing override reverts to active preset"). ## TDD Red commit: `test-issue-1438-marker-css-vars.js` asserts the CSS-var contract across all four files. Failed 5 assertions on `master`: - `makeRoleMarkerSVG emits var(--mc-role-X) in default fill path` - `makeMarkerIcon body references var(--mc-role-*)` - `observer star overlay uses var(--mc-role-observer)` - `addNodeMarker body references var(--mc-role-*)` - `setRoleColorOverride body writes --mc-role-{role} CSS var` Green commit: code fix → all 13 assertions pass. ## Verification - `test-issue-1438-marker-css-vars.js` (new) — 13/13 pass - `test-issue-1407-cb-preset-propagation.js` — 61/61 pass (no regression) - `test-issue-1412-customizer-no-override.js` — 13/13 pass (clear-override-restores-preset contract preserved by `_presetCssSnapshot`) - `test-marker-outline-weight.js` — 6/6 pass - Full `test-all.sh` — same pre-existing pass/fail count (no new failures introduced) Browser verified: CSS-var-on-SVG-fill repaint behavior confirmed live in headless chromium (about:blank test svg, `setProperty('--test-color', '#0000ff')` flips a mounted `<rect fill="var(--test-color)">` from red to blue without re-mount). Staging hot-deploy + CDP verification will happen post-merge (per fix-issue playbook). ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all gates clean. --------- Co-authored-by: OpenClaw Bot <bot@openclaw> |
||
|
|
777f77a451 |
feat(#1420): dark-tile provider picker in customizer (4 variants) (#1430)
# feat(#1420): dark-tile provider picker in customizer (4 variants) Closes #1420. ## What Operator pick: don't force a single dark-tile choice on everyone. Wire 4 candidates into the customizer + server config so users can choose which dark basemap they want, with per-browser persistence. ## Providers shipped | ID | Source | Filter | |---|---|---| | `carto-dark` (default) | `https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png` | none | | `esri-darkgray-labels` | Esri Dark Gray Base + Reference (two stacked layers) | none | | `voyager-inverted` | Carto Voyager + CSS `invert(1) hue-rotate(180deg) brightness(0.9) contrast(1.05)` on `.leaflet-tile-pane` | applied in dark, cleared in light | | `positron-inverted` | Carto Positron + same CSS invert | applied in dark, cleared in light | No new dependencies — all providers are URL-only. ## Architecture - **`public/map-tile-providers.js`** — registry + 5 public helpers (`MC_TILE_PROVIDERS`, `MC_setDarkTileProvider`, `MC_getDarkTileProvider`, `MC_setServerDefaultTileProvider`, `MC_applyTileFilter`). Persists to `localStorage['mc-dark-tile-provider']`. Dispatches `mc-tile-provider-changed` on user pick. - **`public/map.js` / `public/live.js`** — resolve the active dark provider via the registry, manage the Esri labels overlay lifecycle (add when needed, remove cleanly so we don't leak layers on repeated theme toggles), and apply/clear the CSS filter on `.leaflet-tile-pane`. Listen for both `data-theme` mutations AND `mc-tile-provider-changed`. - **`public/customize-v2.js`** — new "Dark Map Tiles" dropdown in the Display tab. On change, calls `MC_setDarkTileProvider(id)`; the maps re-render live without reload. - **`public/roles.js`** — hydrates the server default via `MC_setServerDefaultTileProvider` from `/api/config/client`. - **Server (`cmd/server/`)** — new `mapDarkTileProvider` string on `Config` + surfaced in `ClientConfigResponse`. Default empty → client uses `carto-dark`. - **`config.example.json`** — documents the new field with all allowed values. ## Behavior guarantees (from the acceptance criteria) - ✅ Light mode is **completely unchanged** — `_resolveTileUrl(false)` short-circuits to `TILE_LIGHT` with no filter and no overlay logic. - ✅ Switching dark→light always clears the CSS filter, even if an inverted provider remains selected (`MC_applyTileFilter` is called on every theme change and early-returns to `style.filter = ''` when not dark). - ✅ Switching light→dark with an inverted provider re-applies the filter. - ✅ Attribution is updated per provider (Esri credit for Esri, CartoDB credit for the others); the Leaflet attribution control is refreshed. - ✅ Esri uses two stacked layers (base + reference labels). The reference layer is added/removed cleanly so repeat toggles do not leak. - ✅ Customizer change → immediate re-render, no reload. Uses the same "live setting + persist + dispatch event" pattern as cb-presets (#1361). ## TDD - Red commit: `148b71c3` — `test(#1420): add failing tests for dark-tile provider registry (red)` — 6/7 assertions fail (stub only returns nulls). - Green commit: `49ffb230` — `feat(#1420): dark-tile provider picker — 4 variants wired into customizer` — 7/7 pass. ## Tests `test-issue-1420-tile-providers.js` (wired into `test-all.sh` and `.github/workflows/deploy.yml` JS-unit step): ``` ── #1420 Dark-tile provider registry ── ✅ MC_TILE_PROVIDERS has all 4 IDs with url + attribution ✅ Inverted providers have non-null invertFilter; non-inverted have null ✅ MC_setDarkTileProvider persists to localStorage and dispatches mc-tile-provider-changed ✅ MC_setDarkTileProvider rejects unknown IDs (no persistence, no dispatch) ✅ MC_getDarkTileProvider falls back to server default, then carto-dark ✅ Apply filter for inverted provider in dark mode; clear when switching to non-inverted ✅ Light mode always clears the CSS filter even if inverted provider is selected 7 passed, 0 failed ``` `cd cmd/server && go build ./... && go vet ./...` — clean. ## CDP verification Not run in this PR — the sandbox does not have a Chrome CDP endpoint reachable, and staging cannot exercise this code path until this branch is deployed. The issue body's "CDP-verified candidate set" table covers prior provider-URL validation; the new code path (registry lookup + filter swap + Esri overlay lifecycle) is covered by the unit tests above. **Recommend operator run a quick manual verification on staging post-deploy:** dark mode → open customizer → cycle through all 4 providers, confirm tiles render and the CSS filter is applied for `voyager-inverted` / `positron-inverted` (verify via `getComputedStyle(document.querySelector('.leaflet-tile-pane')).filter`). ## Files touched - `public/map-tile-providers.js` (new) - `public/map.js`, `public/live.js`, `public/customize-v2.js`, `public/roles.js`, `public/index.html` - `cmd/server/config.go`, `cmd/server/routes.go`, `cmd/server/types.go` - `config.example.json` - `test-issue-1420-tile-providers.js` (new), `test-all.sh`, `.github/workflows/deploy.yml` - `.eslintrc.json` (register new `MC_*` globals) --------- Co-authored-by: openclaw <bot@openclaw.local> |
||
|
|
77d1925f30 |
Route view v2 — Tufte redesign (packet context, multi-path picker, mobile bottom-sheet, CB-preset live colors) (#1423)
# Route view v2 redesign Fixes #1418, Fixes #1419, Fixes #1422 This is the route-view redesign that came out of a long iterative QA cycle. The first commit (`a3c39636`) landed the v1 sidebar timeline + multi-path baseline; this PR's second commit (`0e2e913f`) is the v2 polish covering packet context, multi-path picker, mobile bottom-sheet, CB-preset live colors, and dozens of operator-driven UX fixes. ## The journey, in one line > "The data is a sequence. Geography is annotation. The packet is the cargo, the route is the road — show both." ## New surfaces ### 1. Packet context block (sidebar header) Above the multi-path chip, a per-type fact list explaining **what** is traveling. Operator was tired of "the route view shows the road but not the cargo." | Type | Chip | Facts | |-------------|-----------------|---------------------------------------------------------| | ADVERT | 📡 ADVERT | name · role · sig ✓ · self-reported GPS · pubkey prefix | | TXT_MSG | ✉ DM | src → dst · 🔒 encrypted | | REQ/RESPONSE| 🔒/🔓 REQUEST/…| src → dst · 🔒 encrypted | | GRP_TXT | # CHANNEL MSG | #channel · 🔓 decrypted · "…content preview…" · sender | | TRACE | ⌖ TRACE | Official: N hops · Observed: M | | PATH | 🔀 PATH | src → dst (with "from payload" chip on SRC/DST rows) | Sources merge `pkt.decoded_json` + `obs.decoded_json` (channel data often lives at packet level) and fall back to byte-level `raw_hex` parsing for encrypted DMs and unkeyed channel msgs. ### 2. Multi-path picker The header lists every unique observer-path with `<count>/<total>` chip + hex hop string. Click a path → full-clear and redraw that path only (Tufte v6's "replace + retain subpath weights"). "All" → edge-deduplicated UNION view (each unique edge drawn once, stroke = observer count, single accent color, no seq numbers because there's no single ordering). ### 3. Deep-link URLs `#/map?packet=<hash>&obs=<id>` — bookmarkable, shareable, the single source of truth. sessionStorage flow removed. "Back to packet" preserves the obs id. ### 4. Hop resolution Priority: server `resolved_path` → shared `window.HopResolver` (same resolver as packets page, observer-IATA-aware) → raw prefix. Eliminates a whole class of "route view named hops differently than packet detail" bugs. ### 5. Markers (v5/v6/v7) - All markers same 22 px filled circle, seq number rendered **inside** - SRC + DST get a 2 px hollow endpoint ring - SRC = DST loop → **double concentric ring** (ring grammar extended, no new glyph) - Spider-fan within 14 px collisions (16 px arc, dashed hairline), re-runs on `zoomend` only, debounced ### 6. CB preset live colors - Each preset gets a `routeRamp` (5 stops): default/trit = viridis, deut/prot = plasma, achromat = pure luminance - `cb-presets.js` writes `--mc-rt-ramp-0..4` CSS vars; route reads them via `getComputedStyle` - `cb-preset-changed` + `theme-changed` listeners hot-recolor without re-render ### 7. Desktop chrome - **Resize handle** on right edge of sidebar (drag, persisted to `localStorage["mc-rt-sidebar-width"]`) - **Collapse button** = round chevron **centered on the right edge** (Material/Drive style — not in the top-right corner, doesn't collide with the close X) - Collapsed = 36 px strip with rotated "ROUTE" label, expand on click ### 8. Mobile (bottom sheet) - Anchored above bottom-nav (`bottom: 56px + safe-area-inset`) - Collapsed = thin summary line `TYPE · N hops · X km · M obs` + hex preview, tap chevron to expand to ~75 vh - Drag-grip removed (conflicted with browser pull-to-refresh + CoreScope's own pull-to-reconnect) - Desktop collapse / resize affordances hidden on mobile (sheet is the mobile collapse affordance) - Map controls toggle floats top-right, panel collapses on route entry, reachable via toggle click - All three mobile detail panels (`pktRight`, `.slide-over-panel`, `#mobileDetailSheet`) explicitly closed when entering route view ### 9. Map fit / centering - Manual layer-children walk because `L.LayerGroup.getBounds()` doesn't aggregate (only `FeatureGroup` does) - Mobile padding: `paddingTopLeft: [30, 70]`, `paddingBottomRight: [30, 190]` to clear top-nav + sheet+nav stack - Re-fits on: initial render, isolate, All, `window.resize` (iOS URL-bar collapse) - Staggered timers 0/200/600/1400 ms (and 2800 ms on initial render) to survive layout settles ### 10. Hop drill-in refinements - SNR sparkline suppresses connecting polyline when n < 3 (two points implies a trend across time it can't represent — dots only) - "Node details" link properly chip-styled with aria-label including node name + route count ## Edge weight scales | View | Range | |---------------------------------|----------------| | Single-path | 5 px flat | | Multi-path interior | 3..9 | | Origin→hop1 / last-hop→dest | proxy via max adjacent edge count | | Union overlay | 2..8 | Boundary edges (SRC→first hop, last hop→DST) used to render thin because `edgeCounts` only tracks `path_json` transitions. Now they take the strongest adjacent edge count as proxy (every observer who saw the packet implicitly transited that boundary edge). ## Files - **NEW** `public/route-tufte.js` (~1700 lines) — the route renderer + sidebar - **NEW** `public/route-tufte.css` (~750 lines) — all styling - **MOD** `public/map.js` — async draw functions, deep-link loader, `__mc_nodes` exposure, raw_hex extraction - **MOD** `public/packets.js` — View Route → deep-link URL only, closes all mobile panels - **MOD** `public/cb-presets.js` — `routeRamp` per preset + CSS var write - **MOD** `public/index.html` — script + stylesheet tags ## Testing Manually CDP-validated across desktop and mobile-emulator viewports for every major change. Fixtures cover: - ADVERT (4 hops, single-obs) - DM (TXT_MSG, raw_hex parse) - GRP_TXT (#test channel, decrypted text) - PATH (operator's bug case) - TRACE (3-hop) - 1-hop edge case - Multi-path (75-observer 4-hop with 47 unique paths) - 32-hop stress - Loop (SRC = DST) - Bay Area dense cluster (spider-fan) Per AGENTS.md net-new-UI exemption, no failing-test-first; existing tests stay green. **TODO**: Playwright E2E follow-up PR. ## What's deferred to v2.1 / follow-ups - **Glyph overlay on SRC marker** for packet type (e.g. 📡 corner glyph on ADVERT marker, ⌖ on TRACE) - **Per-hop SNR sparkline for TRACE packets** (their payload contains real per-hop SNR contributions, distinct from observer-derived SNR) - **GRP_TXT full content preview** (currently truncated at 80 chars; could expand inline) - **Playwright E2E test** covering the deep-link → isolate → All flow ## Screenshots (would be useful here — CDP screenshots captured during dev show: desktop with sidebar + multi-path picker, mobile with bottom sheet + overlay toggle, isolated-path view, union view, spider-fan on Bay Area cluster, packet context for each of the 5 main types) ## Operator's frustration patterns (lessons for next time) 1. **Browser-validate every UI change, not just compute state** — CDP-screenshot before claiming a UI fix is done. Verifying `display:none` resolves correctly is necessary but not sufficient; the visual layout matters. 2. **Edge-deduplicated drawing beats per-path overlays** for union views (Tufte v6) — operator's instinct was correct from the start. 3. **Material/Drive UI conventions exist** because they work — center collapse handles on borders, don't pile them in corners. 4. **Mobile = different problem than desktop** — bottom-sheet, no drag-grip near pull-to-refresh zone, asymmetric fitBounds padding, redundant refits to survive iOS URL-bar collapse. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
0986caaa44 |
fix(#1412): customizer nodeColors stops force-overriding ROLE_COLORS — CB presets now actually propagate (#1414)
WIP — red commit only. Reproduces #1412. ## TDD red phase `test-issue-1412-customizer-no-override.js` asserts that after `MeshCorePresets.applyPreset('deut')` and a server-config push of legacy `nodeColors`, `window.ROLE_COLORS.repeater === '#FE6100'`. On master this fails because `customize-v2.js:553` pushes server-config into the `_roleOverrides` map, which the live getter prefers over CSS vars. Green commit (customize-v2.js + customize.js fix) follows. Refs #1412 --------- Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
89410d58b4 |
fix(#1413): nav-left + nav-stats overlap at vw~1200 — flex sizing fix (#1417)
## What
Fix the horizontal overlap between `.nav-more-btn` (in `.nav-left`) and
`.nav-stats` (in `.nav-right`) at viewport widths roughly 1101..1599px.
At vw=1200 the count number in the stats badge rendered on top of the
"More ▾" text.
## Root cause
`.top-nav` uses `display: flex; justify-content: space-between;` but had
**no column gap** between its children, and `.nav-links` had **no
flex-grow**. So `.nav-left` only consumed its content's intrinsic width
and `.nav-right` (with `flex-shrink: 0`) was free to abut it. Worse, the
Priority+ measurement loop in `app.js` (`applyNavPriority` → `fits()`)
compared intrinsic widths against `window.innerWidth` while `.top-nav {
overflow: hidden }` masked the actual collision — so the loop happily
declared "fits" while pixels overlapped.
CDP measurement on master at vw=1200 (`/#/packets`):
- `.nav-more-btn` rect: x=499..557 (w=58)
- `.nav-stats` rect: x=496..962 (w=466)
- Gap: **−60.7px** (overlapping)
Fix candidates tested via Chrome DevTools Protocol (`Runtime.evaluate` +
`Emulation.setDeviceMetricsOverride`) across vw=1101, 1200, 1366, 1440,
1600, 1920 (plus 768, 900, 1024, 1080, 1100, 1300, 1500, 1700, 1800 as a
sanity sweep). Winner:
```css
.top-nav { column-gap: 16px; }
.nav-links { flex: 1 1 auto; min-width: 0; }
```
Per-viewport gap (`stats.left - more.right`) baseline → fix:
| vw | baseline | fix |
|------|----------|----------|
| 1101 | −144.0 | **16.0** |
| 1200 | −60.7 | **16.0** |
| 1300 | 8.4 | **16.0** |
| 1366 | 64.2 | 64.2 |
| 1440 | 0.0 | **44.5** |
| 1600 | 24.2 | 24.2 |
| 1920 | more hidden (no overflow) — n/a | n/a |
Single-candidate variants (`.nav-left { flex: 1 1 auto }` alone,
`.top-nav { justify-content: space-between }` alone — already on, no
effect, `.nav-links { flex: 1 1 auto }` alone, margin/padding hacks on
`.nav-right`/`.nav-stats`) all still produced ≤8px gap at vw=1200. Only
the combo (column-gap on parent + flex-grow on `.nav-links`) cleanly
resolves all six required widths.
## TDD
Red commit: `3d374b4c93319805e89e46d8fdc8a8ea8c6c1479` (CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26482870401)
- `test-issue-1413-nav-overlap-e2e.js` — Playwright at vw 1101, 1200,
1366, 1440, 1600, 1920 on `/#/packets`. Asserts `.nav-more-btn.right + 8
<= .nav-stats.left` (when both visible) and that `.top-nav` does not
horizontally scroll. Wired into `.github/workflows/deploy.yml` alongside
the other `test-nav-*-e2e.js` entries.
- Red commit ships ONLY the test (+workflow line); CI fails on the
assertion at vw=1101..1300 and vw=1440 (gap below 8px threshold).
- Green commit applies the two CSS rules above and turns CI green.
## Manual verification
1. Open `http://analyzer-stg.00id.net/#/packets` in a desktop browser.
2. Resize the viewport to ~1200px wide.
3. Confirm the "More ▾" button and the stats badge are visibly separated
(≥16px gap) and the badge count is not stacked on the button text.
4. Repeat at 1101, 1300, 1440, 1600, 1920px — gap ≥16px at all widths
where stats is visible.
5. At ≤1100px confirm `.nav-stats` is still hidden (display:none,
unchanged).
## Scope guards
- No changes to the Priority+ algorithm (`applyNavPriority` / `fits()`
in `app.js`). #1391, #1311, #1139, #1148, #1102, #1055 logic untouched.
- No changes to the More dropdown (`position: fixed`, #1406).
- No changes to `.nav-left { overflow }` (#1405 stayed dropped).
- Mobile (<768px) hamburger layout unchanged.
Fixes #1413
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
f72b1bd2ca |
fix(#1409): channels — stop force-enabling 'show encrypted' on every init (#1410)
## What
Delete the unconditional
`localStorage.setItem('channels-show-encrypted', 'true')` call (+
misleading "#1034 PR1: sectioned sidebar" comment) at
`public/channels.js:783-786`. The sectioned-sidebar grouping the comment
referenced was never implemented; in practice the call was
force-flipping the encrypted-visibility gate on every init so an
operator could never turn it off.
## Root cause
`channels.js` init ran:
```js
var showEncrypted = true;
try { localStorage.setItem('channels-show-encrypted', 'true'); } catch (e) {}
```
unconditionally on every load. The `loadChannels()` reader at line ~1563
(`localStorage.getItem('channels-show-encrypted') === 'true'`) then sent
`includeEncrypted=true` on the `/api/channels` call, so the server
returned all 246 encrypted placeholder channels alongside the 19 real
ones — 265 rows flooding the sidebar with no UI control to suppress.
Verified via CDP on staging:
- `localStorage['channels-show-encrypted']` was always `"true"` after
page load.
- `GET /api/channels` → **19** entries (default — encrypted excluded).
- `GET /api/channels?includeEncrypted=true` → **265** entries (246
encrypted).
- Manually `removeItem('channels-show-encrypted')` + reload → list
dropped to 19.
Confirmed the force-set was the only gate driving the flood.
## TDD
- RED commit `a71cecbc` — `test-issue-1409-no-encrypted-flood.js`
source-greps `public/channels.js` for the forbidden literal
`setItem('channels-show-encrypted', 'true')`. Asserts no match. Fails on
master.
- GREEN commit `14281b63` — delete the 2 lines + rewrite comment. Test
passes.
Tests:
```
$ node test-issue-1409-no-encrypted-flood.js
Issue #1409 — no force-enable of channels-show-encrypted
✅ channels.js does NOT unconditionally setItem(channels-show-encrypted, true)
✅ channels.js still reads channels-show-encrypted (toggle gate preserved)
2 passed, 0 failed
```
## Manual verification
- After fix, default `localStorage.getItem('channels-show-encrypted')`
is `null` on first load.
- `loadChannels()` reader returns `false`, so `includeEncrypted` is
omitted from the API call → server returns the 19 real channels only.
- Existing reader is preserved, so a future user-facing toggle that
writes the flag will continue to work.
## Out of scope (follow-ups)
- "Show encrypted" header toggle UI — issue acceptance criteria mentions
it as optional; not added here.
- Sectioned-sidebar grouping of encrypted channels (#1034 PR1 design) —
separate issue.
- Cap/collapse behavior when toggle is ON — separate issue.
Fixes #1409
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
52b6dd82ac |
fix(#1407): cb-preset propagation via live ROLE_COLORS getter + per-role text color for WCAG AA (#1408)
WIP — RED commit only. Tests demonstrate two bugs from #1407: 1. `window.ROLE_COLORS` is a static literal (legacy April palette), not synced to `--mc-role-*` CSS vars. 2. Achromat preset pairs `#1a1a1a` text with 3 dark grays → WCAG 1.4.3 fails (1.27 / 2.55 / 4.43). Expect CI red on `test-issue-1407-cb-preset-propagation.js` assertion failures (not compile errors). GREEN follows. Refs #1407 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
9b0a4ee054 |
fix(nav): .nav-more-wrap contain:layout — open dropdown inflated parent flex line, clipped nav offscreen (#1406)
ACTUAL root cause of the recurring nav-vanishing bug, validated live via Chrome CDP probe on staging at vw=1030. ## What happens When the More dropdown opens: - BEFORE: nav_links.y = 2.67, nav_left.scrollHeight = 47, nav visible ✅ - OPEN: nav_links.y = -46.67, nav_left.scrollHeight = 279, nav clipped offscreen ❌ The .nav-more-menu is position:absolute but its content extents inflate .nav-more-wrap.scrollHeight. .nav-left { display:flex; align-items:center } then centers a 279px content line in a 52px container, putting everything above the visible band. ## Fix Add contain:layout to .nav-more-wrap — isolates its layout box from the parent flex calculation. No more bubble-up. CDP verification with the fix applied: dropdown opens, all 6 items render at proper y (56, 93, 130, 166, 203, 240), nav_links_y stays at 2.67, nav_left.scrollHeight stays at 47. ## Why prior 22 fixes didn't catch it Every prior fix treated symptoms — Priority+ algorithm tweaks, overflow flag toggles, min-height drops, etc. None instrumented the CLOSED→OPEN state transition that reveals the flex-line bug. Required Chrome DevTools Protocol on a real broken viewport to see the inflate happen live. Fixes #1406 and likely supersedes #1391, #1396, #1400, #1404. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
ae77d58ec5 |
fix(#1403): drop .nav-left overflow:hidden — root cause of nav vanishing + truncated More dropdown (#1405)
Root cause of the recurring nav-vanishing family of bugs — confirmed
live via operator console probe at vw=1030 on /#/channels (also
reproduces on /#/home, /#/packets, all routes).
## Symptoms
1. All `.nav-links` (Home, Packets, Map, Live, Channels, Nodes) and
brand + More button render OFFSCREEN above the visible top-nav band.
`.nav-left` reports y=0..52 but every child reports y=-47.5.
2. More dropdown when opened shows only ONE item ("Tools") instead of
the 6 expected (Channels, Tools, Observers, Analytics, Perf, Audio Lab).
## Root cause
`.nav-left { overflow: hidden }` at `public/style.css:509`. With flex
children whose effective layout exceeds the container box, Firefox clips
children to negative y. The same `overflow: hidden` ALSO clips the
descendant `.nav-more-menu` dropdown contents.
## Fix
Drop `overflow: hidden` from `.nav-left`. The original
horizontal-overflow guard from #1066 is preserved at the `.top-nav`
level (which still has `overflow: hidden`).
## Verification
Operator console probe after applying the same `overflow: visible`
in-page:
- All 6 visible nav links render at y >= 0 inside the top-nav.
- More dropdown contains all 6 expected items (Channels, Tools,
Observers, Analytics, Perf, Lab).
- Both bugs collapse into ONE root cause.
## Why prior fixes didn't catch this
- #1400 fixed `.nav-link { min-height: 48px }` overflow — reduced
children from 56px to 47px tall. Helped slightly but didn't address the
`.nav-left { overflow: hidden }` interaction.
- #1391, #1394 fixed the active-pill-in-overflow algorithm. Different
layer.
- #1311, #1148, #1106, #1102, #1097, #1067, #1055 — every prior
Priority+ fix treated overflow as an algorithmic question, never as a
CSS clipping bug at the container level.
22nd nav fix in this saga. This one targets the actual cause.
Refs #1391, #1396, #1400. Operator probe transcript available on
request.
Fixes #1403
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
7e492a71a0 |
fix(#1400): root cause of recurring nav-vanishing — min-height:48px overflowed 52px top-nav, clipped link strip above viewport (#1401)
**RED commit phase** — TDD failing test for #1400. Green fix incoming next push. See full PR body on ready-for-review. Fixes #1400 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
902f9c4976 |
revert(#1398): nav-instrumentation banner broke page load (#1399)
Reverting PR #1398 — the navdebug banner instrumentation caused pages to hang on load on operator's device. Will respawn safer diagnostic. Refs #1396. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
7f5cc96bd9 |
chore(debug-1396): nav-instrumentation banner — gated on hash ?navdebug=1 (#1398)
## Summary Temporary diagnostic patch for #1396 (mobile / narrow-desktop nav priority reports). Adds a single instrumentation block at the END of `applyNavPriority()` in `public/app.js`, gated on `navdebug=1` appearing in the URL hash. No nav behavior change; reverted once root cause is known. ## What it does When the URL hash contains `navdebug=1` (e.g. `/#/channels?navdebug=1`), the function: 1. Paints a fixed-position green-on-black banner pinned to the bottom of the viewport (`z-index:99999`, `pointer-events:none` so it never blocks interaction) showing: ``` [NAV-DEBUG-1396] vw=<innerWidth> total=N visible=N overflow=N hidden-by-css=N active=<label> visible: [Home,Packets,...] overflow: [Tools,...] ua: <first 80 chars of UA> ``` 2. Emits the same payload via `console.warn('[NAV-DEBUG-1396]', ...)` for anyone who can pop devtools. The whole block is wrapped in `try/catch` — diagnostic code never breaks nav. ## Why a banner (not just console) Affected reporters are on mobile devices where popping devtools is annoying or impossible. A screenshot of the banner gives us: - Viewport width (vs the 768 / 1100 / 1101 breakpoints) - Device UA (Safari iOS quirks, narrow Android, etc.) - Actual link counts after `applyNavPriority` ran - Whether anything is hidden by CSS (`display:none`) despite not being in the overflow set - Which labels are inline vs in the More menu - Active route at time of measurement ## Operator usage On the affected device, open: ``` https://<staging-host>/#/channels?navdebug=1 ``` (or any other route; the gate is hash-wide). Screenshot the green-on-black banner at the bottom of the page and attach to #1396. ## Hard rules respected - Banner is gated — never visible without `navdebug=1` in the hash. - No new dependency. - No change to nav behavior. - Diagnostic-only; revert PR will follow once root cause is identified. ## Out of scope - Root-cause fix for #1396 (this is purely instrumentation). - E2E test for the banner — code is temporary and scheduled for revert. Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
f0a7ed758f |
fix(#1391): Priority+ nav — active-route pill must NEVER drop high-priority links into orphaned More dropdown (#1394)
## What
Pins the active-route `.nav-link` inline at any viewport ≥768px so
Priority+ never shoves it into the More dropdown. Fixes the operator's
screenshot of `/#/perf` at ~1080px where the navbar showed only the
active "Perf" pill missing — and an inverse failure where the active
pill was the only thing **in** the dropdown.
This is the 20th regression of nav Priority+. Single-loop fix only; no
algorithm redesign (per issue out-of-scope).
## Root cause
`public/app.js` `applyNavPriority()` had two places that ignored the
active state:
1. **≤1100 narrow-desktop CSS branch (line ~1197):** `if
(a.dataset.priority !== 'high') a.classList.add('is-overflow')` blindly
overflowed every non-high link — including the active pill.
2. **>1100 measurement loop (line ~1267):** `overflowQueue` is `non-high
reversed + high reversed`. The active non-high link enters the queue and
the loop's only break condition is `priority === 'high'`. fits() keeps
returning false (active pill is wider — has the `.active`
background/padding), so the loop walks the entire non-high tail and
orphans the active route in More.
The acceptance criterion "Active-route pill MUST always be visible
inline" was never encoded — #1311's floor only protected
`data-priority="high"`.
## Why prior #1311 / #1148 / #1139 floors didn't catch this
- **#1311** floored at `data-priority="high"` only. `/#/perf` is
`data-priority=""` so it had no protection.
- **#1148 / #1139** floored the *More menu* at ≥2 items but didn't
constrain *which* links could be promoted/dropped.
- **#1106** narrow-desktop CSS branch (≤1100) was written before
active-pill width drift was a known issue.
## Fix
One conceptual rule applied at three points:
1. In `overflowQueue` construction, skip any link with `.active` (treat
active like high-priority — never enqueue).
2. In the ≤1100 CSS branch, skip the active link when assigning
`.is-overflow`.
3. In the >1100 loop, also break on `.active` (defensive — queue already
excludes it).
Approach chosen over "pin active-pill max-width during measurement":
measurement-pinning would silently shrink the pill visually mid-resize,
and width drift from #1378's new `--mc-*` vars made that fragile.
Treating active as a hard inline pin matches the documented contract and
is one greppable invariant.
## TDD red → green
- **Red commit `34d69012`:** added `test-nav-priority-1391-e2e.js`
covering `/#/perf, /#/audio-lab, /#/analytics, /#/observers` at `1024,
1080, 1100, 1101, 1200, 1300px`. Asserts (1) active pill not in
overflow, (2) all 5 high-pri still inline (#1311 guard), (3) every
overflowed link mirrored in More dropdown (no orphans). 0/24 passed
locally on red.
- **Green commit:** same test 24/24 pass. Existing #1311 (20/20), #1139
floor, #1102 contract still green.
## Manual verification
Local fixture server (`./corescope-server -port 13581 -db
test-fixtures/e2e-fixture.db -public public`):
- `/#/perf` @ 1080×800: brand + 5 high-pri inline + "Perf" pill inline +
"More ▾" containing the 5 low-pri links (Channels, Tools, Observers,
Analytics, Audio Lab). ✅
- `/#/perf` @ 1300×800: brand + 5 high-pri + "Perf" inline; More hidden
(only 4 low-pri items overflow). ✅
- `/#/perf` @ 800×800 (narrow): hamburger code path untouched. ✅
- Inverse `/#/home` @ 1080×800 (active IS high-pri): no behaviour
change. ✅
## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— exit 0.
Browser verified: local fixture server + Playwright on Chromium
(`/usr/bin/chromium`).
E2E assertion added: `test-nav-priority-1391-e2e.js:138-148`
(`activeOverflowed === false`).
Fixes #1391
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
|
||
|
|
9a2270168f |
feat(#893): Material Design dark mode toggle — polished version of #893 (#1389)
## Polished version of #893 This PR carries forward @emuehlstein's Material Design dark-mode toggle from #893, rebased onto current `master` and polished for a11y / first-paint / forced-colors / cross-tab sync. Original commits (preserved as `Co-authored-by`): - `feat: replace dark mode button with Material Design toggle switch` (emuehlstein) - `fix: define --shadow CSS var in theme blocks, drop stopPropagation no-op` (emuehlstein, addressing prior review) #893 had been stuck in CONFLICTING state since 2026-05-24 with no CI runs ever. Rebase resolved a single `public/style.css` `:root` conflict (preserved both the `--text-primary`/`--bg-hover`/`--primary` aliases from #1378 and the new `--shadow` definition). ## Polished improvements (on top of #893) 1. **FOUC fix** (`public/index.html`): inline `<head>` script reads `localStorage('meshcore-theme')` (or `prefers-color-scheme`) and sets `data-theme` *before* stylesheet load. Without this, dark-mode users see a light-mode flash on every page load. 2. **ARIA semantics** (`public/index.html`): moved `aria-label` from the wrapping `<label>` onto the actual `<input role="switch">`. Removed `aria-hidden="true"` from the checkbox (which had been hiding it from assistive tech). Added `aria-hidden` to the decorative track instead. 3. **Keyboard focus indicator** (`public/style.css`): `:focus-visible` on the (visually-hidden) checkbox draws an outline on `.theme-toggle-track`. Previously keyboard users could focus the toggle with Tab but had no visible indicator. 4. **Reduced motion** (`public/style.css`): `@media (prefers-reduced-motion: reduce)` disables the slide/fade transitions. 5. **Forced-colors mode** (`public/style.css`): explicit `CanvasText` border on track + thumb so the switch stays visible in Windows High Contrast. Default CSS tokens collapse to `Canvas`/`CanvasText` and the thumb would otherwise disappear. 6. **Cross-tab sync** (`public/app.js`): `storage` event listener for `meshcore-theme` mirrors the cb-presets pattern from #1378 — toggling theme in one tab now syncs all open tabs. 7. **Tightened E2E test** (`test-e2e-playwright.js`): added assertions for `role="switch"`, checkbox-state ↔ theme parity, and theme persistence across a full page reload (was only asserting one toggle). ## Notes - No `map[string]interface{}` (no Go changes). - All colors via existing `--mc-*` / theme tokens; `--shadow` is defined in both light + dark theme blocks. - No layout shift (track is fixed `46x24` inside the `44x44` label container). - Branch scope is exactly the four files from #893: `public/app.js`, `public/index.html`, `public/style.css`, `test-e2e-playwright.js`. Closes #893. Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com> --------- Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com> Co-authored-by: CoreScope Bot <bot@corescope> |
||
|
|
ff0ee50354 |
fix(#1374): packet-route map modernized — role-aware markers, directional edges, WCAG 2.2 AA (#1381)
## What The packet-route map view (`/#/map?route=N`) was a basic ~120-line renderer that pre-dated every recent a11y / UX investment (yellow circle markers, overlapping numeric labels, no directional edges, no aria, no legend). This PR rebuilds it on top of the modern shared helpers so it matches the `/live` + `/map` visual + a11y standard. Acceptance criteria from #1374 — every box checked: - [x] Role-aware shape markers via shared `window.makeRoleMarkerSVG` (post-#1357). - [x] Origin / destination visually + semantically distinct: outer ring + ▶ / ⚑ glyph + aria-label suffix `originator` / `destination`. - [x] Sequence-number badges (`.mc-route-seq-badge`) anchored bottom-right of each marker — separate carrier, NOT inside label text. - [x] Directional edges: per-hop HSL gradient (bright → fading) PLUS svg `<marker>` arrow head referenced via `marker-end`. Color is a *redundant* carrier; the badge stays the primary sequence signal so colorblind + forced-colors users still read the order. - [x] Per-edge `aria-label="Hop N → N+1, ~Xkm"` (haversine computed). - [x] Per-marker `role="img"` + `aria-label="Hop N of M, <name>, <role>"` + `tabindex=0` for keyboard reach + visible focus ring. - [x] Label deconfliction reuses `window.deconflictLabels` (now exposed by `map.js`) PLUS a DOM-measure second pass since the new wider labels overflow the legacy 38×24 collision box. - [x] Collapsible `.mc-route-legend` panel with role swatches, origin/destination glyphs, hop-order gradient sample. Toggle has `aria-expanded`. - [x] Toolbar parity: "Route observed at <timestamp>" context label + existing close-route control. - [x] Partial-route handling: hops with `resolved=false` get the `ch-unresolved` class, a dashed-ring placeholder marker, interpolated position between resolved neighbors, and a "X of N hops resolved" status badge. - [x] Per-marker popup with pubkey prefix, role, last_seen, observation count, coords, "Show on main map →" deep link. - [x] `prefers-reduced-motion: reduce` disables animations/transitions. - [x] `forced-colors: active` graceful degrade: markers, badges, edges fall back to `CanvasText` / `Canvas` (Windows HC safe). ## How Split the renderer into a dedicated `public/route-render.js` exposing `window.MeshRoute.render(map, layer, positions, opts)`. The existing `drawPacketRoute` in `map.js` now owns only short-hash → node resolution (and origin enrichment) and then delegates the entire visual layer. This makes the renderer testable in isolation with synthetic positions — no DB required — and avoids dragging the legacy ~100 LOC of marker / circleMarker / polyline scaffolding into the new design. Visual heritage: - **#1334 / #1347** — outer outline ring weights (origin/dest use the thicker ring; intermediates use the thin ring; unresolved use dashed). - **#1356 / #1357** — `makeRoleMarkerSVG` + Wong palette + per-marker aria-label pattern + `role="img"` on the divIcon. - **#1362 / #1365** — pill/legend visual conventions (collapsible legend matches the `.mc-section` accordion language users already know from `/map`). ### WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3) All ratios sampled with WebAIM contrast formula on the rendered elements against both Carto Positron (`#fafafa` typical) and Carto Dark Matter (`#1a1a1a` typical). | Element | SC | Ratio (Positron) | Ratio (Dark Matter) | Pass | |--------------------------------------------|----------|------------------|---------------------|------| | Sequence badge text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Sequence badge border `#1a1a1a` | 1.4.11 | 17.6:1 | 12.6:1 | ✅ | | Marker outer ring `#06b6d4` (origin) | 1.4.11 | 3.2:1 | 4.6:1 | ✅ | | Marker outer ring `#ef4444` (destination) | 1.4.11 | 3.8:1 | 4.4:1 | ✅ | | Marker outer ring `#666` (intermediate) | 1.4.11 | 5.7:1 | 3.7:1 | ✅ | | Edge stroke (seq color, mid: `#56c08c`) | 1.4.11 | 3.0:1 (min) | 3.1:1 | ✅ | | Edge arrow head (currentColor) | 1.4.11 | same as edge | same | ✅ | | Label text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Legend body text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Resolved badge `#78350f` on `#fef3c7` | 1.4.3 AA | 8.4:1 | 8.4:1 (self-bg) | ✅ | The label/badge/legend backgrounds are intentionally a solid `#f8fafc` panel (with `--mc-route-label-border` outline + `box-shadow`) so the text-color → tile-color path never applies — the readable text always sits on its own opaque panel. For SC 1.3.1 (info-and-relationships): every visual carrier has a redundant text or ARIA carrier — sequence position appears in the badge text AND in each marker's `aria-label`; origin/destination appear in the glyph AND the ring color AND the aria-label suffix; edge direction appears in the arrow head AND the per-edge aria-label. ### TDD - **Red commit:** `9e4f58e5547720ff3fcf8695a6c325958904683a` (CI: https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks) — adds `test-issue-1374-route-map-a11y-e2e.js` only. The test calls `window.MeshRoute.render(...)` directly with synthetic Bay-Area positions at mobile (375×800) AND desktop (1920×1080), asserts every acceptance criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes the partial-route fixture. Fails on the assertions because `MeshRoute` doesn't exist on master. - **Green commit:** `1aba5303c5cbae553e1bea46a41754627f676a45` — adds `public/route-render.js`, refactors `drawPacketRoute` to delegate, adds `.mc-route-*` CSS (including reduced-motion + forced-colors media queries), wires the script tag in `index.html`, and wires the test into `.github/workflows/deploy.yml`. ### Visual verification 20/20 assertions pass locally (`CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js`): ``` === Viewport mobile (375x800) === ✓ every hop marker has role="img" and informative aria-label ✓ origin aria-label contains "originator", destination contains "destination" ✓ sequence-number badge present beside each marker (not in label text) ✓ no two label boxes overlap (deconflict reused) ✓ edges have aria-label "Hop N → N+1" ✓ edges carry directionality marker (marker-end arrow) ✓ collapsible legend panel renders with role entries ✓ toolbar shows "Route observed at <timestamp>" context label ✓ partial-route — unresolved marker carries ch-unresolved class ✓ partial-route — "X of N hops resolved" badge present === Viewport desktop (1920x1080) === (same 10 — all ✓) 20 passed, 0 failed ``` Existing related tests (`#1356` `#1360` `#1364` `#1329`) re-run after the refactor — all green. ## Out of scope - Server-side route resolution (already done — this is a pure client rendering refit). - Multi-route view / 3D / globe — explicitly excluded by the issue. - Backend untouched — `cmd/server` + `cmd/ingestor` not modified. Fixes #1374 --------- Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
101c11b4b3 |
fix(#1361): theme customizer — colorblind presets [WIP] (#1378)
WIP — draft PR for CI to exercise the RED test commit. Will be promoted
out of draft once the GREEN commit lands.
Red commit:
|
||
|
|
ec98a43d68 |
feat(ci): frontend eslint no-undef gate — catches renamed-function-caller class of bugs (fixes #1342) (#1344)
**TDD:** red commit `03ea965` (canary undef var → CI fails) → green commit `b514aeb` (canary removed → CI passes). CI URL appears in the Checks tab once GitHub Actions queues this branch. `Fixes #1342` ## What ships - **`.eslintrc.json`** at repo root — eslint 8 legacy-config format. `no-undef: error`, `no-unused-vars: warn` (with `^_` allowlist). - **CI step** in `.github/workflows/deploy.yml` (job `go-test`, after JS unit tests, before proto + Playwright): `npm install --no-save eslint@8 && npx eslint public/*.js`. `--no-save` keeps `node_modules` and `package-lock.json` out of the tree (already gitignored). - **One pre-existing fix** in `public/map.js`: `typeof esc === 'function'` → `typeof globalThis.esc === 'function'`. `esc` is a *local* IIFE var in 5 other files, never exported as a true global; the optional lookup was structurally invalid under `no-undef`. Behavior unchanged. ## How this would have caught #1318 / PR #923 PR #923 renamed `drawAnimatedLine`, updated one caller in `public/live.js`, missed the other — leaving a reference to the undefined `hash` var. Playwright didn't hit that path. Reverting #1325 locally (re-introducing the bug) → eslint flags `hash` as `no-undef` → red. With the gate in place, #923 never lands. ## The "quiet pile of globals" reality The config declares **257 globals**. They were discovered by walking `public/*.js` for two patterns: 1. `window.X = ...` assignments (the explicit exports — 168 of them) 2. Top-level `function`/`const`/`let`/`var` declarations in non-IIFE files (the implicit exports — Go-style cross-file linking via shared HTML `<script>` order) Plus 9 vendor/runtime names (`L`, `Chart`, `QRCode`, `qrcode`, `module`, `global`, `process`, `require`, `exports`, `__filename`, `__dirname`) for dual-runtime files like `url-state.js`, `packet-filter.js`, `hash-color.js`, `filter-ux.js` that are also `require()`-d by Node tests. This is honest documentation of an architectural reality, not a workaround. Future refactor → modules will collapse this list. ## Latent bugs discovered **Zero `no-undef` errors against the current `public/*.js` tree** after globals were enumerated honestly. The would-be-#1318-class bug count today: 0. The gate's job is forward-looking — block the next one. ## Out of scope (acknowledged from acceptance criteria) - Inline `<script>` blocks in `public/*.html` — separate ticket. - Per-PR delta-coverage gate — separate ticket. - pr-preflight grep for arg-count mismatch — separate ticket. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → exit 0, clean. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
791c8ae1bc |
fix(#1367): channels page chat-app redesign — restore prod row layout, drop analytics chip, add detail view (#1376)
Red commit:
|
||
|
|
bfebf200b7 |
fix(#1375): scope-stats fetch path — drop duplicate /api prefix (Scopes tab JSON.parse fix) (#1379)
## What Drop the leading `/api` from the Scopes-tab `scope-stats` fetch in `public/analytics.js`. The `api()` helper already prefixes `/api`; passing `/api/scope-stats` produced a runtime URL of `/api/api/scope-stats`, which 404s, falls through to the SPA HTML, and crashes the Scopes tab with `JSON.parse: unexpected character`. Single-line behavior change. ## Why `api()` (defined earlier in the same file) prepends `/api`. Every other caller in `public/analytics.js` correctly passes a helper-relative path (`/observers`, `/nodes`, …). The Scopes loader was the lone offender. The same fix originally landed on the PR #915 branch (commit `2fd22cee`) but that branch never merged, so the bug resurfaced on subsequent rebases. The Scopes tab is therefore broken on production today — open `/analytics` → Scopes and the panel never renders. ## TDD - Red commit `b1fbc5601a985f20eb0ffee9181b7df5333248ca` adds `test-issue-1375-scope-stats-fetch.js`, which reads `public/analytics.js` and asserts: - ZERO matches of literal `api('/api/scope-stats'` (regression guard). - Exactly one match of `api('/scope-stats'` (positive — fix present). - Green commit edits the loader to drop the duplicate `/api`. - Test wired into `.github/workflows/deploy.yml` next to the existing `test-issue-*` entries. ## Manual verification After deploy, open `https://analyzer.00id.net/analytics`, click **Scopes**: panel renders cards instead of throwing a JSON parse error in DevTools console. Fixes #1375 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
91d90d48fb |
fix(#1364): drop over-aggressive .mc-pill max-width — restore multi-digit count visibility (#1365)
Red commit:
|
||
|
|
40aa02b438 |
fix(#1360): cluster pill shows letter+count — restore count visibility regressed by #1357 (#1362)
Red commit: |
||
|
|
933ef4e6ef |
fix(#1356): WCAG 2.2 AA map a11y — cluster bubbles, role pills, multi-byte labels (#1357)
Red commit:
|
||
|
|
0d131808d4 |
fix(map): thinner always-on marker outline — was dominating at zoomed-out levels (#1347)
## Operator feedback on #1334 PR #1334 (the #1293 marker a11y change) added a baked-in white outline at `stroke-width=2` to every node marker via `makeRoleMarkerSVG`. Operator reports it's too heavy and dominates the map at zoomed-out levels — every node reads as a "big white blob with a colour core", which actually drowns out the per-role shape silhouette at the exact zoom levels where the shape distinction matters most. ## Fix Drop the always-on stroke from **2 → 1** across all marker producers: | Producer | Before | After | |----------|--------|-------| | `public/roles.js` `makeRoleMarkerSVG` (circle / square / triangle / diamond / hexagon) | `stroke-width="2"` | `stroke-width="1"` | | `public/roles.js` `makeRoleMarkerSVG` (star branch) | `stroke-width="1.5"` | `stroke-width="1"` | | `public/live.js` `addNodeMarker` inline fallback SVG | `stroke-width="2"` | `stroke-width="1"` | | `public/map.js` `makeMarkerIcon` switch (all shapes) | `stroke-width="2"` / `"1.5"` | `stroke-width="1"` | | `_highlightRing` (pulse on selected/active) | `weight: 3 → 2` | **unchanged** | The highlight ring used by `pulseNodeMarker` is the one place where a heavy outline carries real signal (selected state), so it stays at weight 3 → 2. The always-on shape stroke is now just enough to keep silhouettes distinct on both Carto dark and light basemaps without dominating the surrounding terrain. ## Constraints preserved - Shape variation (#1293) — per-role shapes still rendered, helper untouched except for stroke width. - Colorblind palette — fills/colors unchanged, all via CSS variables / `ROLE_COLORS`. - Highlight ring still visible — pulse weight ≥ 2 retained and asserted. ## Tests New: `test-marker-outline-weight.js` (added to `test-all.sh` unit suite) - Asserts every `stroke-width` literal in `makeRoleMarkerSVG` is `<= 1`. - Asserts `live.js` inline fallback SVG `stroke-width <= 1`. - Asserts the `_highlightRing` (`ringHl.setStyle({ weight: N })`) keeps at least one `weight >= 2` so highlight stays visible. Red commit (`d17cfcc`) fails on assertion; green commit (`6cfe99b`) flips it. Existing `test-issue-1293-marker-shapes.js` still passes — the shape-variation and outline-ring highlight contracts are intact. --------- Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
0f7c03ccaf |
fix(#1293): role-aware marker shapes + outline-ring highlight (#1334)
Fixes #1293 ## What Marker shape now varies per role (WCAG 1.4.1 — colour is no longer the only carrier of role identity), and the live map's selection/highlight no longer stacks same-colour concentric markers. | Role | Shape | Why | |-----------|----------|-----| | repeater | circle | default, most common | | companion | square | flat sides, easy to distinguish from circle | | room | hexagon | tessellation hint = group | | sensor | triangle | "alert-like" silhouette | | observer | diamond | network-infrastructure suggestion | Existing role colours are preserved; the shape is the new differentiator so red/green colourblind operators can still tell roles apart. ## How - `public/roles.js`: new `window.ROLE_SHAPES` map (single source of truth), `ROLE_STYLE.shape` synced, shared `window.makeRoleMarkerSVG(role, color, size)` helper that emits self-contained `<svg>` strings — including a new `hexagon` branch. - `public/map.js`: `makeMarkerIcon` switch picks up the `hexagon` case. - `public/live.js`: `addNodeMarker` now builds an `L.divIcon` via `makeRoleMarkerSVG` (was a flat `L.circleMarker` — colour only). A hidden stroke-only `_highlightRing` is allocated per marker; `pulseNode` grows + fades that ring instead of recolouring the marker fill, so the blue-on-blue concentric stacking the issue called out cannot occur. `rescaleMarkers`, `pruneStaleNodes`, matrix mode toggling now drive the divIcon via small DOM helpers. - `public/live.js` role legend: emits SVG shape + colour swatch (was a bare coloured dot). - `public/live.css`: `.live-shape-swatch` wrapper for the SVG legend swatches. ## TDD Red commit: `7e5e2d95` — `test-issue-1293-marker-shapes.js` asserts the shape map, helper, hexagon branches, divIcon switch in `addNodeMarker`, SVG-based legend, and outline-ring highlight (no same-colour fill overlay). Wired into `deploy.yml` JS unit tests. Green commit: `fb33ca96`. ## Design check Coblis simulator (deuteranopia / protanopia / tritanopia) — reviewer to run on the staging build; shapes carry the signal independent of hue, so all role categories should remain distinguishable. Existing colours are retained per the issue's "keep colours, vary shape" guidance. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all gates pass. --------- Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
adcf29dd6b |
fix(#1329): accordion map controls on mobile, drop 200px scroll cap (#1333)
## Summary
On mobile (≤640px) the Map controls panel was capped at `max-height:
200px` and forced an internal scrollbar through all the
layer/filter/display toggles. This makes every section a single-open
accordion and drops the cap, so the visible content always fits without
internal scroll.
## Changes
- `public/map.js` — Each `fieldset.mc-section` legend becomes a tappable
`aria-expanded` toggle. On mobile the first section opens by default;
activating any other section auto-closes the previously open one
(single-open). Desktop still renders all sections expanded.
- `public/style.css` — `@media (max-width: 640px)` rules:
- `max-height: 200px` → `calc(100vh - 80px)`.
- `.mc-collapsed > *:not(legend) { display: none }` hides bodies of
collapsed sections.
- Legend styled as flex row with ▸/▾ indicator (colors via
`var(--text-muted)`).
- All new rules live inside the mobile media query, so desktop layout is
unchanged.
## Test
`test-issue-1329-map-controls-accordion-e2e.js` (added to CI in
`deploy.yml`):
- mobile 375x812: ≥1 accordion toggle present, ≤1 expanded by default,
no internal scroll, clicking another toggle collapses the first.
- desktop 1280x800: `position: absolute`, panel <50% viewport wide, all
controls visible.
Red commit: `85fdc25267eaf210369371f55da767016435dbff` (test fails on
master — no accordion toggles exist; all fieldsets render expanded under
the 200px cap forcing scroll).
E2E assertion added: `test-issue-1329-map-controls-accordion-e2e.js:56`.
Fixes #1329
---------
Co-authored-by: openclaw-bot <bot@openclaw.dev>
|
||
|
|
92df28a569 |
fix(touch-gestures): stamp data-hash on Trace and Filter buttons (#1305) (#1332)
## Summary
Row-overlay Trace and Filter buttons silently did nothing on touch
swipes. `ensureRowOverlay` stamped `data-hash` only on the Copy button,
while `onClickAction` gates both `trace` and `filter` navigation on
`hash && ...` — so the click handler short-circuited before
`location.hash` was set. Users saw the buttons but tapping them was a
no-op.
## Fix
`public/touch-gestures.js` — in `ensureRowOverlay`, stamp `data-hash` on
all three buttons (Trace, Filter, Copy) from the same source the Copy
button already used (`row.getAttribute('data-hash') ||
row.getAttribute('data-id')`). One-line factoring of the attribute
fragment to avoid duplicating the escape logic.
Behavior after fix:
- Trace → `#/packets/<hash>`
- Filter → `#/packets?hash=<hash>`
- Copy → clipboard (unchanged)
All three match the existing branches in `onClickAction`.
## TDD
- **RED commit** (`dd90f72c`): removes the cov1/cov2 workaround in
`test-touch-gestures-coverage-e2e.js` that artificially stamped
`data-hash` on trace/filter buttons from the test harness. With this
commit alone, cov1/cov2 fail their `location.hash` assertions because
`onClickAction`'s guard short-circuits.
- **GREEN commit** (`a526c30f`): production fix in `ensureRowOverlay`.
cov1/cov2 now pass natively against the real production code path with
no harness-side stamping.
## Browser verified
Coverage E2E (`test-touch-gestures-coverage-e2e.js`) exercises the real
swipe → overlay → button-click → navigation path in headless Chromium
against the running server. cov1 asserts `location.hash ===
#/packets/<hash>`, cov2 asserts `location.hash ===
#/packets?hash=<hash>` — these assertions are the regression gate.
E2E assertion added: test-touch-gestures-coverage-e2e.js:227 (cov1
trace) and test-touch-gestures-coverage-e2e.js:259 (cov2 filter).
## Preflight
All hard gates and warnings pass.
Fixes #1305
---------
Co-authored-by: openclaw <bot@openclaw>
|
||
|
|
fb63236572 |
fix(mobile): expose dark/light toggle in More sheet on narrow viewports (#1327)
## Summary - `#darkModeToggle` sits inside `.nav-right` which is `display: none !important` at ≤768px — mobile users had no way to switch themes - Adds a **Dark mode / Light mode** button at the bottom of the More sheet, separated from the route list by a hairline rule - Click delegates to `#darkModeToggle` so `app.js` remains the single owner of all theme logic (no duplication) - Icon (`🌙` / `☀️`) and label sync on every sheet open and after each toggle ## Test plan - [ ] Mobile (≤768px): open More sheet → "Dark mode" / "Light mode" button visible at the bottom - [ ] Tap button → theme toggles, sheet closes, icon/label update correctly on next open - [ ] Tap button repeatedly → theme keeps toggling correctly - [ ] Desktop (>768px): no visual change, `#darkModeToggle` in top-nav still works normally - [ ] `prefers-reduced-motion`: no transitions (inherited from existing sheet-item rule) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
7b36968554 |
fix(nav): add missing nav-drawer.css — drawer rendered inline at page bottom (#1326)
## Summary - `nav-drawer.js` was wired up in `index.html` (issue #1064) but `nav-drawer.css` was never created - Without `position: fixed` and `transform: translateX(-100%)` the `<aside class="nav-drawer">` rendered as a visible inline block at the bottom of every page, showing **"Navigate×"** followed by the route list - Adds the missing stylesheet with proper slide-over layout, backdrop, transition, and `display: none` guard at ≤768px (bottom-nav More tab covers those routes) ## Test plan - [ ] Desktop (>768px): "Navigate×" bar no longer visible at bottom of any page - [ ] Desktop: left-edge swipe/touch still opens the drawer and it slides in from the left - [ ] Mobile (≤768px): nav drawer fully hidden, bottom-nav More tab unchanged - [ ] Dark mode and light mode: drawer uses the correct `--nav-bg` / `--nav-text` tokens - [ ] `prefers-reduced-motion`: transitions disabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
345788b383 |
fix(live): pass pktMeta.hash to drawAnimatedLine — merge artifact from #923 broke line animation (#1325)
## Summary - `animatePath` signature changed from `(..., hash)` to `(..., pktMeta)` when #923 was merged - The `drawAnimatedLine` call inside `nextHop()` still referenced the bare `hash` variable, which is no longer in scope - This causes a `ReferenceError` on every hop iteration, aborting the chain after the first pulse dot — **animated lines never draw**, only blinking dots appear ## Fix Replace `hash` → `pktMeta?.hash` on the single affected `drawAnimatedLine` call (line 2891 in `public/live.js`). ## Test plan - [ ] Open MESH LIVE page with live MQTT data flowing - [ ] Confirm animated path lines draw between nodes (not just blinking dots) - [ ] Confirm clickable path popups still work (pktMeta.hash still passed correctly) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
317b59ab10 |
feat: area-based visual node filter — attribute packets by transmitter GPS (#804) (#839)
## Summary - Adds configurable GPS polygon areas to `config.json`; nodes are attributed to an area if their last-known position falls inside the polygon - New `Area: …` dropdown filter (matching the existing region filter style) appears on all analytics, nodes, packets, map, and live screens when areas are configured - Backend resolves area membership with a 30s TTL cache; area filter bypasses the 500-node cap on `/api/bulk-health` so all area nodes are always returned - Includes a polygon builder tool (`/area-map.html`) for drawing and exporting area boundaries ## Changes **Backend** - `AreaEntry` type + `Areas` config field - `GetNodePubkeysInArea` DB query + `resolveAreaNodes` (30s TTL, `areaNodeMu` RWMutex) - `PacketQuery.Area` + `filterPackets` polygon check - `?area=` param propagated through all analytics, topology, clock-health, and bulk-health routes - `/api/config/areas` endpoint **Frontend** - `area-filter.js`: single-select dropdown, persists to localStorage, cleans up stale keys on load - Wired into analytics, nodes, packets, channels, map, and live pages - Live map clears node markers on area change **Docs & tools** - `docs/user-guide/area-filter.md` — configuration and usage guide - `docs/api-spec.md` — updated with new endpoint and `?area=` param table - `tools/area-map.html` — polygon builder for defining area boundaries - Demo areas added to `config.example.json` ## Test plan - [x] No areas configured → filter dropdown does not appear on any page - [x] Areas configured → dropdown appears, "All" selected by default - [x] Selecting an area filters nodes/packets/topology/map correctly - [x] Selecting "All" restores unfiltered view - [x] Selection persists across page reloads (localStorage) - [x] Stale localStorage key (area removed from config) is cleared on load - [x] `/api/bulk-health?area=X` returns all nodes in area (no 500-node cap) - [x] `/api/config/areas` returns correct list 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
2329639f45 |
feat: scoped/unscoped transport-route statistics (#899) (#915)
@ ## What this PR does Implements region-scoped transport-route packet tracking with two sub-features: ### Feature 1 — Scope statistics (`scope_name`) - At ingest, transport-route packets (route_type 0/3) with Code1 != `0000` are HMAC-matched against configured `hashRegions` keys (mirroring the `hashChannels` pattern). Matched region name (or `""` for unknown) stored in new `transmissions.scope_name` column via migration `scope_name_v1`. - New `GET /api/scope-stats?window=` endpoint (1h/24h/7d, 30s server-side TTL) returning transport totals, scoped/unscoped counts, per-region breakdown, and time-series. - New **Scopes** tab in Analytics with summary cards, per-region table, and two-line SVG chart. Auto-refreshes every 60s. ### Feature 2 — Node default scope (`default_scope`) - Per-node `default_scope` column on `nodes`/`inactive_nodes` (migration `nodes_default_scope_v1`) tracks the most recently matched region for each node, derived from transport-scoped ADVERT packets. - `GET /api/nodes` response includes `default_scope` field when column is present. - Node detail panel displays the default scope badge. - Async startup backfill (`BackfillDefaultScopeAsync`) populates the column for nodes with pre-existing ADVERT data. ### Config Add `hashRegions` to `config.json` (see `config.example.json`). One entry per region name (with or without leading `#`). @ --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
96a79ce9c1 |
fix(nav): floor Priority+ overflow at high-priority links — fixes nav vanishing on non-high routes (#1311) (#1312)
Red commit: `5f366b71` — CI: pending (will link once first run starts). Fixes #1311 ## The bug `applyNavPriority` in `public/app.js` had no floor on the iterative overflow loop: ```js let i = 0; while (!fits() && i < overflowQueue.length) { overflowQueue[i].classList.add('is-overflow'); i++; } ``` The `overflowQueue` is built non-high-first then high-priority tail. When `fits()` kept returning `false` — because the active-route pill renders wider than other links — the loop walked past the non-high tail and started dropping high-priority links too. On a non-high active route (`/#/perf`, `/#/audio-lab`, `/#/analytics`, `/#/observers`) at ~1101–1200px, this nuked Home/Packets/Map/Live/Nodes and left the user with brand + "More ▾" + the active pill. ## Repro (master) 1. `go build ./cmd/server` and serve against the e2e fixture 2. Visit `http://localhost:13581/#/perf` at 1101px viewport 3. Inline strip shows only "More ▾" + the ⚡ Perf pill — Home/Packets/Map/Live/Nodes are all gone 4. New E2E (`test-nav-priority-1311-e2e.js`) reproduces this: 4/16 cases fail at 1101px on master. ## The fix Two-line floor in the loop guard: break when the next queue item is a high-priority link. ```js while (!fits() && i < overflowQueue.length) { if (overflowQueue[i].dataset.priority === 'high') break; overflowQueue[i].classList.add('is-overflow'); i++; } ``` The `>=2` More-menu floor (#1139) gets the same guard — never promote a high-priority link just to hit the floor. A degenerate 1-item dropdown is a smaller paper-cut than nuking primary nav. ## TDD trail - **RED commit `5f366b71`**: `test-nav-priority-1311-e2e.js` lands first. Asserts (`assert.deepStrictEqual`) all 5 high-priority hrefs are visible inline at 900/1024/1101/1200px on /#/perf, /#/audio-lab, /#/analytics, /#/observers (16 cases). Fails 4/16 against master. - **GREEN commit `6d1a5542`**: floor added; 16/16 pass. Existing nav suite still green: - `test-nav-priority-1102-e2e.js`: 5/5 ✅ - `test-nav-more-floor-1139-e2e.js`: 10/10 ✅ - `test-nav-fluid-1055-e2e.js`: 20/20 ✅ - **Mutation guard**: stash the floor → test fails 4/16 again on the same cases. Browser verified: chromium 136 against local Go server with `test-fixtures/e2e-fixture.db` at 900/1024/1101/1200px on each non-high route. E2E assertion added: `test-nav-priority-1311-e2e.js:107` (`assert.deepStrictEqual`). ## Constraints respected - Existing 5/5 inline behavior on /#/home (active route IS high-priority) — preserved by 1102 suite ✅ - `<=1100` branch — unchanged (already data-priority-aware) ✅ - `>=2` More-menu floor (#1139) — preserved + extended with the same high-pri guard ✅ - All colors via CSS vars ✅ - PII preflight clean ✅ --------- Co-authored-by: CoreScope Bot <bot@corescope> |
||
|
|
afdd455ed9 |
fix(ui): align filter-bar heights and compact MESH LIVE panel (#1182)
## Summary - **Filter bar heights**: `.btn` and `.col-toggle-btn` carried `min-height:48px` from the WCAG touch-target rule, making buttons like `Group by Hash`, `★ My Nodes`, `Columns ▾`, and text inputs visibly taller than the `multi-select-trigger` / `region-dropdown-trigger` controls (which don't carry `.btn` and were already correct at 34px). Fix adds `min-height:34px` overrides to `.filter-bar .btn`, `.filter-group .btn`, `.filter-bar .col-toggle-btn`, and `.filter-bar input, .filter-bar select` so the entire filter bar renders at a uniform 34px on desktop. - **MESH LIVE panel**: `.live-overlay` sets `flex-direction:column` on all overlay panels; `.live-header` did not override this. With `#liveAreaFilter` populated (when areas are configured), the panel stacked 4 rows — title, stats, toggles, area filter — consuming ~⅓ of viewport height. Switch `.live-header` to `flex-direction:row; flex-wrap:wrap`, give `.live-toggles` `flex:0 0 100%` to force it to its own line, and move `#liveAreaFilter` inside `.live-toggles` so the area dropdown is inline with the other controls. Panel shrinks from 4 rows to 2 rows. ## Test plan - [x] Packets page filter bar: `Filters ▾`, text inputs, `All Observers`, `All Types`, `Group by Hash`, `★ My Nodes`, `Columns ▾`, `Hex Paths` all render at uniform ~34px height on desktop - [x] Mobile (≤767px): filter bar touch targets unaffected (mobile media query still authoritative) - [x] Live page: MESH LIVE panel occupies 2 rows (title+stats / toggles) instead of 4 - [x] Live page: `Area: All ▾` appears inline in the toggles row when areas are configured; panel hides the area control entirely when no areas are configured (existing behavior) - [x] Audio controls still appear correctly when the Audio toggle is checked 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f5785e89f4 |
fix(traces): fix path graph legibility and overlapping edges (#1134)
## Summary - Drop prefix-only paths from path graph: partial observations (same packet seen at 1, 2, 4, 5 hops as it propagated) were treated as separate routes, producing long shortcut edges to Dest that visually obscured the actual relay chain. Now filters out any path that is a strict prefix of a longer observed path before building the graph. - Fix invisible node labels: intermediate hop nodes used white text on `--surface-2` background, making labels invisible in the light theme. Labels now appear below circles and use `var(--text)` for theme-aware contrast. Increased SVG height and node radius to give labels room; intermediate fill uses a subtle accent tint with accent border. ## Test plan - [ ] Open a TRACE packet's path graph with a node that has multiple partial observations — verify no spurious shortcut edges - [ ] Check path graph in light theme — verify intermediate hop labels are visible - [ ] Check path graph in dark theme — verify no regression 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
6873219c7a |
feat(live): slow-mo playback — sub-1x VCR speeds (closes #771 M1) (#922)
Extends VCR speed cycle to `[0.25, 0.5, 1, 2, 4, 8]` so users can watch
live paths in slow motion.
## Changes
- `vcrSpeedCycle()`: speed array extended to include `¼x` and `½x`;
saves preference to `localStorage('live-vcr-speed')`
- `speedLabel()`: new helper returning `¼x` / `½x` for sub-1x, used in
the speed button
- `drawAnimatedLine`: step interval scales with speed (`33 / VCR.speed`)
- `drawMatrixLine`: `DURATION_MS` scales with speed (`1100 / VCR.speed`)
- Speed preference restored from localStorage on page load
## Tests
3 new unit tests; 72 pass, 0 regressions.
Closes #771 (M1 of 3)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
5cc7332583 |
feat(live): clickable path overlay — packet info popup (closes #771 M2) (#923)
After a path animation completes, keeps an invisible clickable polyline on the map for 30s. Clicking it shows a compact Leaflet popup with type badge, hop chain, relative time, and a link to the full packets page. Popup auto-dismisses after 20s. ## Changes - `clickablePathsLayer`: new Leaflet layer for invisible hit-target polylines - `buildClickablePathPopupHtml()`: pure function generating popup HTML (type badge, hop chain, time, hash link) - `pruneClickablePaths()`: TTL (30s) + FIFO eviction (max 50); runs on existing `_pruneInterval` - `registerClickablePath()`: adds invisible polyline with click → popup handler - `animatePath()`: accepts optional `pktMeta` (`hash`, `ts`); calls `registerClickablePath` on completion - Teardown clears `clickablePathsLayer` and `clickablePaths` ## Tests 7 new unit tests; 77 pass, 0 regressions. Closes #771 (M2 of 3) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
11dd180219 |
fix(#1306): disambiguate 'collisions' terminology + surface WHICH collides (#1307)
## #1306 — Disambiguate "collisions" terminology + surface WHICH collides (WIP draft) Red commit pending CI URL. ### What **A. Terminology fix** — Prefix Tool currently labels theoretical-math collisions ("38 two-byte collisions") with the same word the Collisions tab uses for packet-traffic-observed collisions ("0 two-byte"). Operators saw contradictory counts and assumed a bug. - Prefix Tool Network Overview cards: replace bare "collisions" with "address conflicts at this hash size" / "would-collide-if-used" wording. - Cross-reference line: "These are theoretical conflicts that would occur IF all repeaters used this hash size. For collisions actually observed in packet traffic, see the Hash Issues tab." → links to `#/analytics?tab=collisions`. - Collisions tab: reverse pointer "Collisions observed in actual packet traffic. For theoretical conflicts at each hash size, see the Prefix Tool tab." → links to `#/analytics?tab=prefix-tool`. **B. Expandable "which collides" list** — Aggregate count "38 colliding 2-byte slices" is unactionable. Operators need to see which slice and which nodes share it. - Per tier, when `opCollisions[b] > 0` OR `stats[b].collidingPrefixes > 0`, render a "Show N colliding slices →" toggle below the count. - Expanding reveals a `Prefix · Nodes sharing` table with node-detail links (`#/nodes/<pubkey>`), scrollable above 50 entries. - Both flavors rendered: theoretical (across all repeaters) and operational (configured-for-this-size only). The operational list is the higher-priority signal. Data is already in `idx[b]` — no backend changes. ### E2E `test-issue-1306-collisions-terminology-e2e.js` asserts wording, cross-ref links, expand-toggle, and node links present. RED commit only ships the test; GREEN commit adds the production code. Fixes #1306 --------- Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> |
||
|
|
7342166f0a |
feat(nodes): add sortable Scope column to nodes list (#1195)
## Summary - Adds a **Scope** column to the nodes list table, positioned after Role - Shows `default_scope` for nodes that have one (populated from scoped ADVERT packets, landed in #899), empty for the rest - Column is sortable (alphabetical); hidden on narrow screens (`data-priority="3"`, same as Public Key) ## Test Plan - [x] `node test-frontend-helpers.js` — all existing tests pass, two new sort tests added (`sortNodes sorts by default_scope asc/desc`) - [x] Open `/nodes` — Scope column visible between Role and Last Seen - [x] Nodes with a known scope show the value in monospace; nodes without show an empty cell - [x] Click Scope header → sorts ascending; click again → sorts descending - [x] Empty-scope rows go to the bottom on asc, top on desc - [x] Narrow the browser → Scope column hides at the same breakpoint as Public Key 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
bdbcb337ca |
fix(home): re-render after config loads to fix null homeCfg on direct load (#1194)
## Summary - On direct page load to `#/home` (or a full refresh), `renderHome()` runs before the async `/api/config/theme` fetch resolves, so `window.SITE_CONFIG` is `undefined` and `homeCfg` is `null` — showing SF defaults instead of the site's customisations. - When navigating from another page the fetch has already completed, which is why it works in that case. - Fix: subscribe to `theme-refresh` (the event fired ~300 ms after the config is fetched and applied) and re-render; clean up the listener in `destroy()`. This matches the existing pattern used by `analytics.js` and `map.js`. Fixes #1193 ## Test plan - [x] Hard-refresh directly to `#/home` — customised `heroTitle`, `heroSubtitle`, steps, footer links must render correctly - [x] Navigate from another page to Home — still renders correctly (no regression) - [x] Site with no custom config — defaults render, no JS errors - [x] Theme customiser changes while on Home page — page re-renders (theme-refresh re-render still works) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |