mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-06 01:01:32 +00:00
fe997fefb2b083062ee252f957b147cd6a3de106
34 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
85b8c8115a |
feat(channels): fluid sidebar + container-query stacking (#1057) (#1095)
## Summary Makes the channels page sidebar + message area fluid as part of the parent #1050 fluid-layout effort. Replaces the hardcoded `.ch-sidebar { width: 280px; min-width: 280px }` with `width: clamp(220px, 22vw, 320px); min-width: 220px`. Adds an `@container` query (via `container-type: inline-size` on `.ch-layout`) that stacks the sidebar above the message area when the channels page itself is narrow (≤700px container width) — independent of the global viewport, so it adapts even when an outer panel is consuming width. Removes the legacy `@media (max-width: 900px)` fixed 220px override; the clamp + container query handle that range. `.ch-main` already used `flex: 1`, so it absorbs all remaining width including ultrawides. The existing mobile (≤640px) overlay rules and the JS resize handle in `channels.js` are untouched and still work (user drag still wins via inline width). Fixes #1057. ## Scope - `public/style.css` — channels section only - (no `public/channels.js` changes needed) ## Tests TDD: red commit (failing tests) → green commit (implementation). - `test-channel-fluid-layout.js` (new): static CSS assertions - `.ch-sidebar` uses `clamp()` for width (not fixed px) - `.ch-sidebar` keeps a sane `min-width` (200–280px) - `.ch-main` keeps `flex: 1` - `.ch-layout` declares `container-type` (container query root) - `@container` rule scopes channels stacking - legacy `@media (max-width: 900px) .ch-sidebar { width: 220px }` is gone - `test-channel-fluid-e2e.js` (new): Playwright E2E at 768 / 1080 / 1440 / 1920 (wide) and 480 (narrow). Asserts: - no horizontal scroll on the body - sidebar AND message area both visible side-by-side at ≥768px - sidebar consumes ≤45% of viewport, main ≥40% - at 480px the layout stacks (or overlays) — no overflow Wired into `test-all.sh` and the unit + e2e steps of `.github/workflows/deploy.yml`. ## Verification - Static unit test: 6/6 pass on the green commit, 4/6 fail on the red commit (only the two trivially-true assertions pass). - Local Go server boot: `corescope-server` serves the updated `style.css` containing `container-type: inline-size`, `clamp(220px, 22vw, 320px)`, and `@container chlayout (max-width: 700px)`. - Local Chromium on the dev sandbox is musl-incompatible (Playwright fallback build crashes with `Error relocating ...: posix_fallocate64: symbol not found`), so the E2E was not run locally. CI will run it on Ubuntu runners. --------- Co-authored-by: clawbot <clawbot@example.com> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
ac0cf5ac7d |
fix(channels): #1087 QR library + share modal + PSK persistence (#1090)
Red commit: 5def4d073c61058fc9f327a3c60ece27e21cbc69 (CI run pending — see Checks tab) Fixes #1087 ## What's broken (4 bugs) 1. **"QR library not loaded"** — `channel-qr.js` checked `root.QRCode` (capital), but the vendored library exports lowercase `qrcode` (Kazuhiko Arase API). Generate & Show QR always fell into the "library not loaded" branch. 2. **QR encodes `name=psk:hex`** — the Share button (and parts of the Generate path) passed the internal `psk:<hex8>` lookup key to `ChannelQR.generate`, ignoring the user's display label stored in `LABELS_KEY`. 3. **PSK channel doesn't persist on refresh** — the persistence path was scattered, and the read-back wasn't verified. Added channels disappeared on refresh and "reappeared" only when a later add ran the persist hook. 4. **Share button reuses the Add Channel modal** — wrong intent reuse (Add = INPUT, Share = OUTPUT). Replaced with a dedicated `#chShareModal` (separate DOM id, separate title, share-only affordances, privacy warning). ## TDD Red commit (this) lands ONLY the failing tests: - `test-channel-issue-1087.js` — source-string contract assertions for all 4 bugs - `test-channel-issue-1087-e2e.js` — Playwright E2E covering generate → QR render, QR display name, persistence across refresh, Share opens dedicated modal Green commit (follow-up) lands the production fixes. ## E2E assertion added E2E assertion added: test-channel-issue-1087-e2e.js:55 ## CI wiring - `test-channel-issue-1087.js` added to `.github/workflows/deploy.yml` (go-test JS unit step) + `test-all.sh` - `test-channel-issue-1087-e2e.js` added to `.github/workflows/deploy.yml` (e2e-test step) --------- Co-authored-by: bot <bot@corescope> Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
282074b19d |
feat(#1034): wire QR generate + scan into channel modal (PR 3/3) (#1081)
## Summary **PR 3/3 of #1034** — wires the existing `window.ChannelQR` module (PR2 #1035) into the existing channel modal placeholders (PR1 #1037). ### Changes **`public/channels.js`** - **Generate handler** (`#chGenerateBtn`): replaced the "QR coming in next update" placeholder text with a real call to `window.ChannelQR.generate(label || channelName, keyHex, qrOut)`. Renders QR canvas + `meshcore://channel/add?...` URL + Copy Key inline into `#qr-output`. - **Scan handler** (`#scan-qr-btn`): removed `disabled` attribute, refreshed title, and added a click handler that calls `window.ChannelQR.scan()`. On success it populates `#chPskKey` (from `result.secret`) and `#chPskName` (from `result.name`); on cancel it's a no-op; on error it surfaces the message via `#chPskError`. The Share button on sidebar entries was already wired to `ChannelQR.generate` in PR1 (no change needed). ### TDD 1. **Red commit** (`178020b`): `test-channel-qr-wiring.js` — 12 assertions, 7 failed against the placeholder code (Generate handler still printed "coming in next update", scan button still disabled). 2. **Green commit** (`e708f3f`): wiring added → all 12 assertions pass. ### E2E (rule 18) `test-e2e-playwright.js` gains 3 Playwright tests (run against the live Go server with fixture DB in CI): - Generate → asserts `#qr-output canvas` and the `meshcore://channel/add` URL appear after the click. - Scan button is enabled (no `disabled` attribute). - Stubs `ChannelQR.scan` to return `{name, secret}`, clicks the button, asserts `#chPskKey` + `#chPskName` are populated. ### CI registration Added `node test-channel-qr-wiring.js` and `node test-channel-modal-ux.js` to the JS unit-test step in `.github/workflows/deploy.yml` (and `test-all.sh`). ### Closes Closes #1034 (final PR in the redesign series). --------- Co-authored-by: OpenClaw Bot <bot@openclaw.local> |
||
|
|
f7d8a7cb8f |
feat(packets): filter UX — in-UI docs + autocomplete + right-click + saved filters (#966) (#1083)
## Summary Implements the full filter-input UX upgrade from #966 — Wireshark-style help, autocomplete, right-click-to-filter, and saved filters. Closes #966. ## Surfaces ### A. Help popover (ⓘ button next to filter input) Auto-generated from `PacketFilter.FIELDS` / `OPERATORS` so it stays in sync with the parser. Includes: - Syntax overview (boolean ops, parens, case-insensitivity, URL-shareable filters) - Full field reference (27 entries: top-level + `payload.*`) - Full operator reference with one example per op - 10 ready-to-paste examples - Tips (right-click, autocomplete, save) ### B. Autocomplete dropdown - Type partial field name → field suggestions (top-level + dynamic `payload.*` keys discovered from visible packets) - Type `field` → operator suggestions - Type `type ==` → list of canonical type values (`ADVERT`, `GRP_TXT`, …) - Type `route ==` → list of route values (`FLOOD`, `DIRECT`, `TRANSPORT_FLOOD`, …) - Keyboard nav: ↑/↓, Tab/Enter to accept, Esc to dismiss ### C. Right-click → filter by this value Right-click any of these cells in the packet table: - `hash`, `size`, `type`, `observer` Context menu offers `==`, `!=`, `contains`. Click → clause appended to filter input (with `&&` if expression already present). ### D. Saved filters - ★ Saved ▾ dropdown next to the input - 7 starter defaults (Adverts only, Channel traffic, Direct messages, Strong signal SNR > 5, Multi-hop, Repeater adverts, Recent < 5m) - "+ Save current expression" prompts for a name and persists to `localStorage` under `corescope_saved_filters_v1` - User filters can be deleted (✕); defaults cannot - User filters with the same name as a default override it ## Implementation **`public/packet-filter.js`** — exposes `FIELDS`, `OPERATORS`, `TYPE_VALUES`, `ROUTE_VALUES`, and a new `suggest(input, cursor, opts)` function that returns ranked autocomplete suggestions with replace-range. Pure function — no DOM, fully unit-tested. **NEW `public/filter-ux.js`** — `window.FilterUX` IIFE owning the help popover, autocomplete dropdown, context menu, and saved-filters store. `init()` is idempotent, called once after the filter input renders. **`public/packets.js`** — calls `FilterUX.init()` after the filter input IIFE; row builders gain `data-filter-field` / `data-filter-value` attrs on hash/size/type/observer cells. `filter-group` wrapper now `position: relative` so dropdowns anchor correctly. **`public/style.css`** — scoped `.fux-*` styles using existing CSS variables (no new theme tokens). ## Tests - `test-packet-filter-ux.js` (19 unit tests, wired into `test-all.sh`): - Metadata exposure (FIELDS / OPERATORS / TYPE_VALUES / ROUTE_VALUES) - `suggest()` for empty input, prefix match, after `==`, dynamic `payload.*` keys - `SavedFilters.list/save/delete` — defaults, persistence, override, dedup - `buildCellFilterClause()` and `appendClauseToExpr()` quoting + appending - `test-filter-ux-e2e.js` (Playwright, wired into `deploy.yml`): - Navigate /packets → metadata exposed - Help popover opens with field reference, operators, examples - Autocomplete shows on focus, filters by prefix, accepts on Enter - Saved-filter dropdown lists defaults, click populates input - Right-click on TYPE cell → context menu → click appends clause - Save current expression persists to localStorage TDD red commit (`bddf1c1`) — assertion failures only, no import errors. Green commit (`0d3f381`) — all 19 unit tests pass. ## Browser validation Spawned local server on :39966 against the e2e fixture DB and exercised every UX surface via the openclaw browser tool. Confirmed: - `window.PacketFilter.FIELDS.length === 27`, `suggest()` available - `FilterUX.SavedFilters.list().length === 7` (defaults seeded) - Help popover renders with `payload.name`, `contains`, `ADVERT` text content - Right-click on a `data-filter-field="type"` / `data-filter-value="Response"` cell → context menu showed three options → clicking == populated the input with `type == "Response"` (and the existing alias resolver matched it to `payload_type === 1`) - Autocomplete on `pay` returned `payload_bytes`, `payload_hex`, `payload.name`, `payload.lat`, `payload.lon`, `payload.text` ## Out of scope (deferred per the issue) - Server-synced saved filters (cross-device) - Visual filter builder - Custom field expressions ## Acceptance criteria - [x] Help icon (ⓘ) next to filter input opens documentation popover - [x] Field reference table + operator reference + 6+ examples in popover - [x] Autocomplete dropdown on field names (top-level + `payload.*`) - [x] Autocomplete dropdown on values for `type` / `route` operators - [x] Right-click on packet cell → "Filter ==" / "Filter !=" / "Filter contains" - [x] Right-click context menu hides when clicking elsewhere / Esc - [x] Saved-filters dropdown with at least 5 default examples (7 shipped) - [x] User-saved filters persist in localStorage - [x] Real-time match count next to filter input (already shipped pre-PR; preserved) - [ ] Improved error messages with token + position — partial: existing parse errors already cite position; not a regression - [x] No regression in existing filter behavior (`test-packet-filter.js`: 69/69 pass) --------- Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
5f6c5af0cf |
fix(observers): correct column headings after Last Packet (#1039) (#1075)
## Summary Fixes #1039 — the Observers page table had 10 `<td>` cells per row but only 9 `<th>` headings, so labels drifted starting at the Packet Health badge cell. The headings `Packets`, `Packets/Hour`, `Clock Offset`, `Uptime` were each one column to the left of their data. ## Changes - `public/observers.js`: added missing `Packet Health` heading (over the `packetBadge()` cell) and renamed the count column header from `Packets` to `Total Packets` to disambiguate from `Packets/Hour`. ## TDD - **Red commit** (`7cae61c`): `test-observers-headings.js` asserts `<th>` count equals `<td>` count and verifies the expected header order. Both assertions fail on master (9 vs 10; `Packets` vs `Packet Health`/`Total Packets`). - **Green commit** (`8ed7f7c`): heading row updated; both assertions pass. ## Test ``` $ node test-observers-headings.js ── Observers table headings (#1039) ── ✓ thead column count equals tbody row column count ✓ expected headings present and ordered 2 passed, 0 failed ``` Wired into `test-all.sh`. ## Risk Frontend-only, static template change. No data flow / perf impact. Fixes #1039 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
8b924cd217 |
feat(ui): encode view & filter state in URL hash (#749) (#1072)
## Summary Encodes view + filter state in the URL hash so deep links restore the exact page state (issue #749). ## Changes New shared helper `public/url-state.js` exposing `URLState`: - `parseSort('col:asc')` → `{column, direction}` (defaults to `desc`) - `serializeSort('col', 'desc')` → `'col'` (omits default direction) - `parseHash('#/nodes/abc?tab=x')` → `{route: 'nodes/abc', params: {tab:'x'}}` - `buildHash(route, params)` and `updateHashParams(updates, currentHash)` for round-tripping while preserving subpaths. Wired into: - **packets.js** — sort column/direction now in `#/packets?sort=col[:asc]`, restored on init (overrides localStorage). Subpath `#/packets/<hash>` preserved. - **nodes.js** — sort encoded as `#/nodes?sort=col[:asc]`, restored on init. Subpath `#/nodes/<pubkey>` preserved. - **analytics.js** — both selected tab (`tab=topology`) AND time-window picker value (`window=7d`) now round-trip via URL. Subview keys used by rf-health (`range/observer/from/to`) cleared when switching tabs to keep URLs clean. Existing deep links (`#/nodes/<pubkey>`, `#/packets/<hash>`, `?filter=…`, `?node=…`, `?observer=…`, `?channel=…`, `?timeWindow=…`, `?region=…`) all keep working — additive change only. ## Tests TDD red→green: - Red: `5e1482e` (stub throws "not implemented"; 18/18 tests fail on assertions) - Green: `512940e` (helper implemented; 18/18 pass) Wired `test-url-state.js` into `test-all.sh`. Fixes #749 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
f9cd43f06f |
fix(analytics): integrate channels list with PSK decrypt UX + add link from Channels page (#1042)
## What Integrates the Analytics → Channels section with the PSK decrypt UX (PRs #1021–#1040). Replaces nonsense `chNNN` placeholders with useful display names and groups the table the same way the Channels sidebar does. ## Before - Encrypted channels showed raw `ch185`, `ch64`, `ch?` placeholders. - Locally-decrypted PSK channels (with stored keys + labels) were not surfaced — every encrypted row looked identical and useless. - Single flat list, sorted by last activity by default. ## After - **My Channels** 🔑 — any analytics row whose hash byte matches a stored PSK key (via `ChannelDecrypt.getStoredKeys()` + `computeChannelHash`). Display name uses the user's label if set, otherwise the key name. - **Network** 📻 — known cleartext channels (server-provided names) and rainbow-table-decoded encrypted channels. - **Encrypted** 🔒 — unknown encrypted, rendered as `🔒 Encrypted (0xNN)` instead of `chNNN`. - Within each group: messages descending (most active first). - New `📊 Channel Analytics →` link in the Channels page sidebar header → `#/analytics`. ## How - Pure `decorateAnalyticsChannels(channels, hashByteToKeyName, labels)` — testable in isolation, sets `displayName` + `group` per row. - `buildHashKeyMap()` — async helper that resolves stored PSK keys to their channel hash bytes via `computeChannelHash`. Used at render time; first paint uses an empty map (best-effort) and re-renders once keys resolve. Graceful fallback when `ChannelDecrypt` is missing or there are no stored keys. - `channelTbodyHtml` gains an `opts.grouped` flag — opt-in so the existing flat sort still works for any other caller. - The analytics API endpoint is **unchanged** — this is purely frontend rendering. ## Tests `test-analytics-channels-integration.js` — 19 assertions covering decoration, grouping, sort order, and the channels-page link. Added to `test-all.sh`. Red commit: `5081b12` (12 assertion failures + stub). Green commit: `6be16d9` (all 19 pass). --------- Co-authored-by: bot <bot@corescope.local> Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
cea2c70d12 |
feat(#1034): channel UX redesign PR1 — Add Channel modal + sectioned sidebar (#1037)
## Summary PR 1 of 3 for #1034 — channel UX redesign. Replaces the cramped inline "type a name or 32-hex blob" form with a clear modal dialog, and reorganizes the sidebar into three labeled sections. **Scope of this PR:** Modal UI + sectioned sidebar. QR generation/scan is deferred to PR #2 (placeholders are wired and ready). `channel-decrypt.js` crypto is untouched. ## What changed ### New modal: `[+ Add Channel]` Triggered by the new sidebar button. Three sections: 1. **Generate PSK Channel** — name + `[Generate & Show QR]` → `crypto.getRandomValues(16)` → hex → `ChannelDecrypt.storeKey`. QR rendering ships in PR #2; for now `#qr-output` surfaces the hex key as text. 2. **Add Private Channel (PSK)** — 32-hex input (regex-validated), optional display name, `[Add]`. `[📷 Scan QR]` placeholder is present but `disabled` (PR #2 wires it). 3. **Monitor Hashtag Channel** — non-editable `#` prefix + free text + case-sensitivity warning + `[Monitor]`. Reuses `ChannelDecrypt.deriveKey`. Privacy footer: _"🔒 Keys stay in your browser. CoreScope is a passive observer..."_ Close ✕, backdrop click, and Escape all dismiss. ### Sectioned sidebar `renderChannelList()` rewritten to render three sections: - **My Channels** — `userAdded` channels. ✕ always visible. Last sender + relative time. - **Network** — server-known cleartext channels. - **Encrypted (N)** — collapsed by default (toggle persists in `localStorage`). Shows hash byte + packet count. The legacy "🔒 No key" checkbox and `#chShowEncrypted` toggle are removed entirely. Encrypted channels are always fetched; the renderer groups them. ## Tests - **Unit** — `test-channel-modal-ux.js` (33 assertions): added to `test-all.sh`. Covers sidebar button, modal markup, three sections, QR placeholders, privacy footer, sectioned sidebar, modal handlers (incl. `crypto.getRandomValues(16)`). - **E2E** — `test-channel-modal-e2e.js` (Playwright, 14 steps). Covers modal open/close, section rendering, invalid-hex error, valid-hex storage, encrypted-section toggle. Run with: ``` CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:38201 node test-channel-modal-e2e.js ``` - `test-channel-psk-ux.js` — updated to reference `#chPskName` (was `#chKeyLabelInput`). ### Red→green proof - Red commit (`7ee421b`): test added with 31 expected assertion failures, no source change. - Green commit (`897be8f`): implementation lands, test passes 33/33. ## Browser-validated Built `cmd/server/`, ran against `test-fixtures/e2e-fixture.db`, exercised modal open → invalid hex → valid hex → key persisted → modal closes → sectioned sidebar renders + Encrypted toggle expands. All 14 E2E steps pass. ## What's NOT in this PR - QR code rendering (PR #2) - Camera/QR scanning (PR #2) - Migration of legacy localStorage format (PR #3, if needed — current key format is unchanged) - `channel-decrypt.js` changes (none — UI-only PR) ## Acceptance criteria from #1034 - [x] Modal opens on `[+ Add Channel]` click - [x] Three sections clearly separated with labels - [x] Add PSK: accepts 32-hex (QR scan = PR #2) - [x] Monitor Hashtag: derives key, case-sensitivity warning shown - [x] Privacy footer present - [x] Sidebar: three sections (My Channels / Network / Encrypted) - [x] ✕ button visible and functional on My Channels entries - [x] "No key" checkbox removed - [ ] Generate PSK QR display — text fallback only; QR is PR #2 - [ ] Old stored keys migrate seamlessly — no migration needed (storage format unchanged) Refs #1034 --------- Co-authored-by: meshcore-bot <bot@meshcore.local> |
||
|
|
c1d0daf200 |
feat(#1034): channel QR generate + scan module (PR 2/3) (#1035)
## PR #2 of channel UX redesign (#1034) — QR generation + scanning Self-contained QR module for MeshCore channel sharing. Wirable but **not wired** — PR #3 wires this into the modal placeholders shipped by PR #1. ### What's in - **`public/channel-qr.js`** — new module exporting `window.ChannelQR`: - `buildUrl(name, secretHex)` → `meshcore://channel/add?name=<urlencoded>&secret=<32hex>` - `parseChannelUrl(url)` → `{name, secret}` or `null` (strict: scheme, path, hex32 secret) - `generate(name, secretHex, target)` — renders QR (via vendored qrcode.js) + the URL string + a "Copy Key" button into `target` - `scan()` → `Promise<{name, secret} | null>` — opens a camera overlay, decodes with jsQR, parses, auto-closes on first valid match. Graceful no-camera/permission-denied fallback ("Camera not available — paste key manually"). - **`public/vendor/jsqr.min.js`** — vendored jsQR 1.4.0 - **`public/index.html`** — loads `vendor/jsqr.min.js` + `channel-qr.js` after `channel-decrypt.js` - **`test-channel-qr.js`** + wired into `test-all.sh` — 16 assertions on `buildUrl` / `parseChannelUrl` (DOM/camera paths covered by Playwright in #3) ### TDD - Red commit `d6ba89e` — stub module + failing assertions on `buildUrl` / `parseChannelUrl` (compiles, runs, fails on assertion) - Green commit `25328ac` — real impl, 16/16 pass ### License note Brief specified jsQR as MIT — it's actually **Apache-2.0** (https://github.com/cozmo/jsQR/blob/master/package.json). Apache-2.0 is permissive and compatible with the repo's ISC license; flagging here so reviewers can confirm. Cited in the file header. ### Independence guarantees - Does **not** touch `channels.js` or `channel-decrypt.js` - Does not call any UI from `channels.js`; PR #3 will call `ChannelQR.generate(...)` into `#qr-output` and wire `#scan-qr-btn` to `ChannelQR.scan()` Refs #1034 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
d967170dd3 |
fix(channels): sidebar layout for user-added (PSK) rows — nested <button> bug (#1033)
## Problem Channel sidebar layout broke for user-added (PSK) channels. Visible symptoms in the screenshot: - No ✕ (delete) button on user-added rows - 🔑 emoji floating in the wrong position - Message preview text (e.g. `KpaPocket: Тест`) orphaned **between** channel entries instead of inside the row - Spinner/loading dots misaligned ## Root cause **HTML5 forbids nested `<button>` elements.** The `.ch-item` row is a `<button>`, and #1024 added a `<button class="ch-remove-btn">` inside it. The HTML parser implicitly closes the outer `.ch-item` the moment it sees the inner `<button>`, then re-parents everything after it (✕ and the `.ch-item-preview` line) outside the row. Resulting DOM tree (parser-corrected, simplified): ``` <button class="ch-item">[icon] Levski 🔑</button> <-- closes early <button class="ch-remove-btn">✕</button> <-- orphaned, "floating" <div class="ch-item-preview">KpaPocket: Тест</div> <-- orphaned <button class="ch-item">[icon] #bookclub …</button> ``` Compounded by `.ch-remove-btn { opacity: 0 }` (only visible on row hover), which made the ✕ undiscoverable on touch devices even before the parser bug. ## Fix `public/channels.js` - Replace the inner `<button class="ch-remove-btn">` with `<span class="ch-remove-btn" role="button" tabindex="0">`. Click delegation already keys off `[data-remove-channel]` so behavior is unchanged. - Add `keydown` (Enter / Space) handler on `#chList` so the role=button span stays keyboard-accessible. - Relabel the ambiguous `🔒 No key` toggle to `🔒 Show encrypted (no key)`, with an explanatory `title` ("Show encrypted channels you don't have a key for (locked, can't decrypt)") so users understand it controls visibility of channels they haven't added a PSK for. `public/style.css` - `.ch-remove-btn`: drop `opacity: 0` default. Now `0.55` idle, `0.9` on row hover, `1` on direct hover/focus. Added `:focus` outline removal + `display: inline-flex` so the ✕ centers cleanly. - Add `.ch-user-badge` rule (was unstyled — contributed to the misalignment of the 🔑). ## TDD - Red commit `eeb94ad` — `test-channel-sidebar-layout.js` (7 assertions, 3 failing on master). - Green commit `2959c3d` — fix; all 7 pass. - Wire commit `4d6100d` — added to `test-all.sh`. Existing channel test files still pass (`test-channel-psk-ux.js`, `test-channel-live-decrypt.js`, `test-channel-live-decrypt-userprefix.js`, `test-channel-decrypt-m345.js`, `test-channel-decrypt-insecure-context.js`). ## Files changed - `public/channels.js` - `public/style.css` - `test-channel-sidebar-layout.js` (new) - `test-all.sh` |
||
|
|
3aaa21bbc0 |
fix(channel-decrypt): pure-JS SHA-256/HMAC fallback for HTTP context (P0 follow-up to #1021) (#1027)
## P0: PSK channel decryption silently failed on HTTP origins User reported PSK key `372a9c93260507adcbf36a84bec0f33d` "still doesn't work" after PRs #1021 (AES-ECB pure-JS) and #1024 (PSK UX) merged. Reproduced end-to-end and found the actual remaining bug. ### Root cause PR #1021 fixed the AES-ECB path by vendoring a pure-JS core, but **SHA-256 and HMAC-SHA256 in `public/channel-decrypt.js` are still pinned to `crypto.subtle`**. `SubtleCrypto` is exposed **only in secure contexts** (HTTPS / localhost); when CoreScope is served over plain HTTP — common for self-hosted instances — `crypto.subtle` is `undefined`, and: - `computeChannelHash(key)` → `Cannot read properties of undefined (reading 'digest')` - `verifyMAC(...)` → `Cannot read properties of undefined (reading 'importKey')` Both throws are swallowed by `addUserChannel`'s `try/catch`, so the only user-visible signal is the toast `"Failed to decrypt"` with no console-friendly explanation. Verdict: PR #1021 only fixed half of the crypto-in-insecure-context problem. ### Reproduction (no browser required) `test-channel-decrypt-insecure-context.js` loads the production `public/channel-decrypt.js` in a `vm` sandbox where `crypto.subtle` is undefined (mirrors HTTP browser). Pre-fix it failed 8/8 with the exact error above; post-fix it passes 8/8. ### Fix - New `public/vendor/sha256-hmac.js`: minimal pure-JS SHA-256 + HMAC-SHA256 (FIPS-180-4 + RFC 2104, ~120 LOC, MIT). Verified against Node `crypto` for SHA-256 (empty / "abc" / 1000 bytes) and RFC 4231 HMAC-SHA256 TC1. - `public/channel-decrypt.js`: `hasSubtle()` guard. `deriveKey`, `computeChannelHash`, and `verifyMAC` use `crypto.subtle` when available and fall back to `window.PureCrypto` otherwise. Same API, same return types, same async signatures. - `public/index.html`: load `vendor/sha256-hmac.js` immediately before `channel-decrypt.js` (mirrors the `vendor/aes-ecb.js` wiring from #1021). ### TDD - **Red** (`8075b55`): `test-channel-decrypt-insecure-context.js` — runs the **unmodified** prod module in a no-`subtle` sandbox, asserts on the known PSK key (hash byte `0xb7`) and synthetic encrypted packet round-trip. Compiles, runs, **fails 8/8 on assertions** (not on import errors). - **Green** (`232add6`): vendor + delegate. Test passes 8/8. - Wired into `test-all.sh` and `.github/workflows/deploy.yml` so CI gates the regression. ### Validation (all green post-fix) | Test | Result | |---|---| | `test-channel-decrypt-insecure-context.js` | 8/8 | | `test-channel-decrypt-ecb.js` (#1021 KAT) | 7/7 | | `test-channel-decrypt-m345.js` (existing) | 24/24 | | `test-channel-psk-ux.js` (#1024) | 19/19 | | `test-packet-filter.js` | 69/69 | ### Files changed - `public/vendor/sha256-hmac.js` — **new** (~150 LOC, MIT, decrypt-side only) - `public/channel-decrypt.js` — `hasSubtle()` guard + fallback in `deriveKey`/`computeChannelHash`/`verifyMAC` - `public/index.html` — script tag for `vendor/sha256-hmac.js` - `test-channel-decrypt-insecure-context.js` — **new** (8 assertions, pure Node, no browser) - `test-all.sh` + `.github/workflows/deploy.yml` — wire the test ### Risk / scope - Frontend-only, decrypt-side only. No server, schema, or config changes (Config Documentation Rule N/A). - Secure-context behaviour unchanged (still uses Web Crypto when present). - HMAC `secret` building, MAC truncation (2 bytes), and AES-ECB delegation untouched. - Hash vector for the user's PSK key matches: `SHA-256(372a9c93260507adcbf36a84bec0f33d) = b7ce04…`, channel hash byte `0xb7` (183) — confirmed against Node `crypto` and against the new pure-JS path. ### Note on the FIPS test data in the new test The PSK `372a9c93260507adcbf36a84bec0f33d` is shared test data from the bug report, not a real channel secret. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
a1f4cb9b5d |
fix(channels): PSK channel UX — delete, label, badge, toast (#1020) (#1024)
## Problem The PSK channel decrypt UX was unusable (#1020): 1. ✕ button only appeared when a `userAdded` flag happened to be set, which wasn't reliable for keys matching server-known hashes. 2. PSK channels visually indistinguishable from server-known encrypted channels — both rendered with 🔒. 3. No way to give a PSK channel a friendly name; sidebar always showed `psk:<hex8>`. 4. "Decrypt count" toast was scraped from `#chMessages .ch-msg` after a race, so it often reported zero or stale numbers. ## Changes ### `public/channel-decrypt.js` - **New API**: `saveLabel(name, label)`, `getLabel(name)`, `getLabels()`. - `storeKey(name, hex, label?)` — third optional `label` argument persists alongside the key under a separate `corescope_channel_labels` localStorage namespace. - `removeKey` now also clears the stored label. ### `public/channels.js` - Add-channel form gets a second row with `#chKeyLabelInput` ("optional name (e.g. My Crew)"). - `addUserChannel(val, label)` — passes the label through to `storeKey`. - `mergeUserChannels()` reads `getLabels()` and propagates `userLabel` onto channel objects (both new ones and ones that match an existing server-known hash). - `renderChannelList()` distinguishes user-added rows: - `.ch-user-added` class + `data-user-added="true"` attribute. - 🔓 badge icon (vs 🔒 for server-known no-key) and a 🔑 marker next to the name. - Display name uses the user-supplied label when present. - ✕ remove button is now keyed off `userAdded` (which `mergeUserChannels` always sets for stored keys). - `selectChannel` now returns `{ messageCount, wrongKey?, error?, stale? }`. `addUserChannel` uses that for the toast instead of scraping the DOM, and surfaces `wrongKey` explicitly: "Key does not match any packets for …". ## Acceptance criteria - [x] ✕ (delete) button on all user-added PSK channels in sidebar - [x] Clicking ✕ removes key + label + cache from localStorage and removes from sidebar - [x] Visual badge/icon distinguishing "my keys" (🔓 + 🔑 + `.ch-user-added`) from "unknown encrypted" (🔒 + `.ch-encrypted`) - [x] Optional name field in the add-channel form (`#chKeyLabelInput`), stored alongside key in localStorage - [x] Name displayed in sidebar instead of `psk:<hex>` - [x] Toast shows decrypt result count after adding (and reports `wrongKey` explicitly) ## Tests `test-channel-psk-ux.js` (added to `test-all.sh`) — 19 assertions: - ChannelDecrypt label storage + retrieval + `removeKey` cascade. - E2E DOM contract for `channels.js`: `#chKeyLabelInput`, `.ch-user-added`, 🔓 icon, `addUserChannel` accepts label, no DOM scraping for decrypt count. - End-to-end `mergeUserChannels` label propagation through a sandbox-loaded `ChannelDecrypt`. Red commit (`da6d477`) failed 8/15 assertions; green commit (`542bb1d`) — all 19 pass. Existing channel tests still green: ``` node test-channel-decrypt-ecb.js → 7/7 node test-channel-decrypt-m345.js → 24/24 node test-channel-psk-ux.js → 19/19 ``` (The pre-existing `test-frontend-helpers.js` failure on `nodes.js` `loadNodes` reproduces on `origin/master` — unrelated.) ## Notes - Decrypt logic untouched (PR #1021 already fixed it). - No config fields added. - Keys + labels stay in the user's browser; nothing transmitted. Fixes #1020 --------- Co-authored-by: corescope-bot <bot@corescope.local> |
||
|
|
f438411a27 |
chore: remove deprecated Node.js backend (-11,291 lines) (#265)
## Summary Removes all deprecated Node.js backend server code. The Go server (`cmd/server/`) has been the production backend — the Node.js server was kept "just in case" but is no longer needed. ### Removed (19 files, -11,291 lines) **Backend server (6 files):** `server.js`, `db.js`, `decoder.js`, `server-helpers.js`, `packet-store.js`, `iata-coords.js` **Backend tests (9 files):** `test-decoder.js`, `test-decoder-spec.js`, `test-server-helpers.js`, `test-server-routes.js`, `test-packet-store.js`, `test-db.js`, `test-db-migration.js`, `test-regional-filter.js`, `test-regional-integration.js` **Backend tooling (4 files):** `tools/e2e-test.js`, `tools/frontend-test.js`, `benchmark.js`, `benchmark-ab.sh` ### Updated - `AGENTS.md` — Rewritten architecture section for Go, explicit deprecation warnings - `test-all.sh` — Only runs frontend tests - `package.json` — Updated test:unit - `scripts/validate.sh` — Removed Node.js server syntax check - `docker/supervisord.conf` — Points to Go binary ### NOT touched - `public/` (active frontend) ✅ - `test-e2e-playwright.js` (frontend E2E tests) ✅ - Frontend test files (`test-packet-filter.js`, `test-aging.js`, `test-frontend-helpers.js`) ✅ - `package.json` / Playwright deps ✅ ### Follow-up - Server-only npm deps (express, better-sqlite3, mqtt, ws, supertest) can be cleaned from package.json separately - `Dockerfile.node` can be removed separately --------- Co-authored-by: you <you@example.com> |
||
|
|
71ec5e6fca |
rename: MeshCore Analyzer → CoreScope (frontend + .squad)
Phase 1 of the CoreScope rename — frontend display strings and squad agent metadata only. index.html: - <title>, og:title, twitter:title → CoreScope - Brand text span → CoreScope - og:image/twitter:image URLs → corescope repo (placeholder) - Cache busters bumped public/*.js headers (19 files): - All file header comments updated public/*.css headers: - style.css, home.css updated JavaScript strings: - app.js: GitHub URL → corescope - home.js: 3 fallback siteName references - customize.js: default siteName + heroTitle Tests: - test-e2e-playwright.js: title assertion → corescope - test-frontend-helpers.js: GitHub URL constant - benchmark.js: header string - test-all.sh: header string .squad: - team.md, casting/history.json - All 7 agent charters + 5 history files NOT renamed (intentional): - localStorage keys (meshcore-*) - CSS classes (.meshcore-marker) - Window globals (_meshcore*) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
629606bbdd |
feat: optimize observations table schema (v3 migration)
- Replace observer_id TEXT (64-char hex) + observer_name TEXT with observer_idx INTEGER (FK to observers rowid) - Remove redundant hash TEXT and created_at TEXT columns from observations - Store timestamp as INTEGER epoch seconds instead of ISO text string - Auto-migrate old schema on startup: backup DB, migrate data, rebuild indexes, VACUUM - Migration is safe: backup first, abort on failure, schema_version marker prevents re-runs - Backward-compatible packets_v view: JOINs observers table, converts epoch→ISO for consumers - In-memory observer_id→rowid Map for fast lookups during ingestion - In-memory dedup Set with 5-min TTL to prevent duplicate INSERT attempts - packet-store.js: detect v3 schema and use appropriate JOIN query - Tests: 29 migration tests (old→new, idempotency, backup failure, ingestion, dedup) - Tests: 19 new v3 schema tests in test-db.js (columns, types, view compat, ingestion) Expected savings on 947K-row prod DB: - observer_id: 61 bytes → 4 bytes per row (57 bytes saved) - observer_name: ~15 bytes → 0 (resolved via JOIN) - hash: 16 bytes → 0 (redundant with transmission_id) - timestamp: 25 bytes → 4 bytes (21 bytes saved) - created_at: 25 bytes → 0 (redundant) - Dedup index: much smaller (integers vs text) - Estimated ~118 bytes saved per row = ~112MB total + massive index savings |
||
|
|
3efd040faf | ci: trigger build to test badge auto-push | ||
|
|
2adf4f668b |
test: 101 server route tests via supertest — 76% coverage
server.js now exportable via require.main guard. Tests every API endpoint: stats, nodes, packets, channels, observers, traces, analytics, config, health, perf, resolve-hops. Covers: params, pagination, error paths, region filtering. |
||
|
|
909b53c2b7 |
Add 41 frontend helper unit tests (app.js, nodes.js, hop-resolver.js)
Test pure functions from frontend JS files using vm.createContext sandbox: - timeAgo: null/undefined handling, seconds/minutes/hours/days formatting - escapeHtml: XSS chars, null input, type coercion - routeTypeName/payloadTypeName: known types + unknown fallback - truncate: short/long/null strings - getStatusTooltip: role-specific threshold messages - getStatusInfo: active/stale status for repeaters and companions - renderNodeBadges: HTML output contains role badge - sortNodes: returns sorted array - HopResolver: init/ready, single/ambiguous/unknown prefix resolution, geo disambiguation with origin anchor, IATA regional filtering Note: c8 coverage doesn't track vm.runInContext-evaluated code, so these don't improve the c8 coverage numbers. The tests still validate correctness of frontend logic in CI. |
||
|
|
9f5f2922ee |
Add spec-driven decoder tests with golden fixtures from production
- 255 assertions: spec-based header/path/transport/advert parsing + 20 golden packets - Verifies header bit layout, path encoding, advert flags/location/name per firmware spec - Golden fixtures from analyzer.00id.net catch regressions if decoder output changes - Notes 5 discrepancies: 4 missing payload types (GRP_DATA, MULTIPART, CONTROL, RAW_CUSTOM) and encrypted payload field sizes differ from spec (decoder matches prod behavior) |
||
|
|
21e7996c98 |
Extract server-helpers.js and add unit tests for server logic + db.js
- Extract pure/near-pure functions from server.js into server-helpers.js: loadConfigFile, loadThemeFile, buildHealthConfig, getHealthMs, isHashSizeFlipFlop, computeContentHash, geoDist, deriveHashtagChannelKey, buildBreakdown, disambiguateHops, updateHashSizeForPacket, rebuildHashSizeMap, requireApiKey - Add test-server-helpers.js (70 tests) covering all extracted functions - Add test-db.js (68 tests) covering all db.js exports with temp SQLite DB - Coverage: 39.97% → 81.3% statements, 56% → 68.5% branches, 65.5% → 89.5% functions |
||
|
|
3fdad47bfc |
Add decoder and packet-store unit tests
- test-decoder.js: 52 tests covering all payload types (ADVERT, GRP_TXT, TXT_MSG, ACK, REQ, RESPONSE, ANON_REQ, PATH, TRACE, UNKNOWN), header parsing, path decoding, transport codes, edge cases, validateAdvert, and real packets from the API - test-packet-store.js: 34 tests covering insert, deduplication, indexing (byHash, byNode, byObserver, advertByObserver), query with filters (type, route, hash, observer, since, until, order), queryGrouped, eviction, findPacketsForNode, getSiblings, countForNode, getTimestamps, getStats Coverage improvement: - decoder.js: 73.9% → 85.5% stmts, 41.7% → 89.3% branch, 69.2% → 92.3% funcs - packet-store.js: 53.9% → 67.5% stmts, 46.6% → 63.9% branch, 50% → 79.2% funcs - Overall: 37.2% → 40.0% stmts, 43.4% → 56.9% branch, 55.2% → 66.7% funcs |
||
|
|
8a1bfd8b06 |
feat: code coverage with c8, npm test runs full suite
npm test: all tests + coverage summary npm run test:unit: fast unit tests only npm run test:coverage: full suite + HTML report in coverage/ Baseline: 37% statements, 42% branches, 54% functions Fixed e2e channels crash (undefined .length on null) |