mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 03:07:54 +00:00
fb7bb240ccf4714e00a7628ad56e09c6881b60d2
56
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c80b44787 |
fix: Domestic/Foreign node filter now classifies from lat/lon, not the stale foreign flag
The `foreign` flag only reflects nodes whose ADVERT was decoded and classified AFTER geo_filter was configured. On stg.meshview.dk this made the just-shipped filter nearly useless: only 27 of 1503 nodes were flagged foreign, while 570 actually have GPS placing them outside the configured box (543 of those simply never got re-flagged). Exposes the geo_filter box/polygon via /api/config/client (new GeoFilter field on ClientConfigResponse), and adds nodePassesGeoFilter + helpers to app.js — a faithful JS port of internal/geofilter's PassesFilter/PointInPolygon/DistToSegmentKm, cross-checked line-for-line against the real Go implementation for bbox, polygon, and buffer cases. Nodes with no GPS fix (or (0,0)) count as domestic, matching both the Go semantics and the user's explicit choice for this feature. nodes.js's Domestic/Foreign filter-group (added in f99df27) now calls nodePassesGeoFilter(n.lat, n.lon, window.MC_GEO_FILTER) instead of reading n.foreign. |
||
|
|
06554ba172 |
feat: Domestic/Foreign filter on the Nodes page
The unfiltered node list is dominated by foreign-flagged nodes (#730 geo_filter classification) on deployments with heavy cross-border traffic — hard to see which of 1500+ listed nodes are actually local. Adds an All/Domestic/Foreign filter-group next to the existing Status filter, narrowing on each node's `foreign` boolean, persisted to localStorage like the other Nodes-page filters. |
||
|
|
40391b126d |
feat: Nodes Not Using Any Scope section on the Scopes tab
The flip side of the existing "Nodes Running This Region" section: every node with no default_scope configured at all — never set up a hashRegions region for itself. Computed client-side from the full node list (default_scope isn't windowed/aggregated server-side the way the rest of scope-stats is). On stg.meshview.dk this surfaced a real adoption gap: 1423 of 1491 known nodes (95%) have no scope configured at all, 1128 of them repeaters. Shows the role breakdown plus a most-recently-active-first table (capped at 100 rows) — worth reaching out to first, since they're still transmitting. Extracted the filter/tally/sort logic into computeNodesWithoutScope, a pure module-level function (same pattern as packets.js's reconcileVisibleCols) so it's unit-testable without needing to stand up the Scopes tab's full DOM skeleton, which updateData reaches into directly via getElementById rather than writing to a passed-in element the way renderForeignTrafficTab does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
679ab2fb86 |
test: add DOM coverage for the Foreign Traffic tab + auto-refresh hook
Per bot review on PR #1852 (comment 5012304813): renderForeignTrafficTab shipped without a test, repeating the same TDD-policy finding flagged on the prior round, and the tab lacked the 60s auto-refresh + stop-hook convention its Roles/Scopes siblings use (data would go stale until the user re-navigated away and back). - Added _stopForeignTrafficRefresh + a 60s setInterval loop, wired into the tab-switch handler and destroy(), matching _stopRolesRefresh/ _stopScopesRefresh exactly. - New test-analytics-foreign-traffic-tab.js: loads analytics.js in a vm sandbox (same pattern as test-frontend-helpers.js's makeAnalyticsSandbox), stubs fetchAllNodes with a fixed node mix, and asserts row sort order, role/zero-count exclusion, the empty state, both foreignCount note variants, and that the new stop-hook is exported and idempotent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a06ac8aceb |
fix(#1849): render — for TRACE Hop Bytes column (path bytes are SNR, not hop hashes) (#1850)
Fixes #1849. ## Problem The packets table "HB" (Hop Bytes / col-hashsize) column always shows `1` for TRACE packets. TRACE packets use header path bytes as per-hop SNR readings, not truncated hop hashes — see `internal/packetpath/route.go` `PathBytesAreHops(TRACE) = false`. The high-2-bit "hash_size" derivation applied to a SNR byte is meaningless (typically `1`). ## Fix At the 3 render sites in `public/packets.js` (`buildGroupRowHtml` header row, its child rows, `buildFlatRowHtml`), when `payload_type === 9` (TRACE) render `—` in the `col-hashsize` cell with a `title` tooltip explaining that TRACE path bytes are SNR readings and directing users to the sidebar decoder for the actual hop count. Non-TRACE rows unchanged. ## TDD - Red commit: `dd7a4e78` — `test-issue-1849-trace-hashbytes.js` asserts col-hashsize cell equals `—` with a title tooltip for TRACE, numeric for non-TRACE. CI must fail on this commit. - Green commit: `115368d0` — 3-site fix in `public/packets.js`. ## Verification ``` $ node test-issue-1849-trace-hashbytes.js ✅ All 4 tests passed ``` Preexisting failures in `test-packets.js` (13) are unchanged by this PR (verified via `git stash`) — unrelated to the surface touched here. ## Scope - `public/packets.js`: 3 render sites, +21/-8. - `test-issue-1849-trace-hashbytes.js`: new unit test. - `test-all.sh`: wire new test. No public API change. --------- Co-authored-by: clawbot <bot@example.invalid> |
||
|
|
4654ce3386 |
feat(analytics): "My Repeaters" favorites monitoring dashboard (#1761)
My Repeaters monitoring dashboard. Closes #1765. --------- Co-authored-by: Waydroid Builder <build@waydroid.local> |
||
|
|
707d70c738 |
fix(packets): clamp .col-details to one line on mobile (#1770 S path) (#1805)
## Summary Partial fix for #1770 (S quick-fix path only; L refactor remains as follow-up). The packets-view virtual-scroller assumes a constant `VSCROLL_ROW_HEIGHT`, but the base rule at `public/style.css` L1097 lets `td.col-details` wrap on narrow viewports (`white-space: normal; word-break: break-word`). Wrapped rows produce variable row heights → visible jitter when scrolling past ~900px on iOS. **Quick-fix (S path):** under the existing `@media (max-width: 640px)` block in `public/style.css`, clamp `.col-details` to a single line: ```css .data-table td.col-details { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ``` Trade-off accepted in triage: Details column truncates on mobile in exchange for smooth scrolling. The base rule keeps wrapping on desktop (≥641px) so nothing changes there. **Out of scope:** the full L-path fix (per-row measurement, `_rowHeightsPx[]`, cumulative offsets, re-measure on hop-resolver finalize) — tracked separately on #1770. ## TDD - **Red commit** `7f58bedc` — adds `test-issue-1770-mobile-row-clamp.js`, a CSS-grep test (same pattern as `test-issue-1364-pill-no-clamp.js`) that walks every `@media (max-width: 640px)` block in `public/style.css` and asserts a `.col-details` rule declares `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`. Verified to FAIL on master (assertion failure, not a parse error) and PASS after the CSS change. - **Green commit** `d46271b8` — applies the 5-line CSS clamp inside the existing mobile breakpoint at L2362. ## Files touched - `public/style.css` (+13) - `test-issue-1770-mobile-row-clamp.js` (+101, new) ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all gates pass (PII, branch scope, red commit, css-vars, css self-fallback, LIKE-on-JSON, sync migration, async-migration, XSS). No warnings. --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
9757178aad |
fix(#1789): add Firmware + Client columns to observers table (#1790)
## Summary Adds **Firmware** and **Client** columns to the observers table (`#/observers`). Both values already come back from `/api/observers` (`firmware`, `client_version`) — they were just never rendered. Fleet operators have been asking to sort/scan firmware versions to coordinate upgrades. Closes #1789. ## Changes - `public/observers.js` - Two new `<th data-priority="4" data-sort-key="...">` headers (Firmware, Client). Priority 4 matches Clock Offset / Uptime so `TableResponsive` hides them first on narrow viewports. - Two new `<td class="mono">` cells with `data-value="${escapeHtml(raw)}"` for sort and the rendered text escape-wrapped. - `truncateBuildSuffix()` helper trims the long `" Build: ..."` tail from firmware in the displayed text; the full string is preserved in `title=` for hover. - `test-issue-1789-observer-firmware-cols.js` — TDD red→green static-source regression test (same pattern as `test-observers-headings.js`). - `test-observers-headings.js` — updated expected heading list with the two new columns (existing #1039 invariant test). - `test-all.sh` — wires the new test into CI. ## TDD evidence - Red: `c6c4e594c1084d664730666ba069871ac6d9755c` — test commit fails on assertions (not import errors): 5/6 cases fail because the headers/cells/title attr don't yet exist; the column-count invariant still passes because both thead and tbody are unmodified. - Green: `02a0246a185950c903828ac1225d6795f5a3b2f4` — implementation; all 6 cases pass. ## Browser verified To be verified post-deploy on staging (`http://analyzer-stg.00id.net/#/observers`). No backend changes — purely additive frontend render of fields that are already on the wire. ## Perf No new API calls, no extra fetches, two extra template-literal cells per observer row (~10s of observers in prod). O(n) render unchanged. --------- Co-authored-by: clawbot <bot@meshcore.local> |
||
|
|
5c0de8fb41 |
feat(live): optional "Multibyte only" view filter (#1780) (#1781)
Closes #1780. ## What Adds an opt-in **"Multibyte only"** toggle to the live map controls. When ON, packets whose path hash size is `< 2` bytes (single-byte, or unresolvable) are excluded from the entire live view — feed, map polylines/rain, and the packet counter — in both LIVE and REPLAY modes. - **Default OFF** — no behavior change for existing users. - Persisted in `localStorage` under `live-multibyte-only`. - Distinct from the existing global "hide 1-byte path hops" toggle: that filters individual hops within a path at every render site; this filters whole packets, on the live view only. They share no state. ## How - **`public/hop-filter.js`** — new pure, dependency-free classifier `MC_packetHashSize(rawHex, routeType)` returning `1|2|3`, or `0` when unresolvable. Reads the path-length byte from `raw_hex` (`(pathByte >> 6) + 1`), offset `5` for transport routes (route_type 0/3) else `1` — mirroring the existing `getPathLenOffset`/`computeBreakdownRanges` logic in `app.js`. Lives next to the existing `hopByteLen`/`MC_*` family; `app.js` is untouched (no duplication of the byte math). - **`public/live.js`** — `groupIsMultibyte(packets)` consumes that helper; applied at two render-time sites: the top of `renderPacketTree` (above the counter increment, so the counter reflects multibyte-only) and inside the `rebuildFeedList` group loop (so toggling re-filters the buffered feed). Toggle markup + change handler mirror the existing `liveFavoritesToggle` pattern. ## Why read from `raw_hex` and not the path hops The hash size is a property of the whole packet and is present even for zero-hop packets (where there are no hops to inspect), so reading the path-length byte is correct in all cases. Unresolvable size is treated as single-byte (excluded when ON) — we only show packets we can positively confirm are multibyte. ## Performance (hot path) The filter runs in the packet-render hot path, so: classification is **O(1) per packet group** — it reads the first resolvable observation's `raw_hex` (a short hex string, single `parseInt` of one byte) and short-circuits. No per-packet API calls, no allocation in the loop, no added O(n²). When the toggle is OFF (default) the check is a single boolean guard and does nothing else. The buffered-feed re-filter reuses the existing `rebuildFeedList` pass — no extra traversal. ## Tests - **Unit** (`test-live-multibyte-filter.js`, 9 cases): single/2-byte/3-byte classification, transport-route offset, missing/short/garbage `raw_hex` → 0, whitespace tolerance. - **E2E** (`test-live-multibyte-only-e2e.js`, Playwright): toggle present and defaults OFF; ON hides a single-byte packet while a multibyte one renders; OFF restores it; setting persists across reload. Registered in the CI live-E2E block in `deploy.yml`. ## Docs User-guide entry added in `docs/user-guide/live.md`. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
72d451221c |
tone down naive-clock observer notice (#1478 follow-up) (#1759)
Red commit:
|
||
|
|
89eade6e7b |
M6: emoji → Phosphor — final sweep, lint gate, carry-forwards (#1648) (#1654)
Red commit:
|
||
|
|
b812a98a71 |
M3: emoji → Phosphor Icons — detail panes & badges (#1648) (#1651)
Red commit:
|
||
|
|
3062745437 |
M2: emoji → Phosphor Icons — page headers & table chrome (#1648) (#1650)
Red commit:
|
||
|
|
55e4d957b1 |
M1: emoji → Phosphor Icons — top-nav, mobile nav, Compare (#1648) (#1649)
Red commit:
|
||
|
|
9002b25bce |
fix(nodes): paginate /api/nodes across map/live/analytics/packets/area-map (500-row cap) (#1637)
## Summary The server clamps `/api/nodes` `?limit` to **500** (DoS guard, PR #1540 / v3.8.3) and orders by `last_seen DESC`. Every node-list consumer issued a single big-`?limit` fetch and trusted it as the full set, so on >500-node meshes the top-500-by-advert window silently hid the tail. Because `nodes.last_seen` is updated **only on self-adverts** (never on relay traffic; `UpsertNode` is called solely from the advert path), a repeater that relays constantly but last advertised hours ago fell outside that window and **vanished from the map and live view** — while still showing "Active" in its detail panel and (since #1606) in the paginated Nodes list. #1606 fixed only the Nodes page (`nodes.js`). This generalizes that fix to the deferred siblings. ## Changes - **`public/app.js`** — new shared `fetchAllNodes(extraQuery, opts)`: pages `limit=500` + `offset` until a short page (the server's `total` is unreliable — clamped to the page size and overwritten with the filtered length under area/region filters, so we stop on a short page, not on `total`), dedups by `public_key`, returns the real deduped count as `total`. - **`public/map.js`**, **`public/live.js`** (keeps the `LIVE_MAP_MAX_NODES` ceiling via `safetyCap`), **`public/analytics.js`** (×2), **`public/packets.js`** now use the helper. - **`public/area-map.html`** is standalone (cross-origin `baseUrl`, no `app.js`) so it gets an inline copy of the same loop. - **`.eslintrc.json`** — declare `fetchAllNodes` global (no-undef). ## Tests - **`test-fetch-all-nodes-pagination.js`** — unit-tests the helper via the real `api()`+`fetch` path: pagination past 500, short-page stop vs. the unreliable server `total`, dedup across a page boundary, counts pass-through, `safetyCap` bound. 5/5. - **`test-map-nodes-pagination-e2e.js`** — browser E2E (Playwright) proving `map.js` surfaces a 501st node reachable only on page 2 and renders its marker. Verified **red→green**: against the pre-fix single fetch all 3 assertions fail (500 nodes, page-2 node absent, no marker); after the fix all pass. Wired into `deploy.yml`. ## Verification - unit 5/5, E2E 3/3, `test-frontend-helpers.js` 611/611, `npx eslint public/*.js` → 0 errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dc433e417f |
fix(#1614): getTileUrl() invokes function-typed provider urls (+ regression tests) (#1615)
Fixes #1614 ## Problem `window.getTileUrl()` in `public/roles.js` returned the active provider's `url` property as-is. After #1533 added carto/osm/stamen providers with lazy-resolved URLs (`url: function () { ... }`), the helper returned the function itself instead of a URL template string. Callers handed that function to `L.tileLayer()`, which stringified the source as the template — every tile 404'd, the map went blank, and Leaflet logged no error. User-visible impact: node-detail inset map and analytics minimap rendered zero tiles whenever a function-`url` provider was the active dark-theme pick. ## Root cause `public/roles.js:365-381` — `return p.url || p.baseUrl;` with no `typeof === 'function'` invocation. The provider registry in `public/map-tile-providers.js:45-53` declares almost every provider with `url: function() { ... }` for lazy config resolution (cartocdn domain, OSM provider/token, Stamen API key). ## Fix One-line change in the consumer (`getTileUrl()`). Invoke `url` / `baseUrl` if it's a function; otherwise return it verbatim. `map-tile-providers.js` is not touched — it remains the source of truth for the lazy-resolver pattern. ```js var u = p.url || p.baseUrl; return (typeof u === 'function') ? u() : u; ``` ## Callers reviewed | Caller | Disposition | | --- | --- | | `public/nodes.js:94` (`_applyTilesToNodeMap`) | Routes through `window.getTileUrl()` → fixed transitively | | `public/analytics.js:2055` (`L.tileLayer(getTileUrl(), …)`) | Routes through `getTileUrl()` → fixed transitively | | No other `getTileUrl()` callers | `grep -n "getTileUrl\b" public/*.js` confirms only the two above | ## Commits (red → green) - `a2b23392` — `test(#1614): red — getTileUrl() must return string, not function` — adds `test-issue-1614-tile-url-function.js`. Verified to fail on assertion (not build error) before the fix landed; passes after. - `26fcacd1` — `fix(#1614): invoke provider url() when it's a function` — minimal one-line fix in `roles.js` plus wiring the new test into `deploy.yml` and `test-all.sh`. ## Tests Unit test asserts the public contract from three angles so any regression of either branch fails CI: 1. Dark + `url: function()` → returns a string template containing `{z}/{x}/{y}`. 2. Dark + `url: 'https://…'` → returns the string verbatim (no double-invoke). 3. Dark + `baseUrl: function()` fallback → also invoked, also returns a string. Wired into CI via `.github/workflows/deploy.yml` and `test-all.sh`. ## E2E coverage Skipped intentionally. The existing Playwright harness (`test-e2e-playwright.js`) runs against a deployed BASE_URL and is not invoked from the Go CI workflow (`deploy.yml`). Adding a new E2E flow there would require standing up a leaflet/tile-loading harness for a single one-line regression. The unit test covers the exact `getTileUrl()` contract that this bug violates and would have caught it; if reviewers want a Playwright assertion later we can add it as a follow-up. Manual verification was performed against staging (`http://analyzer-stg.00id.net/#/nodes/...`). ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean (all gates pass, PII clean, red commit verified). --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
571c960ca0 |
feat(a11y/#1380): colorblind sim overlay (Brettel/Vienot) + reset-to-Wong button (#1600)
Implements the two deferred a11y stretch goals from #1361 / PR #1378. ## What 1. **Brettel/Vienot 1997 dichromatic simulation overlay** — `public/index.html` ships inline `<svg>` defs with `<filter id="cb-deut|cb-prot|cb-trit|cb-achromat">` using `feColorMatrix`. Activation rule: `body[data-cb-sim="X"] { filter: url(#cb-X); }`. `public/customize-v2.js` renders a radio group (off/deut/prot/trit/achromat) under the existing CB preset section. Preview-only — **not persisted**, per the issue spec. 2. **Reset to default Wong button** — `data-cv2-cb-reset` button that calls `MeshCorePresets.applyPreset('default')` and removes `localStorage["meshcore-cb-preset"]`. Two helpers exposed on `window._customizerV2` for unit-test drive: `applyCbSim(id)` and `resetCbPreset()`. ## TDD (red → green) - **Red:** `49155723` — `test-issue-1380-cb-sim-overlay.js` + `test-issue-1380-cb-reset-button.js`. Both load `customize-v2.js` and (for reset) `cb-presets.js` in a vm sandbox; failure is assertion (not compile). - **Green:** `5d8f3c1f` — both tests pass (21 + 7 assertions). ## Files changed - `public/index.html` — inline SVG `<defs>` + 4-rule `<style>` block. - `public/customize-v2.js` — render fns `_renderCbSimSelector` + `_renderCbResetButton`, change/click handlers, helper exports. - `test-issue-1380-cb-sim-overlay.js` (new) — string-asserts on index.html SVG filters / CSS rules / customize-v2 hooks + vm.createContext drive of `applyCbSim`. - `test-issue-1380-cb-reset-button.js` (new) — vm.createContext seeds `meshcore-cb-preset=trit`, calls `resetCbPreset()`, asserts storage cleared + `body[data-cb-preset="default"]`. - `test-all.sh` + `.github/workflows/deploy.yml` — register both tests. ## Out of scope - No new preset palettes (locked from MVP). - No persistence for the sim overlay (preview-only per spec — `localStorage` intentionally untouched by sim radio). - No colorblind-sim JS library — pure inline SVG `feColorMatrix`. Browser verified: filter rule matches via CSS sandbox; visual confirmation deferred to operator (single-tab radio, no fetch). E2E DOM assertion lives in the cv2 vm tests. Fixes #1380 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
d7bd9d57b8 |
feat(live): fullscreen toggle + collapse controls by default (closes #1532) (#1572)
Closes #1532.
## What
Implements the triage's 3-step fix path + tufte keyboard shortcut:
1. **`.live-controls` collapsed by default at all viewports** (was
≤768px only). The existing ⚙ pin reveals the toggles row on demand —
parity with the map-controls accordion pattern in `map.js`.
2. **New `#liveFullscreenToggle` button (⛶) next to ⚙.** Click or press
`F` to flip `body.live-fullscreen`. CSS under that class hides:
- `.live-header-body` (title)
- `.live-controls-body` (toggle row contents)
- `.vcr-controls` and `.vcr-bar` (timeline scrubber)
- `.bottom-nav`
- secondary panels (`.live-feed`, `.live-legend`, related show-buttons)
3. **`.live-stats-row` stays pinned top-right** with translucent chip
styling so the 3 KPI pills (nodes / active / pkts·min) earn permanent
residence per the tufte finding.
## Tufte rationale (from triage)
> data-ink ratio is poor — 11 controls + 3 KPIs displayed permanently
steal pixels from THE data (the firework animation). Defaults-on chrome
should collapse behind a pin/cog; only the 3 stat pills earn permanent
residence (sparkline-grade density). … "Fullscreen" is the right
primitive — Tufte's "shrink principle" says strip until unreadable, then
add back.
## Keyboard shortcut
`F` toggles fullscreen. Guards:
- Skips when focus is in `INPUT`/`TEXTAREA`/`SELECT`/contenteditable (no
interference with node-filter / audio sliders typing).
- Skips when modifier keys are held.
- Only fires on the `.live-page` route.
- State persists across reloads via `localStorage('live-fullscreen')`.
## TDD
| Commit | SHA | What |
|--------|-----|------|
| RED | `852a474b` | Source-invariant assertion test
`test-issue-1532-live-fullscreen.js` (17 assertions, all fail against
master). |
| GREEN | `906c6cc0` | Implementation: HTML button, JS click+keydown
wiring, CSS body-class rules + top-level `.is-collapsed` rule. |
Verify the RED commit gates the change:
```
git checkout
|
||
|
|
75a38f0285 |
Additional live map performance optimizations (#1521)
This PR introduces a major performance optimization by migrating the final DOM-heavy animation (node pulses) into the hardware-accelerated canvas engine by migrating the concentric "pulse" rings (rendered when a node receives a packet) from DOM-based Leaflet L.circleMarker elements into the high-performance HTML5 canvas animation loop (activePulses). This completely eliminates DOM thrashing when dozens of nodes broadcast simultaneously, ensuring a buttery-smooth 60 FPS even under extreme packet volume. |
||
|
|
268751ff56 |
fix(#1485): put live map animations on custom pane z=650 (above markerPane) (#1491)
## Summary Animations on the live map (packet pulses, hop-to-hop trails, drawAnimatedLine, pulseNode rings, matrix chars) render BEHIND the node base layer — community-confirmed by @EldoonNemar in #1485 after pulling latest and rebuilding. The live map looks completely static because every node marker paints on top of moving packets. Closes #1485 ## Root cause PR #1334 ("role-aware marker shapes + outline-ring highlight") swapped node markers: - **Before:** `L.circleMarker([n.lat, n.lon], {...})` — rendered into the default Leaflet `overlayPane` (z=400) alongside other vector shapes. - **After:** `L.marker([n.lat, n.lon], { icon: L.divIcon({...}) })` — rendered into the default Leaflet `markerPane` (z=600). `animLayer` and `pathsLayer` (built from `L.polyline` / `L.circleMarker` shapes) still default to `overlayPane` @ 400. With nodes now in pane 600, every node marker occluded every animation. CDP confirmed pre-fix: ``` overlayPane z=400 (animations live here) ← 2 children markerPane z=600 (nodes live here) ← 516 children ← occludes ``` ## Fix Create a custom Leaflet pane `liveAnimPane` at `z-index: 650` (strictly above markerPane) and pin both `animLayer` and `pathsLayer` to it via the `{ pane: 'liveAnimPane' }` option on `L.layerGroup`. Polylines + circleMarkers added to those groups inherit the pane from their parent, so all `drawAnimatedLine` / `pulseNode` / `animatePath` / matrix-char shapes now paint above markers. `pointerEvents: 'none'` on the pane so it does not steal hover/click events from the markerPane beneath (`clickablePathsLayer` keeps the default overlayPane and continues to handle path clicks). Diff is +14 / -2 in `public/live.js`. No CSS changes, no API changes, no protocol changes. ## TDD Red commit (`b7ca794f`): test asserts on `public/live.js` source — 1. `map.createPane('liveAnimPane')` is called in init 2. that pane is assigned `style.zIndex` ≥ 650 (strictly above markerPane @ 600) 3. `animLayer` AND `pathsLayer` are constructed with `{ pane: 'liveAnimPane' }` 4. (sanity) animLayer still hosts ≥3 animation shapes, pathsLayer ≥3 trail shapes — regression detector if someone moves circles to the default pane. CI must fail on `b7ca794f` (RED). Fix lands in `627ce341` (GREEN). Test reruns 5× clean — non-flaky (source invariants). ## Browser verified Local headless chromium (CDP) against `http://analyzer-stg.00id.net/#/live`: - **Before fix:** overlayPane z=400 (2 anim children), markerPane z=600 (516 marker children) — animations buried. - **After hot-deploy:** liveAnimPane z=650 above markerPane z=600 — animations visible on top. Will attach screenshot post-merge once staging redeploys. E2E assertion added: `test-issue-1485-live-anim-z.js:54` (`liveAnimPane z-index >= 650`). ## Test wiring `test-all.sh` line 51 added; CI runs the new test alongside the existing 1418/1420/1438/1470 suite. ## Credit Reported by @EldoonNemar in #1485 — pulled via git, built the docker image, noticed the regression same day. Bug-report quality was excellent (concise repro: "live map now shows the animated packets behind the node base layer so you can't actually see the nodes moving"). --------- Co-authored-by: mc-bot <bot@meshcore.local> |
||
|
|
93b2f4b6bb |
fix(#1473): treat 0x00 and 0xFF as reserved prefixes (matrix + generator) (#1474)
## Summary Two CoreScope surfaces treated `0x00` and `0xFF` as ordinary node prefixes, but the MeshCore firmware actively rerolls any identity whose public-key first byte is `0x00` or `0xFF` (see [`examples/simple_repeater/main.cpp:64`](https://github.com/meshcore-dev/MeshCore/blob/6b52fb32301c273fc78d96183501eb23ad33c5bb/examples/simple_repeater/main.cpp#L64)): ```cpp while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes the_mesh.self_id = radio_new_identity(); count++; } ``` As a result the analyzer was steering new operators toward identities the firmware will silently refuse — `0xFF` is also used as a wildcard flood marker in parts of the routing flow, so this isn't cosmetic. Reporter: **@halo779** (community). ## What this PR does * **`public/prefix-reserved.js`** — small new module, single source of truth. Exposes `isReservedPrefix`, `filterReserved`, `reservedCount`, `markReservedCells`. Firmware citation lives in the file header. * **Hash matrix (1-byte view)** — cells `00` and `FF` get the `.prefix-reserved` class, lose `.hash-active` so the matrix click handler skips them, and pick up an `aria-disabled` + a tooltip explaining why. * **Prefix generator** — random sampling, enumeration fallback, and the "available count" all filter out reserved prefixes. A visible note under the generator card cites `simple_repeater/main.cpp:64` directly. * **Prefix checker** — pasting a reserved prefix or full pubkey now surfaces a red `⚠️ Reserved prefix` alert above the per-tier breakdown. * **`public/style.css`** — `.prefix-reserved` greys + strikes through the cell and sets `pointer-events: none`. * **`public/index.html`** — loads `prefix-reserved.js` before `analytics.js`. ## Tests Red-then-green visible in commit history: * `test-issue-1473-reserved-prefixes.js` — `isReservedPrefix()` semantics (case + multi-byte) and `markReservedCells()` behavior on a mock 256-cell matrix. * `test-issue-1473-prefix-generator.js` — `filterReserved`, `reservedCount` per byte length, RNG-bias simulator showing the generator never returns a reserved prefix, enumeration-first-free skips `00`, and an assertion that `analytics.js` actually wires `PrefixReserved` into the generator. Both added to `test-all.sh`. Fixes #1473 --------- Co-authored-by: clawbot <bot@openclaw.invalid> |
||
|
|
d964c27964 |
feat(mobile): packets UX overhaul + nav surface + map inset + channel synthesis fixes (#1471)
## Summary Mobile UX overhaul for the packets surface plus two discoverable defects found along the way. All UI changes are mobile-only (`@media (max-width: 900px)` or `isMobile()` gates) — desktop unchanged. ## Closes - #1415 — packets layout cross-viewport jank - #1458 — Tufte mobile packets critique (P0s) - #1461 — Tufte v2 mobile packets critique (P0/P1) - #1467 — Favorites/Search/Customize unreachable on mobile - #1468 — client-side "unknown" channel synthesis - #1470 — node-detail map inset doesn't honor customizer dark provider ## Commits 1. `fix(#1468): drop client-side "unknown" channel synthesis` — `channels.js` 2. `feat(#1470): node-detail map inset honors customizer dark-tile provider` — `nodes.js`, `roles.js` 3. `feat(mobile): packets UX overhaul + bottom-nav More controls (#1415, #1458, #1461, #1467)` — `style.css`, `index.html`, `mobile-page-actions.js` (new) ## Mobile-list view changes - Kill empty chevron rail - Slim sticky THEAD (24px, retains sort affordance per operator preference) - Hide entire page-header on mobile - Mirror pause + Filters pill into navbar via new `mobile-page-actions.js` - Convert group-header `toggle-select` → `select-hash` on mobile (no dead-end expand) ## Mobile detail-panel changes - Drop redundant src→dst line (identity already in sticky header) - Hide boxed "decoded message" duplication card - Hide PAYLOAD TYPE row (already in header badge) - 2-col label/value grid (cuts panel height ~40%) - Sticky in-sheet header for packet identity - Kill iOS-style drag handle (conflicts with browser pull-to-refresh) - Make ✕ close visible + always reachable - Outer sheet `overflow:hidden`, inner content `overflow-y:auto` (scrollable region distinct, scrollbar visible) - Bottom-nav clearance (`padding-bottom: 60px`) - Close detail sheet on route change away from /packets - Tap-to-toast popovers for score tooltips (`title=` doesn't fire on touch) ## Mobile nav surface - Mirror Favorites ⭐ / Search 🔍 / Customize 🎨 into bottom-nav More sheet (#1467) - Brand stays in top nav; per-page controls (pause, Filters) injected into `.nav-left` ## Other fixes shipped together - **#1468**: drop CHAN messages with no decoded channel name (eliminates fake "unknown" channel row) - **#1470**: `_applyTilesToNodeMap` helper — node-detail inset map reads from `MC_TILE_PROVIDERS[active]` instead of hardcoded OSM; honors customizer's dark-tile provider pick + applies invert filter for inverted variants - `getTileUrl()` + new `getActiveTileProvider()` in `roles.js` now consult `MC_TILE_PROVIDERS` ## CDP verification (local chromium) Tested on staging at viewport 390×844 + 1206×928. | Surface | Before | After | |---|---|---| | Chrome above first data row | 231px (27% viewport) | ~80px (10% viewport) | | Packets visible above fold | 10 | 17 | | Detail panel duplications | 3× identity | 1× (header only) | | Mobile group-expand UX | dead-end (no chevron) | converts to select-hash | | Score tooltips on touch | broken (title= silent) | tap → toast popover | | Node detail map inset (dark mode) | always OSM light tiles | honors customizer provider + invert filter | | Bottom-nav More controls | Dark mode only | + Favorites, Search, Customize | ## What's NOT in this PR - Paths-through-node sort fix lives in #1431 (parallel PR for #1145) - Detail-panel hex byte-grid behind disclosure — operator wants it; follow-up - Group-header row sizing (some render 200–700px tall) — existing behavior, follow-up ## Test plan - [ ] Existing frontend tests stay green (`test-issue-1415-packets-layout.js`, `test-issue-1420-tile-providers.js`, `test-issue-1454-channels-toggle.js` all pass locally on this branch) - [ ] Existing Playwright E2E stays green - [ ] CDP on local chromium: 390×844 mobile + 1024×768 tablet + 1440×900 desktop — no regressions --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
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 |