mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 01:48:13 +00:00
efd66ea3f527cb9ec243dcdf72ea3170f94af968
977
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
efd66ea3f5 |
feat(mqtt): per-source status endpoint + Observers panel (#1682)
## Summary Adds MQTT source status visibility per #1043 acceptance criteria: - **Ingestor:** per-source counter registry (`cmd/ingestor/source_status.go`) tracking `connected`, `lastConnectUnix`, `lastDisconnectUnix`, `lastPacketUnix`, `connectCount`, `disconnectCount`, `packetsTotal`, `packetsLast5m` (sliding 5-min window via per-second buckets keyed by unix second — no stale-leak), `lastError`. Wired at the existing OnConnect / ConnectionLost / DefaultPublish callsites alongside the liveness watchdog. Idempotent registration so counters survive reconnects. Snapshot emitted in the existing stats file under `source_statuses` (additive, `omitempty`). - **Backend:** new `GET /api/mqtt/status` handler reads the ingestor stats file and returns the per-source list. **Broker passwords are masked** via a regex over the `scheme://user:pass@host` form (covers mqtt/mqtts/tcp/ssl/ws/wss). Mask is also applied to `lastError` as defense-in-depth (broker libs occasionally quote the failing URL). OpenAPI completeness gate satisfied with a `routeDescriptions` entry. - **Frontend:** small self-contained panel (`public/mqtt-status-panel.js`) mounted above the Observers table. Auto-refreshes every 10s, color-codes each row (green = connected + recent packet, yellow = connected idle, red = disconnected), and tears down its timer on SPA route change. ## TDD - Red commit `f19a93b5` — stub `/api/mqtt/status` handler + assertion test that the broker password is `****`-redacted. Test fails on the assertion (handler passes the URL through verbatim). Compile-clean — assertion-fail, not build-fail. - Green commit `77042e41` — `maskBrokerURL` helper + table-driven unit tests across all schemes + handler rewires to mask both `Broker` and `LastError`. - Subsequent commits land the ingestor wiring and the frontend panel. ## Tests ``` $ cd cmd/server && go test -run 'TestMqttStatus|TestMaskBrokerURL' -v ./... PASS: TestMqttStatus_MasksBrokerPassword PASS: TestMqttStatus_EmptyWhenNoStatsFile PASS: TestMaskBrokerURL_Patterns (10 subtests) $ cd cmd/ingestor && go test -run 'TestSourceStatus|TestSnapshotSourceStatuses' -v ./... PASS: TestSourceStatus_BasicLifecycle PASS: TestSourceStatus_Disconnect PASS: TestSnapshotSourceStatuses_ReturnsAll $ node test-mqtt-status-panel.js 7 passed, 0 failed ``` Full `go test ./...` clean in both `cmd/server` and `cmd/ingestor`. ## Preflight overrides - `cross-stack`: justified — issue #1043 is intrinsically full-stack (ingestor stats → server endpoint → observers panel). Per-stack split would land an unreachable endpoint or a fetch with no backend. - `check-xss-sinks` (public/mqtt-status-panel.js:55): justified — the flagged `innerHTML=` is a fully-static literal (empty-state placeholder, no payload data interpolated). All payload-bearing `innerHTML=` sites in this file run through `escapeHTML` (defined in the same file); the test `renderPanel never echoes a plaintext password (defense-in-depth)` exercises the rendered HTML against payload strings. ## Acceptance criteria - [x] `/api/mqtt/status` returns per-source connection state — `cmd/server/mqtt_status.go` - [x] UI panel shows all configured sources with live status — `public/mqtt-status-panel.js` - [x] Connection state updates on reconnect/disconnect events — `MarkConnect` / `MarkDisconnect` wired in `cmd/ingestor/main.go` - [x] Broker URLs don't expose passwords in the API response — `maskBrokerURL` + 13 test cases - [x] Works with 1-N sources — registry is keyed per-source, snapshot iterates the map **Partial fix for #1043** — per-packet `mqtt_source` attribution (the issue's "Follow-up" section) is **deferred** per the `mc-bot-triaged:v1` triage and the autofix comment ("Per-packet attribution deferred to follow-up issue"). That work requires a new observation-row column and DB schema migration, both explicitly out of scope for this PR. Refs #1043 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
626900a22a |
fix(#1668): typography pass — 14px body / 12px+500 chip floor (M3) (#1679)
Red commit:
|
||
|
|
edc6d5da02 |
fix(#1107): content-drive Live PACKET TYPES legend + dock toggles bottom-right (#1669)
Fixes #1107 Per triage fix path (#1107 comment 4672137236): the Live view PACKET TYPES legend was oversized (>60% whitespace per tufte review) and the activate/hide toggle buttons were scattered and cramped at the bottom of the map. ## Changes `public/live.css`: - `.live-legend` — added `height: max-content` + `max-width: 260px`. Panel now hugs its content instead of dominating the map. - `.legend-toggle-btn` — switched from `position:absolute; bottom:82px; right:12px` to `position:fixed; bottom:1rem; right:1rem` (the conventional map-control corner-dock per mesh-operator review). - `.feed-show-btn` — switched from scattered `position:absolute; bottom:12px; left:12px` to `position:fixed; bottom:1rem; right:1rem` with `margin-bottom:56px` so it stacks above the legend toggle. Activate/hide controls now dock together as one tidy bottom-right cluster. All colors via existing CSS variables (no hex tokens added). `test-issue-1107-live-layout.js` (new) — source-invariant assertions following the `test-issue-1532-live-fullscreen.js` pattern. Wired into the JS unit-test gate in `.github/workflows/deploy.yml`. ## TDD trace - Red commit: `c86073f68e30bb3c1c9f3880b39f4239cb681905` — test added asserting the layout invariants. Verified locally: 8 assertion failures on master CSS (exit 1). - Green commit: `4bd29f9b87ad0a1b214f60ec55ae17d6c9f2d819` — CSS fix. All 14 assertions pass. Reverting `public/live.css` returns 8 failures (test gates behavior, not tautology). ## E2E / browser verification E2E assertion added: `test-issue-1107-live-layout.js:48` (`.live-legend` height/max-width invariants) and `:72-90` (toggle button group pinned bottom-right). This is a CSS-only layout fix; the assertions are source-invariant on `public/live.css` (same pattern the codebase uses for #1532 / #1234 layout fixes — runs in the JS unit-test gate without needing a live server). Browser visual verification of the docked cluster can be done at the staging URL `http://analyzer-stg.00id.net/#/live` once the deploy runs. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean (all gates pass, no warnings to ack). --------- Co-authored-by: clawbot <bot@kpa-clawbot.local> Co-authored-by: meshcore-bot <bot@meshcore.dev> |
||
|
|
f0addfdabf |
fix(#1668): palette indirection + WCAG AA token bumps (M2 + #1671) (#1676)
Red commit:
|
||
|
|
e74e860725 |
fix(#1648): final emoji leaks — .obs-clock-naive-chip warning + analytics Channels encrypted group labels (#1665)
## What Two Phosphor lint-gate breaches found by the v3.8.4 manual-test executor — app-controlled UI labels still shipping raw emoji glyphs that the M6 final sweep (#1648) missed. One PR, two sprite swaps, same playbook as #1657. ### Findings | Test ID | Surface | Glyph | File:line | Fix | |---|---|---|---|---| | v384-1.2 | `/observers` `.obs-clock-naive-chip` | `⚠️` (U+26A0) ×14 | `public/observers.js:30` | `ph-warning` sprite | | v384-12.18 | `/analytics?tab=channels` encrypted row name cells | `🔒` (U+1F512) ×158 | `public/analytics.js:978–979` | `ph-lock` sprite | Finding 2 is a different surface from the M3/#1657 fix (which swapped the section-header label, not the per-row `displayName`). The unknown-encrypted row's `displayName` carried a raw `🔒 Encrypted (0xNN)` text label that then flowed through `esc()` into the rendered name cell as an escaped emoji glyph — exactly the same `innerText → innerHTML` class of bug. Refactored to mirror the section-header pattern: `displayNameHtml` carries the sprite-bearing raw HTML; `displayName` stays plain text for sort/aria/tests. ## TDD - **RED** `cde12370` — `test-issue-1648-followup-phosphor-leaks.js` asserts ph-warning sprite + zero ⚠ in chip output, and ph-lock sprite + zero 🔒 in analytics row labels. 6 assertions failed on master. - **GREEN** `f1c64b17` — sprite swaps applied. All 9 assertions pass. - **Anti-tautology proven both directions**: reverting only `public/observers.js` → 2 chip-related assertions fail; reverting only `public/analytics.js` → 4 analytics-related assertions fail. ## Verify - ✅ `node test-issue-1648-followup-phosphor-leaks.js` — 9/9 pass - ✅ `node test-issue-1648-m6-final-sweep.js` — 0 violations - ✅ `node test-observer-naive-clock-1478.js` — 8/8 pass (existing chip test accepts ph-warning sprite) - ✅ `node test-analytics-channels-integration.js` — pre-existing unrelated `Channel Analytics` failure only; encrypted-row assertions all pass with new plain-text `displayName` - ✅ pr-preflight all gates green (PII, branch-scope, red-commit, CSS-var, LIKE-on-JSON, async-migration, XSS sinks) - ✅ Browser-verified on staging: 11 chips render ph-warning sprite (0 emoji), 156 ph-lock sprites in row name cells (0 lock emoji on page) Browser verified: http://analyzer-stg.00id.net/#/observers + /#/analytics?tab=channels (hot-patched) E2E assertion added: `test-issue-1648-followup-phosphor-leaks.js:67` (chip), `test-issue-1648-followup-phosphor-leaks.js:147` (row cell) --------- Co-authored-by: meshcore-bot <bot@meshcore.dev> |
||
|
|
e04c7113cb |
feat: integrate hashtag channels from meshcore-channels catalogue (#1323) (#1656)
Fixes #1323 ## Summary Adds a small in-memory cache of the community-maintained hashtag-channels catalogue (`marcelverdult/meshcore-channels`) and exposes it as `GET /api/known-channels?region=XX` plus a collapsed sidebar section on the Channels view ("Known channels (catalogue)") with a one-click "+ Add" button per row. Per triage (#1323): new `cmd/server/known_channels_cache.go`, new `GET /api/known-channels?region=…`, frontend section in `public/channels.js`. No new DB tables — cache is in-memory only. ## What changed - `cmd/server/known_channels_cache.go` — `knownChannelsCache` with an atomic snapshot pointer, 24h default refresh, 30s HTTP timeout, 4 MB body cap, custom `User-Agent`. Fail-soft: a failed refresh leaves the last-known snapshot in place. Background goroutine started from `main.go` after the neighbor-graph recomputer; never blocks startup. - `cmd/server/known_channels_route.go` — `GET /api/known-channels?region=` serves the cached snapshot off the atomic pointer (never blocks on upstream). Region filter is case-insensitive ISO 3166-1 alpha-2. Empty/missing cache returns 200 with an empty entries list (fail-soft for the UI). - `cmd/server/config.go` — `KnownChannelsURL` + `KnownChannelsRefreshMs`. - `config.example.json` — example values + `_comment_knownChannels`. - `public/channels.js` — new collapsed sidebar section "Known channels (catalogue)" that lazy-fetches `/api/known-channels` on first render and renders rows with a "+ Add" button. The button calls the existing `addUserChannel(name)` path, so adding catalogue channels reuses the full save-key + decrypt flow that user-typed hashtags already use. - `cmd/server/known_channels_cache_test.go` — failing-first tests: - `TestKnownChannelsParseFixture` asserts the parser populates `GeneratedAt`/`License` and region-stamps every entry while skipping empty countries. - `TestKnownChannelsRouteRegionFilter` asserts the route returns 200 with exactly the filtered subset for `?region=be`. - `TestKnownChannelsFailSoftOn500` asserts a failed upstream fetch leaves the prior snapshot in place and bumps `failCount`. ## Upstream pinning The default URL is pinned to the specific file `channels-by-country.json` on `main`: > https://raw.githubusercontent.com/marcelverdult/meshcore-channels/main/channels-by-country.json Shape (verified 2026-05-24): ```json { "generated_at": "...", "license": "CC0-1.0", "countries": { "be": [{"channel": "#antwerpen", "description": "..."}], ... } } ``` ## Test plan ``` cd cmd/server && go test -run 'TestKnownChannels' -count=1 . ok github.com/corescope/server 0.008s ``` Red commit: 5c43cff3 (all three tests fail on assertions, build clean). Green commit: 54a1080e (parser + cache + route implemented, all three pass). ## TDD evidence (red → green) - **Red commit `5c43cff3427afd8aa2f3cce20c31058190aebc37`** — tests added with stub implementations that compile but return zero/empty so each test fails on an assertion (not a compile/import error). `go test -run TestKnownChannels` output captured in the commit message. - **Green commit `54a1080e45fd2e10da2caa156f376bf4d0212976`** — parser, cache, route, main-wiring, frontend section land; all three tests pass. ## Frontend verification Browser verified: http://analyzer-stg.00id.net/#/channels (with the `/api/known-channels` response stubbed in DevTools to simulate the cache being populated on staging, which is still on master and doesn't have the new endpoint yet). E2E assertion added: cmd/server/known_channels_cache_test.go:71 — asserts the route returns 200 and the response body's `entries` length matches the filtered subset. ## Limitations / follow-ups (not in scope of this PR) - The catalogue only ships PSK keys for a small subset of entries (the upstream schema makes `key` optional). For entries WITHOUT a `key`, the "+ Add" button still wires through `addUserChannel("#name")` — which derives the standard public-channel key from the name (the same path used today when a user types `#foo` into the Add Channel modal). For entries WITH a `key`, a follow-up PR can pass the key through to `addUserChannel` so the UX matches "paste-a-PSK". Today the key is shown in the JSON payload but not yet wired into the FE button. - No deduplication against the in-memory `/api/channels` list — the catalogue section is intentionally separate so the user sees which channels exist worldwide even if their server hasn't seen traffic. - No per-section region selector yet — the section shows the full catalogue regardless of the page-level region filter. Future work: add a dropdown. ## Preflight ``` ═══ Preflight clean. ═══ ``` cross-stack: justified — issue #1323 spans `cmd/server` (cache + route) and `public/channels.js` (sidebar surface); same feature, both halves required. --------- Co-authored-by: Kpa-clawbot <bot@corescope.local> |
||
|
|
fb6bb085a5 |
fix(analytics): render Channels group-header sprites as HTML, not escaped text (#1657) (#1658)
Fixes #1657
## Bug
On `/analytics` → **Channels** tab, the "Channel Activity" table's
group-header rows ("My Channels", "Network", "Encrypted") rendered
literal HTML source text:
```
<SVG CLASS="PH-ICON" ARIA-HIDDEN="TRUE"><USE HREF="/ICONS/PHOSPHOR-SPRITE.SVG#PH-KEY"/></SVG> My Channels
```
instead of the actual Phosphor sprites. Per-row encrypted/lock icons
rendered fine — the bug was isolated to the group-header render path.
## Root cause
`public/analytics.js` `channelTbodyHtml` builds each group-section
header by wrapping the section label in `esc()`:
```js
esc(sections[si].label) + ' <span class="text-muted">(' + rows.length + ')</span>'
```
But the labels (`sections[].label`) are hardcoded sprite-bearing
strings:
```js
{ key: 'mine', label: '<svg class="ph-icon" aria-hidden="true"><use href="…#ph-key"/></svg> My Channels' },
```
`esc()` HTML-encoded the `<` / `>` so the browser displayed the source
text rather than rendering the sprite. Affects all 3 groups (and any
future group with a sprite).
## Fix
Drop the `esc()` wrap on the hardcoded label (single line change, same
pattern as M3 commit
|
||
|
|
89eade6e7b |
M6: emoji → Phosphor — final sweep, lint gate, carry-forwards (#1648) (#1654)
Red commit:
|
||
|
|
1116801b2f |
M5: emoji → Phosphor Icons — settings & customize (#1648) (#1653)
**Red commit:** `851cc8c3a024b1675558092d772444bf4f1ec625` — failing test on a stub branch (will link CI run after PR opens). Partial fix for #1648 (M5 of 6). **Do NOT close the tracking issue** — M6 (server-side residual emoji sweep + lint gate) still pending. ## Per-file swap counts | File | Phosphor `<use>` refs | Notes | |---|---|---| | `public/customize.js` | 20 | DEFAULTS → `ph:<name>` tokens; render path keeps legacy emoji branch (back-compat) | | `public/customize-v2.js` | 26 | same as v1; cv2 overrides path unchanged | | `public/home.js` | (helpers added) | `_renderHomeGlyph` / `_renderHomeLabel` accept both `ph:<name>` and legacy emoji | | `public/geofilter-builder.html` | 5 | clear / undo / save / load buttons (+inline `.ph-icon` CSS) | | `public/audio.js` | 1 | audio unlock prompt | | `public/filter-ux.js` | 5 (3 new) | help popover star + close, saved-filter delete | | `public/style.css` | 0 | `#chList .ch-share-btn::before { content: '📤' }` removed; JS now renders an inline sprite | | `cmd/server/routes.go` | (6 `ph:` tokens) | onboarding home defaults updated in lockstep with customize-v2.js | ## Operator config back-compat — PROMINENT Per design call #1 (user-locked): existing operator-stored emoji values in `config.json` / `localStorage` are **NOT** touched. The render path supports both: ```js function renderConfigGlyph(value) { var m = String(value || '').match(/^ph:([a-z][a-z0-9-]+)$/); if (m) return '<svg class="ph-icon"><use href="/icons/phosphor-sprite.svg#ph-' + m[1] + '"/></svg>'; return esc(value); // EMOJI-OK-LEGACY-RENDER — operator-stored emoji/text path } ``` Defaults flipped to `ph:<name>` tokens, so new operators (and operators who hit "Reset to Defaults") see Phosphor sprites. Operators with stored emoji values continue to see their emoji exactly as before. Verified end-to-end (see E2E (b) below). ## cmd/server/routes.go — changed in lockstep Per design call #2: the home-defaults `steps` / `footerLinks` mirror the JS DEFAULTS, so they MUST update together. routes.go now emits `ph:<name>` tokens; the frontend home-render path resolves them. Existing tests (`TestConfigThemeHomeDefaults`) still pass — they assert structure, not glyph values. ## E2E assertions added - `test-issue-1648-m5-emoji-scan.js` — per-file zero-emoji + ph-token DEFAULTS + sprite presence - `test-issue-1648-m5-icons-e2e.js`: - (a) customize chrome — tabs/header rendered as sprites; chrome text icon-free - **(b) back-compat — injects fake `🐙` operator step into localStorage, reloads, opens customize, asserts the emoji renders verbatim in both the input value AND the live preview span; asserts the ph-token step renders as a sprite** (design call #1 in action) - (c) `/channels` modal sprite count - (d) `/audio-lab` sprite presence - (e) `geofilter-builder.html` control buttons sprite-driven - (f) every `<use>` resolves to a defined symbol id ## Out of scope (M6 cleanup) - cmd/server/routes.go residual server-rendered emoji **not** tied to customize defaults (none found by my grep — file already audited) - `make lint-no-emoji` CI grep gate (M6 owns it) - `public/icons/README.md` workflow doc cross-stack: justified — design call #2 requires Go + JS update together. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
2b6809cd28 |
M4: emoji → Phosphor Icons — map & route overlays (#1648) (#1652)
Draft for milestone 4 of #1648 — emoji → Phosphor Icons (map & route overlays). Currently at the red commit (failing test only). Implementation follows. Partial fix for #1648 (M4 of 6). Do NOT close the tracking issue. --------- Co-authored-by: bot <bot@corescope> |
||
|
|
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:
|
||
|
|
167af54eb8 |
polish(#1645): tighten observer compare — checkboxes, hierarchy, selector strip (#1647)
## Summary Polish follow-ups for the #1644/#1645 observer-comparison redesign — addresses all 5 parent visual-review findings + 3 Tufte additions in one PR. Fixes #1646. ## Coalesced fix list (status: ✅ all landed) | # | Tag | Item | Fix | Evidence | |---|---|---|---|---| | 1 | [both] | Native checkboxes were bare white squares against dark theme | Global `input[type=checkbox]/[type=radio] { accent-color: var(--accent) }` + `color-scheme: dark` on dark theme blocks. UA renders themed checkboxes everywhere now. | `screenshots-1646/after-observers-selected-dark.jpg` vs `before-observers-selected-dark.jpg` | | 2 | [parent] | Compare CTA was heavy-blue primary, redundant once both dropdowns set | `#compareBtn` now `.btn-ghost`; hidden when both observers selected (collapsed state) | `after-compare-desktop-dark.jpg` — no blue button visible | | 3 | [parent] | "vs" label at parity with dropdowns | 10px, centered, letter-spaced, opacity 0.7 | Compare-page screenshots — "vs" sits as small-caps annotation | | 4 | [parent] | SHARED column had three competing font weights; count outshone the percentage | Inverted hierarchy via new `.compare-strip-mid-pct` + `.compare-strip-mid-pct-unit`. 87% leads at `var(--fs-xl)` accent; "3,452 shared" demotes to `var(--fs-sm)`; "OF ALL UNIQUE" stays 10px caps | `after-compare-desktop-dark.jpg` middle column | | 5 | [parent] | Selector strip competed with headline strip for "look here first" attention | `.compare-controls.is-collapsed` (toggled when both observers selected) shrinks padding, hides labels + Compare button, narrows dropdowns. A/B swap still reachable | `after-compare-desktop-light.jpg` — picker compressed above the headline | | 6 | [tufte] | Decorative left accent border on `.compare-asym-line` encoded nothing | Removed (chartjunk) | `after-compare-desktop-dark.jpg` — squared cards | | 7 | [tufte] | Decorative left green border on `.compare-type-summary` encoded nothing | Removed (chartjunk) | Same | | 8 | [tufte] | Bare "87" was ambiguous; needed unit integrated as annotation | Wrapped `%` in smaller `.compare-strip-mid-pct-unit` — words and graphics co-located | Middle column hierarchy | ## Push-backs / scope discipline - **Did NOT remove the selector strip entirely.** Parent rule + operator UX: A/B swap must remain reachable. Collapsing > removing. - **Did NOT introduce a custom checkbox widget.** UA native + `accent-color` + `color-scheme` is the minimal-ink fix; new SVG/library would add chrome the data didn't ask for. - **Did NOT add new color tokens.** All restyling uses existing `--accent`, `--text`, `--text-muted`, `--surface-1/2`, `--border`. ## TDD - Red commit: `2863cfb3 test(#1646): RED — assertions for compare-polish ...` — `test-issue-1646-compare-polish.js` 9 assertions, all FAIL on master. - Green commits (3 logical groups): 1. `deb0737f fix(#1646): theme native checkboxes — global accent-color + color-scheme on dark` 2. `fb791a6f fix(#1646): tighten compare-strip hierarchy + scrub decorative borders` 3. `8033ac36 fix(#1646): ghost-style Compare CTA + collapse the picker once both observers chosen` Final: `node test-issue-1646-compare-polish.js` → 9/9 pass; `node test-issue-1644-redesign.js` → 13/13 pass (no regression); `node test-compare-overlap.js` → 6/6 pass; `node test-frontend-helpers.js` → 611/611 pass. ## Visual verification All staging-validated via local headless chromium against the hot-swapped files at `http://20.x.y.z` (staging). Surface matrix covered: - Observers list — desktop dark (with 2 rows checked) — themed accent on checkboxes - Observers list — mobile 375px dark - Compare page — desktop light + dark - Compare page — mobile 375px dark **Reviewer note: screenshot artifacts were captured locally (sandbox does not have a GitHub UI session for attachment upload).** Paths below — pull these from the same workspace location if you want to inspect: ``` screenshots-1646/before-observers-desktop-dark.jpg ← bare white checkboxes screenshots-1646/before-observers-selected-dark.jpg ← bare white checked + unchecked screenshots-1646/before-compare-desktop-dark.jpg ← blue Compare CTA; flat hierarchy; deco borders screenshots-1646/after-observers-selected-dark.jpg ← themed checkboxes screenshots-1646/after-observers-mobile-dark.jpg screenshots-1646/after-compare-desktop-light.jpg ← collapsed picker; pct leads mid column screenshots-1646/after-compare-desktop-dark.jpg screenshots-1646/after-compare-mobile-dark.jpg ``` No raw `MEDIA:` UUIDs in this body — that was the mistake on #1645 and is not being repeated. If maintainers want the images inline, drag-drop the JPGs into a follow-up comment via the GitHub web UI. ## Risk Low. Pure CSS + one class-toggle in `compare.js`'s `updateBtn` (idempotent, no race, no event loop change). `accent-color` is supported in all evergreen browsers since 2021; degrades gracefully (UA white fallback) on the rare browser that ignores it — i.e. exactly the current-master state. --------- Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@users.noreply.github.com> |
||
|
|
c93ae67ed0 |
redesign(#1644): make observer comparison feel amazing — themed button vocabulary + state-preserving multi-select + Tufte-grade compare page (#1645)
## What was wrong PR #1642 promoted observer comparison to a first-class IA citizen but shipped three problems: `class="btn-secondary"` buttons that fell back to browser-default white/gray because no such CSS rule existed; the 30-second auto-refresh blew away `<tbody>.innerHTML` and destroyed every compare-select checkbox along with its state; and the `#/compare` page itself showed three card-boxes forcing the eye to do mental subtraction. ## Design rationale (Tufte) The comparison page now leads with **one row of three numbers above one proportional diff bar** — shared-axis small multiples in place of three nearly-identical cards. The eye reads the whole comparison in one fixation. Asymmetric reach is demoted from two big cards to two compact, ctx-style sentences with mono-numeric percentages. The button vocabulary borrows route-view v2's restraint: surface tokens for neutral chrome, accent only on the primary CTA, no gradients or shadows. The checkbox column visually recedes when no row is picked (empty-state IS the design) and lights up only once a selection exists. Everything composes existing CSS tokens — no new top-level color literals — so all themes (light, dark, CB presets) Just Work. ## Inventory of CSS additions | Selector | Role | |---|---| | `.btn-secondary`, `.btn-secondary[disabled]` | Themed neutral button (low-emphasis CTA) | | `.btn-ghost` | Minimal transparent-until-hover variant (reserved for future) | | `.compare-page`, `.compare-page .page-header` | Page-level container, overrides `.page-header { justify-content: space-between }` | | `.compare-breadcrumbs` | Themed breadcrumb link strip | | `.compare-controls`, `.compare-selector`, `.compare-select-group`, `.compare-select`, `.compare-vs`, `.compare-btn` | Selector strip — re-themed with surface tokens | | `.compare-strip`, `.compare-strip-row`, `.compare-strip-side`, `.compare-strip-mid`, `.compare-strip-name`, `.compare-strip-count`, `.compare-strip-mid-count`, `.compare-strip-mid-label`, `.compare-strip-sub` | The headline small-multiples row (A \| shared \| B) | | `.compare-bar`, `.compare-bar-seg`, `.compare-bar-{a,both,b}`, `.compare-bar-legend`, `.compare-legend-item`, `.compare-dot-{a,both,b}` | Single proportional diff bar | | `.compare-asym`, `.compare-asym-line`, `.compare-asym-pct` | Compact directional-reach sentences (replaced the two big cards) | | `.compare-type-summary`, `.compare-type-summary-label`, `.compare-type-badge` | Shared-type pill row with ctx-style border-left accent | | `.compare-tabs`, `.compare-tabs .tab-btn`, `.tab-btn.active` | Tabs reskinned to match the muted-then-accent pattern | | `.compare-summary-text`, `.compare-warning`, `.compare-good` | Themed status notes | | `.col-compare-select`, `.col-compare-select input[type="checkbox"]` | Compare-select column — muted when empty, full text + `--selected-bg` row tint when populated | | `.obs-table.has-compare-selection` | Marker class so the column changes intensity only when something is picked | | `.observers-page .page-header`, `.obs-refresh-spacer` | Header layout (flex with right-side refresh icon) | | `.observer-detail-page .compare-with-group` | Grouped picker + Compare button surface on the detail page | **Tokens used:** `--surface-1`, `--surface-2`, `--border`, `--accent`, `--accent-hover`, `--text`, `--text-muted`, `--row-hover`, `--hover-bg`, `--selected-bg`, `--status-green`, `--status-amber`, `--status-amber-light`, `--status-amber-text`, `--radius-sm`, `--radius-md`, `--badge-radius`, `--space-xs..xl`, `--fs-sm..xl`, `--mono`. **No new top-level color tokens were introduced.** ## Before PR #1642's bare `<button class="btn-secondary">` rendered with the browser-default white pill and the compare page showed three rgba-tinted cards (`rgba(34,197,94,0.1)`, `rgba(74,158,255,0.1)`, `rgba(255,107,107,0.1)`) — chartjunk with no theme awareness. See #1644 description for the bug repro. ## After (screenshots) **Desktop — observers page (light, empty + selected states):** - Empty: `MEDIA: 42d90aa5-643c-4e88-8b5d-3383cfa2dfe4.jpg` - Two selected (rows tinted, button enabled): `MEDIA: a6d9b397-ffe5-4eeb-b07b-ef89041ab6ea.jpg` **Desktop — observer detail (light, picker + Compare grouped):** `MEDIA: 17b9b47d-5e97-4293-8558-e9b37c244335.jpg` **Desktop — compare page (light, real data via mock — fixture has 0 overlap):** `MEDIA: be169bf2-f31b-480a-97b1-4f678745471b.jpg` **Desktop — compare page (dark):** `MEDIA: 436477a7-600c-4ac4-aa9d-97db968246d3.jpg` **Desktop — observers (dark, two selected):** `MEDIA: 850242c3-db77-460f-895f-0a6e6b150758.jpg` **Mobile 375px — observers (dark):** `MEDIA: 338b543c-0705-41ec-95da-e2c2a8db2065.jpg` **Mobile 375px — compare page (dark, stacks cleanly):** `MEDIA: 380a984c-26f0-4f47-b4ba-d655571721c9.jpg` ## Test plan - `node test-issue-1644-redesign.js` — 8/8 (new behavioral suite for this PR) - `node test-issue-1562-observers-summary.js` — 13/13 - `node test-compare-overlap.js` — 6/6 - `node test-compare-flood-filter.js` — 6/6 - `node test-frontend-helpers.js` — 611/611 - `node scripts/check-css-vars.js` — 0 undefined refs across 1901 var() calls - Browser-validated against local fixture build at `localhost:13580`: desktop light/dark, mobile 375px light/dark, observers + detail + compare pages. Checkbox preservation verified by manual refresh click — state survives the tbody rewrite. ## TDD - Red commit: `94e019c5` — 7 behavioral assertions that all FAIL on master (no top-level `.btn-secondary`, no `preserveCompareSelection` helper, rgba literals in compare-card rules). - Green commit: `a246208d` — implementation. All 8 assertions pass (the rgba assertion was relaxed to a conditional check after the cards were removed entirely in favor of the strip; an additional `.compare-strip exists` assertion was added). ## Out of scope - The server-side `&since=...` parser is strict about RFC3339 and rejects the `.000Z` suffix the frontend emits; this means the comparison page shows zeros against any data > 24h old. Filed separately — not a regression introduced by this PR. Screenshots showing populated numbers use a `comparePacketSets` test stub. - Backend Go untouched. Fixes #1644 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw> |
||
|
|
531bc8acb3 |
feat(#1640): promote observer comparison to first-class — 3 new entry points + multi-select (#1642)
## Summary The observer-comparison page (`#/compare`) is a powerful side-by-side overlap tool but was reachable from exactly one place — an icon-only 🔍 button in the observers page header. Most operators never found it. This PR promotes it to an IA citizen with **three new entry points** plus breadcrumbs back from the compare page to each observer's detail page. Red commit: `f937d29658e25973786f88a9ddeaaa33768f269e` (test asserts all three new affordances are present + navigate correctly; would have caught the original undiscoverability). Green commit: `5ceb34b66d780a971d3a43de06a0744445bdbecf`. ## Design rationale Three orthogonal user paths reach the same goal: - **Operator who lands on `/observers`** sees a labeled button — no more icon-guessing — and a row-selection workflow for direct manipulation ("pick two, compare"). - **Operator who lands on a specific observer's page** sees an in-context "Compare with…" picker — the comparison is parameterised with the current observer, removing the cognitive jump back to the list. - **Operator who already has two observer IDs** can still hit `#/compare?a=…&b=…` directly — legacy deep-links regression-guarded by the E2E. Plus: every compare-page view now shows `Observers › <A> ⇆ <B>` breadcrumbs that link back to each observer's detail page, so users can navigate sideways instead of bouncing through the list. ## Entry points added | # | Surface | Affordance | File:line | |---|---|---|---| | A | `/observers` header | `<button>` labeled "🔍 Compare observers" | `public/observers.js:125-130` | | B | `/observers/<id>` header | "Compare with…" `<select>` + Compare button | `public/observer-detail.js:90-103`, `:128-145`, `:436-456` | | D | `/observers` table | Per-row checkbox column + "Compare selected (N)" button enabled at exactly 2 | `public/observers.js:131-137`, `:295-302`, `:148-167`, `:354-378` | | breadcrumbs | `/compare` page | `data-role="compare-breadcrumbs"` with linked anchors → both detail pages | `public/compare.js:108`, `:202-228` | The pre-existing 🔍 link was REMOVED and replaced by (A) — the issue explicitly called for the icon-only affordance to go away. ## Before — current state on staging - Observers page header has only a bare 🔍 icon — no text label, indistinguishable from a generic search affordance. - Observer-detail page has zero comparison affordances; the user has to back out, find the observers list, locate the icon, then re-select both observers from scratch. - Compare page has a single back-arrow to `/observers` but no breadcrumb links to either compared observer's detail page. ## After — each new entry point browser-verified locally Built `cmd/server`, ran against `test-fixtures/e2e-fixture.db` on `:13581`, drove via headless chromium. Each step taken from a clean reload, screenshot captured (attached separately to the requesting session): - (A) Observers page header now shows a clearly-labeled "🔍 Compare observers" button alongside a "⚖️ Compare selected (N)" button (disabled when count !== 2). - (D) Two rows checked → "Compare selected (2)" enables → click → navigates to `#/compare?a=…&b=…` with both selects pre-populated and breadcrumbs reading `Observers › Kennedy Repeater ⇆ GY889 Repeater`. - (B) Observer-detail header now hosts a "Compare with…" `<select>` populated with the 30 other observers + a Compare button (disabled until a target is picked) → pick + click → navigates with the current observer pre-set as A. - Legacy `#/compare?a=…&b=…` deep-link still pre-populates both selects unchanged (covered by the E2E regression guard). ## Test plan - New: `test-issue-1640-compare-discovery-e2e.js` — 9 assertions across all three entry points + breadcrumbs + legacy-deep-link regression guard. Wired into `.github/workflows/deploy.yml`. - Local browser-verified each new affordance end-to-end (screenshots above). - `node --check test-issue-1640-compare-discovery-e2e.js` ✅ - Preflight clean (all 11 gates ✅), see below. ## Preflight checklist ``` ── [GATE] PII ── ✅ pass ── [GATE] Branch scope ── ✅ pass (5 files: 1 workflow, 3 frontend, 1 E2E) ── [GATE] Red commit ── ✅ pass (f937d29 verified failing) ── [GATE] CSS-var defined ── ✅ pass ── [GATE] CSS self-fallback ── ✅ pass ── [GATE] LIKE-on-JSON ── ✅ pass ── [GATE] Sync migration ── ✅ pass ── [GATE] Async-migration gate ── ✅ pass ── [GATE] XSS sinks ── ✅ pass ── [WARN] img/SVG ratio ── ✅ pass ── [WARN] Themed <img> SVG ── ✅ pass ── [WARN] Fixture coverage ── ✅ pass ═══ Preflight clean. ═══ ``` ## Accessibility - (A) and "Compare selected" buttons carry both visible text AND `aria-label`; disabled state uses both `disabled` and `aria-disabled="true"`. - (B) picker has an `<label class="sr-only">` plus `aria-label` for screen readers. - (D) per-row checkbox has `aria-label="Select <observer name> for comparison"`. - Breadcrumbs use `<nav aria-label="Compare breadcrumbs">` with a meaningful `›` separator (aria-hidden). ## Out of scope - The compare engine itself (`public/compare.js` data flow) is untouched. - New comparison metrics (track #671). - Analytics-nav link suggested as option (C) in the issue — covered by (A) which is more visible at the same top-nav tier; happy to add later if needed. Fixes #1640 --------- Co-authored-by: clawbot <bot@openclaw> |
||
|
|
d72ab69f87 |
fix(#1639): observers table — wire TableSort with numeric/time column types (#1641)
## Summary Wires the shared `TableSort` helper (already used by the nodes table, #679) into the observers table at `#/observers`. Adds `data-sort-key` / `data-type` attrs on every `<th>`, `data-value` on every `<td>` with the raw sortable value (epoch-ms for times, integers for counts, abs-seconds for clock skew, derived health rank for the status dot), and initializes `TableSort` at the end of `render()` — after the new `tbody` is in the DOM — to avoid the #679 init race on async refresh. ## Before / after - **Before:** clicking any column header on `#/observers` does nothing — bare `<th>` cells, no click handlers, no `TableSort.init` call (per #1639 repro). - **After:** clicking a header toggles asc/desc with `aria-sort` indicator + ▲/▼ glyph. Numeric columns (Packet Health, Total Packets, Packets/Hour, Clock Offset, Uptime) sort numerically. Time columns (Last Status, Last Packet) sort by ISO timestamp, not the `"23d ago"` display string. Active column + direction persisted in `localStorage` under `meshcore-observers-sort`. Default sort: Last Status desc (matches existing default ordering). ## Test plan - TDD red commit `0dcd5304` — fails on assertion `Total Packets <th> must carry data-sort-key="packet_count"` against master. - Green commit `d4f0376f` — both assertions pass. - E2E assertion added: `test-issue-1639-observers-sort-e2e.js:46` (header has `data-sort-key`+`data-type`) and `:62` (click reorders rows numerically desc). - Local commands run from the worktree: - `cd cmd/migrate && go build -o ../../cs-migrate-1639 .` → `./cs-migrate-1639 -db test-fixtures/e2e-fixture.db` - `cd cmd/server && go build -o ../../cs-server-1639 .` → run on port 13581 against the fixture DB - `CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1639-observers-sort-e2e.js` → ✅ both tests pass - `node test-observers-headings.js` (#1039 regression) → ✅ still passes - Browser verified: headless chromium against the local fixture server. Clicked Total Packets header three times: first click → `aria-sort=descending` + ▼ glyph + rows ordered 139,261 → 5,791. Second click → `aria-sort=ascending` + ▲ glyph. Third click → back to descending. tbody re-renders correctly after the 30s `loadObservers` auto-refresh (no init race — the new TableSort controller binds to the fresh header). - pr-preflight: clean (all hard gates + warnings pass against `origin/master`). ## Files changed - `public/observers.js` — wire TableSort, add `data-sort-key`/`data-type`/`data-value`, init after render - `test-issue-1639-observers-sort-e2e.js` — new E2E (red→green) - `.github/workflows/deploy.yml` — run the new E2E alongside existing playwright group Fixes #1639 --------- Co-authored-by: openclaw-bot <bot@openclaw> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
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> |
||
|
|
59d664692d |
fix(#1630): reach page — narrow-viewport CSS (no h-scroll, shrunken map) (#1634)
Red commit:
|
||
|
|
e2212f5015 |
feat(nodes): per-node Reach page + GET /api/nodes/{pubkey}/reach (v2, review-complete) (#1627)
Re-submission of #1625 (which was merged early, then reverted in #1626) — now with **all three round-1 reviews addressed** so it lands in one hardened state instead of as post-merge follow-ups. ## What Per-node **Reach** view: a standalone page (`#/nodes/{pubkey}/reach`) + a node-detail section + `GET /api/nodes/{pubkey}/reach`. It shows which nodes a node has a **stable two-way RF link** with, derived from raw `path_json` adjacency (a path travels origin→observer, so `[A,B]` ⇒ B heard A). A link is bidirectional when both directions have observations; the **bottleneck** (weaker direction) rates two-way reliability. Nodes are identified only by **unique 2–3 byte** path prefixes (1-byte collides → excluded). ## Review fixes folded in vs #1625 **Performance (Carmack):** hard scan LIMIT (200k) + modest prealloc; `json.Unmarshal` replaced by a single-pass `parsePathTokens` (100k-row scan 2.2M→1.3M allocs, 344→203ms); memoized resolver; size-hinted maps (attribution over 100k rows: 102 allocs); `context.Context` plumbed; cache `RWMutex` + evict-oldest (no full wipe); singleflight dedup; degree/rank from a 60s shared snapshot; bench rewritten (ReportAllocs, 1k/10k/100k, mixed-payload, isolated attribution). **Correctness/safety + tests (Independent + Kent Beck):** pubkey validation → 400; error logging instead of silent swallow (first_seen / degree / marshal→500 / discarded rows); `public_key=?` index use; canonical `PayloadADVERT`; `min()` builtin; documented cache-slice immutability; mux ordering comment. New tests: scanReachRows decode, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation across rows, HTTP-level attribution (asserts non-zero we_hear/they_hear), 400/404/blacklist/cache-hit. **UI / a11y / Tufte:** in-map legend (tiers + thresholds); dropped the colour+width double-encoding (constant width, colour-only); colour-blind glyphs (●●●/●●/●) + tier title beside the bottleneck number; dark-theme `--link-*`; lighter table (horizontal rules, sentence-case headers); map built once + link layer updated in place on toggle (no flicker); time-range no longer flashes a loader; `destroy()` generation guard; statCard escaping; scoped `@media print` to `#nq-report`; `fieldset/legend` + `for/id` toggles; `aria-pressed` / `aria-live` / back-link `aria-label`; "distance (km)" + bottleneck tooltip + no-GPS note; inline styles → CSS; decorative emoji removed. **Docs:** api-spec documents the 5-min cache, 200k scan cap, and 400. ## Testing - `cmd/server` full suite green; reach unit + endpoint + bench all pass. - `eslint public/*.js` (no-undef) and the XSS-sink gate clean. - E2E updated: request status checks + exact (non-tautological) toggle assertions + hard map-render assert. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## TDD-history note (Kent Beck gate) This branch carries production + tests together, not a fabricated red→green sequence. That's deliberate: the branch was rebased onto upstream and the intermediate SHAs were squashed, so reconstructing a "failing-test-first" commit after the fact would be theatre, not evidence — and rewriting history to stage it would be dishonest. The behaviour is instead covered by a comprehensive, anti-tautological suite (directional attribution edges, 3-byte token branch, non-advert first-hop guard, observer SNR aggregation, HTTP-level attribution asserting non-zero counts, scan-cap truncation, zero-reach 200-not-404, companion mis-attribution, cache eviction). Requesting maintainer acceptance of the work on test *substance* rather than commit *choreography*; the net-new-UI exemption is not claimed for the server endpoint. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: meshcore-bot <bot@meshcore> |
||
|
|
9c5faab1e4 |
Revert "feat(nodes): per-node Reach page (#1625)" (#1626)
Reverts #1625. #1625 was merged before the round-1 reviews (Independent / Kent Beck / Tufte) were addressed. Reverting to land it cleanly: a fresh PR will re-add the feature with the perf pass, the backend correctness/safety + test-coverage fixes, and the UI/a11y (Tufte) batch folded in, so it goes through review in a single hardened state rather than as a string of post-merge follow-ups. No functional loss — the feature returns in the replacement PR. |
||
|
|
47f85f6c4c |
feat(nodes): per-node Reach page + GET /api/nodes/{pubkey}/reach (directional link quality) (#1625)
## What
Adds a per-node **Reach** view that answers "how well does this specific
node hear, and get heard by, its neighbours?" — both as a standalone
page (`#/nodes/{pubkey}/reach`) and as a section on the node detail
page.
New endpoint: **`GET /api/nodes/{pubkey}/reach`**.
## What it measures
For the target node it derives, from raw `path_json` adjacency (a path
travels origin→observer, so in `[A,B]` B received A directly):
- **Directional link counts** per neighbour: `we_hear` (how often we
received them) vs `they_hear` (how often they received us).
- **Bidirectional / bottleneck**: a link is two-way stable when both
directions > 0; the weaker direction is the bottleneck and rates real
two-way reliability.
- **Importance**: neighbour degree + rank, relay-observation volume,
bidirectional-link count, direct-observer count.
- **Direct observers**: who received the node at 0 hops, with SNR.
Reliability rule: a neighbour is only attributed when its pubkey
**prefix is unique** at the path's byte length (collisions are skipped,
never misattributed).
## UI
- Standalone Reach page + node-detail section.
- Reusable bidirectional link map (OSM) with links coloured by
bottleneck.
- Incoming/outgoing toggles to isolate each direction.
## Naming note (deliberate, no collision)
This is distinct from the existing **per-observer reachability** in
topology analytics (`ReachNode` / `ObserverReach` / `perObserverReach`).
This PR adds its own `NodeReach*` response structs in a new
`node_reach.go` and a new `/api/nodes/{pubkey}/reach` route — there are
no symbol or route collisions (verified: `go build ./...` clean). Happy
to rename to disambiguate further (e.g. "Link Quality") if you'd prefer
to reserve "Reach" for the per-observer feature.
## Testing
- `cmd/server`: endpoint shape/404/limit-clamp + unit tests for token
derivation and directional attribution, plus a scan benchmark — all
pass.
- Frontend: helper tests + Reach-page E2E (`test-node-reach-e2e.js`),
standalone route + incoming/outgoing toggles.
- `go build ./...` and `eslint public/*.js` (no-undef) clean.
## Docs
Design spec, implementation plan, and the `GET
/api/nodes/{pubkey}/reach` API contract are included under `docs/`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a4776557ae |
feat(#1290): use firmware repeat:on|off hint to exclude listener-only observers from disambiguator (#1624)
Closes #1290. cross-stack: justified — backend persists firmware-side `repeat` hint to a new observers column, frontend surfaces the listener/repeater status as a badge on the observers list and node-detail Heard By table per the issue's UI acceptance criterion. ## What Firmware 1.16 publishes a `repeat: on|off` flag in the MQTT `/status` JSON (confirmed by @cwichura on the issue thread — see [`MQTTMessageBuilder.cpp:58`](https://github.com/agessaman/MeshCore/blob/b45373a31f111fb0de98bb3b168226d09ceadc47/src/helpers/MQTTMessageBuilder.cpp#L58) in `agessaman/MeshCore mqtt-bridge-implementation-flex`). Listener-only observers (`repeat:off`) by firmware contract never relay packets, so they cannot legitimately be a hop in someone else's resolved path. This PR plumbs the hint end-to-end so the disambiguator stops considering them. ## How * **`internal/dbschema`**: idempotent `can_relay INTEGER DEFAULT 1` migration on `observers`, plus `AssertReady` probe (server fatal-logs if absent). Mirrored in `cmd/ingestor/db.go` `CREATE TABLE` for fresh DBs. Annotated `PREFLIGHT: async=true` — `DEFAULT 1` is constant so SQLite does this as a metadata-only schema rewrite. * **`cmd/ingestor`**: `extractObserverMeta` accepts `repeat` as bool, case-insensitive string (`on|off|true|false|yes|no`), or numeric `0|1`. Missing field → `nil` → `COALESCE` preserves the existing column value (back-compat with legacy observers). Plumbed through `UpsertObserverAt` and the prepared upsert statement. * **`cmd/server`**: `GetNonRelayObserverPubkeys` + new `prefixMap.markNonRelay` drop matching candidates inside `pm.resolveWithContext` at the top of the resolver, so all 4 tiers see the pruned candidate set. `ObserverResp.CanRelay` is surfaced on `/api/observers` and `/api/observers/{id}`. `GetNodeHealth` enriches per-observer rows with `can_relay` so the node-detail badge renders. Probe-and-fall-back when the `can_relay` column is absent (legacy test fixtures). * **`public/`**: listener vs repeater pill on observers list, observer detail `Relay` stat card, and node-detail `Heard By` table. CSS uses existing theme vars. ## Test Added `TestResolveWithContext_ExcludesNonRelayObservers_Issue1290` in `cmd/server/resolve_non_relay_1290_test.go` covering all three required cases: * `repeat:off` pubkey → not a candidate (assertion failed in red commit `5f7fdb96`, passes after green `f12911dc`) * `repeat:on` pubkey → still a candidate (regression guard) * legacy obs (no field) → still a candidate (back-compat) Red→green proof: ``` $ git log --oneline origin/master..HEAD |
||
|
|
e9aed641bd |
fix(traces): overlay per-hop SNR on path graph for TRACE packets (#1004) (#1622)
## Summary Phase 2 of #979 — overlay per-hop relay SNR onto the Traces page path graph for TRACE-type packets. When the viewed packet is a firmware TRACE and `decoded.snrValues` is non-empty, each hop edge in the existing path graph gets a small `<text class="hop-snr">` label at its midpoint with the corresponding numeric SNR value (Tufte: numeric overlay only — edge color encodes observer attribution, thickness encodes count; per triage, do **not** double-encode). Non-TRACE packets render unchanged. Observer-level SNR in the timeline is unaffected (different concept: observer receive SNR vs relay hop SNR). ## TDD - **Red commit:** `8d441aa51e4b38dec962c7a32d31e9f7080f2786` — adds 4 assertions in `test-traces.js` against the (not-yet-emitted) `<text class="hop-snr">` element. CI run: see Actions on this PR. - **Green commit:** implements the SNR-label emission in `renderPathGraph` (`public/traces.js`). ## Test `test-traces.js` asserts: - TRACE + non-empty `snrValues` → `<text class="hop-snr">` labels render with the numeric values - non-TRACE → labels absent (regression gate for AC2) - TRACE + empty `snrValues` → labels absent - `decoded` omitted → labels absent (back-compat) Fixes #1004 --------- Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: clawbot <bot@openclaw.local> |
||
|
|
f66ff40a54 |
fix(#1619): bump feed-detail-card z-index + make popup draggable (#1620)
Red commit: 7eeeee5d76f385b939d4c49256be6418ca5dfe34 (CI run: pending — first PR-triggered run) Fixes #1619 ## Problem The `feed-detail-card` popup in the Live view (the one with the ↻ Replay button) is undraggable and frequently sits behind the legend (z=1000) in the lower-right, leaving the Replay button unreachable. ## Fix 1. `public/live.css` — bump `.feed-detail-card` z-index from `600` → `1050` (above legend z=1000, below mobile bottom-nav z=1100). Immediate unblock. 2. `public/live.js` — add a `<div class="panel-header">` containing a small title + the existing close button to the card markup; register the card with the existing `DragManager`. The bootstrap-scoped `dragMgr` is exposed on `window._liveDragMgr` so the popup-creation site (outside that scope) can call `dragMgr.register(card)` after appending. Responsive gate (`enabled` flag) is handled inside DragManager — no extra wiring needed. No localStorage persistence: the popup is ephemeral (dismissed on outside-click). Initial position (`right:14px; top:50%`) unchanged — drag is opt-in. ## Test (RED → GREEN) Source-invariant assertions on live.css and live.js: - `.feed-detail-card` z-index === 1050 - card markup contains `.panel-header` - `window._liveDragMgr` is assigned - popup-creation site calls `_liveDragMgr.register(card)` RED commit asserts all four — failed CI as expected. GREEN commit makes them pass. E2E assertion added: test-issue-1619-feed-detail-card-draggable.js:36 Triage: https://github.com/Kpa-clawbot/CoreScope/issues/1619#issuecomment-4641392168 |
||
|
|
16c7ea4b82 |
fix(#1528): theme-track .vcr-scope-btn.active + .copy-link-btn:hover backgrounds (#1578)
Red commit: b018a752e82723b076316693df3271dbfb5e608b Fixes #1528 ## What Completes the four-surface accent-token migration from the triage on #1528. PR #1530 handled three of the four call-out surfaces (`.field-table .section-row td`, `.copy-link-btn` base rule, `.multibyte-badge`). This PR finishes the remaining two surfaces that still had hardcoded blue `rgba(59,130,246,...)` literals on their tinted backgrounds: - `public/live.css:1045` `.vcr-scope-btn.active` — `background` + `border-color` now go through `var(--accent-bg)` / `var(--accent-border)` with the prior literals retained as safe fallbacks. - `public/style.css:2673` `.copy-link-btn:hover` — `background` now goes through `var(--accent-border)`. ## Why The triage's "CSS-var theming illusion" finding: foreground text on these surfaces was already bound to themable tokens, but the backgrounds were blue-locked. Picking a non-blue accent in the customizer produced surfaces where the foreground tracked the theme but the background stayed blue — failing WCAG-AA on light accents (the bug screenshots in the issue). ## TDD - Red commit (`b018a752`): adds a Playwright E2E assertion that overrides `--accent-bg` / `--accent-border` on `:root` with sentinel colors and asserts `.vcr-scope-btn.active`'s computed `backgroundColor` / `borderColor` reflect them. Verified failing against the unfixed CSS — actual bg was `rgba(59, 130, 246, 0.2)`, sentinel was ignored. - Green commit (`d46055cd`): the two-line token swap. Verified passing after `docker cp` of the patched CSS onto staging — bg followed the override. E2E assertion added: `test-e2e-playwright.js:3318` ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all 9 hard gates pass, no warnings. Critically the "CSS self-fallback" and "CSS-var defined" checks (the gates that exist for exactly this class of bug) both pass. ## Scope Strictly the two remaining surfaces from #1528's fix path. No other `--accent` usage was touched. --------- Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> |
||
|
|
1bdb92de88 |
feat(#1574): operator-configurable liveMap.maxNodes (default 2000) (#1577)
Red commit: 94dc1d70a5a710271721d981cb5e36b7127b00dc Fixes #1574. cross-stack: justified — by design. Adds one server-side knob (`liveMap.maxNodes`) on the Go API and consumes it on the frontend (`public/live.js`) via the shared `/api/config/client` bootstrap in `public/roles.js`. Cannot land server-only or frontend-only without either dropping operator config (frontend-only) or leaving the literal in place (server-only). ## Problem (per triage) `public/live.js:2515-2516` hardcodes `/api/nodes?limit=2000` for the live-map node-load path. Reporter measured headroom at N=4300 and asked for an operator knob. Same `2000` magic also lives at `public/live.js:480` for the VCR-rewind `/api/packets?limit=2000`. ## Fix - New `liveMap.maxNodes` field in `Config` (default 2000). - `Config.LiveMapMaxNodes()` server-side clamp: `[100, 20000]`; zero/negative falls back to default. Defangs misconfig (e.g. 1M would OOM the SQLite read + JSON serialization path). - `/api/config/client` now returns `liveMapMaxNodes`. - `public/roles.js` reads it at bootstrap into `window.LIVE_MAP_MAX_NODES` (default 2000 to preserve behavior on stale caches). - `public/live.js` consumes `LIVE_MAP_MAX_NODES` at both the `/api/nodes` call sites (formerly :2515-2516) and the VCR-rewind `/api/packets` call (formerly :480) — single source of truth, in-scope per triage's "factor into a sibling const" suggestion. - `config.example.json` documents the knob with `_comment_maxNodes` per AGENTS.md config rule. ## TDD 1. **Red** (`94dc1d70`): added `test-issue-1574-live-map-max-nodes.js` (grep-asserts the literal is gone + `LIVE_MAP_MAX_NODES` / `liveMapMaxNodes` are wired + config example has the field) and `cmd/server/livemap_maxnodes_1574_test.go` (`/api/config/client` exposes `liveMapMaxNodes` + clamp table-driven cases). Stub `LiveMapMaxNodes()` returns 0 so the test compiles and fails on assertion, not import. 2. **Green** (this commit): real `LiveMapMaxNodes()` clamp + wire-up. All assertions pass; existing `cmd/server` suite still green. ## E2E note Frontend assertion is grep-based (literal removal + constant reference), in the established `test-issue-*` style used elsewhere (e.g. `test-issue-1189-live-iata-badge.js`). No Playwright change needed for a literal-replace; behavior validation is the server-side clamp + JSON shape tests. ## Out of scope No customizer UI change — operators set this in `config.json`, same pattern as `liveMap.propagationBufferMs`. Customizer surfacing can land as a follow-up if the operator wants it. --------- Co-authored-by: mc-bot <bot@corescope.local> Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> |
||
|
|
3898688d6d |
analytics: Relay Airtime Share endpoint + dumbbell chart (#1359) (#1601)
Implements the locked spec from #1359. Red commit: 68a140a8 — `distinctRelayCount` stub returns 0; test fails on assertion (compiles + runs to assertion, not a build error). Green commit: 48c2ddad — real implementation. ## Backend (in-memory, no SQL, no schema change) - `cmd/server/relay_airtime_share.go` - `distinctRelayCount(tx)` — unions the resolved-pubkey reverse index for `tx.ID`. That index already dedups `(pubkey-hash, txID)` pairs across every observation's `resolved_path`, so its length IS the count of distinct repeaters that forwarded the packet. NOT length of any single observation's resolved_path (the bug-trap from #1358). - `computeRelayAirtimeShare(window)` — per-tx `score = payload_bytes × distinctRelays`, bucketed by `payload_type`, sorted desc by airtime_pct. - `GetRelayAirtimeShareWithWindow` — cached behind existing `rfCache` + `rfCacheTTL` pool. Shallow-copies the cached payload with `cached=true` for the client. - `cmd/server/routes.go` — `GET /api/analytics/relay-airtime-share?window=…` returning `{rows:[{payload_type,type,count,count_pct,score,airtime_pct}], total_count, total_score, window, cached}`. ## Frontend - `public/analytics.js` - `renderRelayAirtimeDumbbell(data)` — horizontal dumbbell chart per payload_type. Gray dot = count %, colored dot = airtime %, connector line between them = the divergence, shared 0-100% axis, sorted desc by airtime. - Tooltip: payload_type, count %, count N, airtime %, raw score, within-mesh caveat. - Title: **Relay Airtime Share**. - Subtitle (exact): `Score = payload bytes × distinct repeaters that forwarded the packet. Counts relay re-transmissions; originator TX excluded. Not comparable across meshes.` - Mounted on the Overview tab immediately beneath Payload Type Mix. ## Tests `TestRelayAirtimeShare_ADVERTvsACKDivergence` — the locked acceptance scenario: - 1 ADVERT (200 B, 8 distinct relays) → score 1600, airtime 100% - 1000 ACKs (10 B, 0 relays each) → score 0, airtime 0% - Count distribution is the inverse (ACK 99.9%, ADVERT 0.1%). - Sort assertion: ADVERT is rows[0] by airtime_pct desc. Full suite: `go test -short ./cmd/server/...` → PASS (25.9s). ## Acceptance criteria - [x] In-memory `airtime_usage_score` accumulator in analytics path - [x] `distinctRelayCount(tx)` helper unioning resolved-pubkey reverse index across all observations of `transmission_id` - [x] `/api/analytics/relay-airtime-share?window=…` endpoint - [x] Cached via existing `rfCache` + `rfCacheTTL`; no new cache layer - [x] Dumbbell chart on `/analytics` beneath Payload Type Mix; gray=count, colored=airtime, shared axis, sorted desc by airtime - [x] Title + subtitle exactly as specified - [x] Tooltip with payload_type, count %, count N, airtime %, raw score, caveat - [x] Unit test demonstrates the ADVERT-vs-ACK divergence - [x] No new SQL, no new index, no schema migration (verified via diff) - [ ] Live staging bench (<5ms p99 uncached / <1ms cached) — deferred to follow-up; cached behind 60s `rfCacheTTL` so steady-state cost is a map lookup ## Preflight overrides - Branch scope cross-stack: justified — backend endpoint and frontend chart are a single deliverable per #1359 spec (one chart bound to one endpoint, no incremental staging). Fixes #1359 --------- Co-authored-by: bot <bot@local> |
||
|
|
a26a412c9b |
feat(perf): 5-min rolling-baseline anomaly detection for Write Sources (#1120) (#1593)
## Summary Addresses the remaining acceptance gap on #1120: a true **5-minute rolling-baseline anomaly detector** for the Perf-page Write Sources table. The endpoints + ingestor wiring + UI scaffolding landed in #1123 (partial); this PR replaces the ad-hoc tx-rate comparison with the rolling baseline the issue actually asks for, and adds a JS unit test that proves the ⚠️ flag fires at 11× baseline. ## What changed - **`public/perf.js`** — new pure helper `detectPerfAnomalies(history, current, opts)`. Computes per-component current rate and rolling baseline rate over a window (default 5 min). Flags components whose current rate > 10× baseline. Includes a 0.05/s floor so a stale `0` baseline doesn't false-positive at startup. - **UI** — Write Sources table now shows `Rate/s`, `Baseline/s`, and `Anomaly` columns. Operators can sanity-check the ⚠️ rather than trusting opaque output. History is kept on `window` and pruned to a 6-min sliding ring. - **`test-perf-anomaly.js`** — new VM-sandbox test asserting: - ⚠️ fires when one component runs at 11× its 5-min baseline - No ⚠️ at 5× (under threshold) - No ⚠️ until ≥30s of history has accumulated ## TDD evidence (red → green) - Red commit `590f04d3`: introduces the stub `detectPerfAnomalies` (returns empty `{flags:{}}`) + the test. Test FAILS on the `assert(r.flags.backfill_path_json === true, ...)` assertion — not a build error. ``` ❌ ⚠️ fires when backfill rate hits 11× the 5-minute baseline: expected backfill_path_json flagged at 11× baseline, got flags={} 2 passed, 1 failed ``` - Green commit `726a5e78`: implements the rolling-baseline detector. All 3 tests pass; existing `test-packet-filter.js` (79 tests) still green; `cmd/server` Go tests for `/api/perf/*` still green. ## What is NOT in this PR (deferred / out of scope per brief) - **SQLite-stats subsection** (WAL size + cache hit rate + pending checkpoint) — `/api/perf/sqlite` already exists (landed in #1123). Issue body lists it as a metric category, brief explicitly marks it OPTIONAL. Not regressed; no changes needed. - **Ingestor `/proc/self/io` bridge** — already lives in the ingestor stats file (`ProcIO` field, `internal/perfio`) and is rendered on the Perf page. No change. - **Issue #1340** (SQLite write-lock instrumentation) — separate PR in flight, not piggybacked. - **No new metrics backend** (no Prometheus, no OpenTelemetry). Pure JSON over `/api/perf/*`. ## Hard-rule compliance - Files changed: 2 (`public/perf.js`, `test-perf-anomaly.js`) — well inside the 3-files-outside-allowed-set cap. - `Stats` struct unchanged. - All colors via CSS variables — no hex literals introduced (grep clean). - TDD: red commit fails on assertion, green commit passes — visible in branch history. - PII preflight: clean on both commits. Partial fix language deliberately not used — this completes the issue's UI acceptance criterion. Leaving `Fixes #1120` off so the user can verify on the staging deploy before closing. --------- Co-authored-by: meshcore-bot <bot@meshcore> |
||
|
|
d6384c3c59 |
fix(#1217): honor time-window filter on Route Patterns analytics (#1592)
## What The Route Patterns chart on `/#/analytics` ignored the Time window picker — every selection returned identical data. This PR threads `?window=` through to the backing endpoints and the store-level computation. ## Root cause `cmd/server/routes.go:2065` (`handleAnalyticsSubpaths`) and `cmd/server/routes.go:2090` (`handleAnalyticsSubpathsBulk`) never called `ParseTimeWindow(r)`. The store-level entry points (`GetAnalyticsSubpaths`, `GetAnalyticsSubpathsBulk`) had no window-aware variant. The frontend (`public/analytics.js`) didn't append `&window=` to the `/analytics/subpaths-bulk` request. ## Fix ### Backend (`cmd/server/store.go`) Added `GetAnalyticsSubpathsWithWindow` + `GetAnalyticsSubpathsBulkWithWindow`. Zero `TimeWindow` → byte-equivalent to the existing fast path (no perf regression on the default view). Non-zero window → iterate `s.packets`, filter on `tx.FirstSeen` via `TimeWindow.Includes`, reuse `rankSubpaths`. Cached by `(region|area|window)`. ```diff -data := s.store.GetAnalyticsSubpaths(region, minLen, maxLen, limit) +window := ParseTimeWindow(r) +data := s.store.GetAnalyticsSubpathsWithWindow(region, minLen, maxLen, limit, window) ``` ```diff -results := s.store.GetAnalyticsSubpathsBulk(region, groups) +results := s.store.GetAnalyticsSubpathsBulkWithWindow(region, groups, ParseTimeWindow(r)) ``` ### Frontend (`public/analytics.js`) `renderSubpaths` now appends `&window=<value>` to the `/analytics/subpaths-bulk` request, matching how RF / topology / channels tabs already wire the picker. ## Before / after ``` GET /api/analytics/subpaths?window=24h → totalPaths=2 (all data — ignored window) GET /api/analytics/subpaths?window=24h → totalPaths=1 (24h-bounded — honored) ``` ## Tests `cmd/server/subpaths_window_test.go`: - `TestSubpathsHonorsTimeWindow_StoreLevel` — seeds a 1h-old tx with path `[aa,bb]` + a 30d-old tx with path `[cc,dd]`; asserts the unbounded call sees both and the 24h-windowed call sees only the recent one. - `TestSubpathsHandlerHonorsTimeWindow` — same scenario via the HTTP handlers for `/api/analytics/subpaths` and `/api/analytics/subpaths-bulk`. TDD: red commit `eefc27d3` (test fails on assertion with stub that ignores window), green commit `4c4c45d0` (implementation makes it pass). Full `go test ./...` in `cmd/server` green locally (~47s). ## Performance Default view (no window selected) is unchanged — `window.IsZero()` short-circuits to the existing precomputed-index hot path. Windowed view is O(N_tx · path²), same complexity as the existing region-filtered slow path. Results cached per `(region|area|window)`. Closes #1217 --------- Co-authored-by: Kpa-clawbot <bot@corescope> |
||
|
|
37a7a92730 |
fix(#1616): detach slide-over panel on close (architectural focus-restore fix) + --repeat-each=20 CI gate (#1617)
Fixes #1616. Supersedes the soften-and-track approach from #1172 (now closed). ## What Architectural fix for the slide-over close path so it no longer transitions through a `focused-but-hidden` state. Chromium-headless cannot deterministically order focus/blur events when `panel.hidden = true` happens in the same microtask as a delegated table re-render — root cause of the flake family that was blocking ~8 unrelated PRs at a time and flipping master CI ~50%. ## How (three changes per #1616 acceptance criteria) 1. **Panel detach on close.** `open()` attaches panel + backdrop to `<body>`; `close()` removes them. `isOpen()` is now a boolean flag (`panelOpen`) instead of `(!panel.hidden)` — the closed panel literally does not exist in the document tree, so there is no focused-but-hidden window. 2. **Focus restore by `data-value` lookup at restore time.** Sync `tr.focus()` BEFORE detach. If `document.activeElement !== tr` after the sync call, attach a one-shot `MutationObserver` on the table's `tbody`; on a matching row re-attach, call `.focus()` once and `disconnect()`. Observer has a 2s timeout fallback so it doesn't leak when the row is genuinely gone. 3. **Permanent CI flake-gate.** New step in `.github/workflows/deploy.yml`: runs `test-slideover-1056-e2e.js` 20 consecutive times. Any single non-zero exit aborts. If this step ever turns red post-merge, the focused-but-hidden state has crept back in. ## Hard-asserted (no more soft-warn) All three deferred assertions are now `assert(...)`: - `focus-restore@800: Escape returns focus to originating row` - `focus-restore@800: X-button click returns focus to originating row` - `resize@800→1440 nodes: cleanup releases panel, backdrop, scroll-lock, focus` (focusRestored portion) ## Commits - `fce39304` — RED: un-skip the two soft-skipped assertions - `cead78df` — GREEN: architectural fix (detach + MutationObserver) - `4f6d5c47` — CI: permanent `--repeat-each=20` flake-gate ## Verification The 20-run gate is the verification. Watch the new `Slide-over E2E flake-gate (#1616, --repeat-each=20)` step on this PR's CI; merge only if it passes. ## Why this is the right fix Five prior patches (`7891b70`, `366af4f`, `36ebecc`, `df5397f`, `d681505`) all targeted the focus call ordering and all flaked in CI Chromium-headless. The unfixable bit is "hidden-but-was-focused" — Chromium reorders blur/focus across that transition non-deterministically. Removing the transition (detach instead of hide) removes the race entirely. Closes #1616. Closes #1172 (already closed). --------- Co-authored-by: openclaw-bot <bot@openclaw> Co-authored-by: CoreScope bot <bot@corescope.local> Co-authored-by: clawbot <bot@clawbot.local> |
||
|
|
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> |
||
|
|
26105748ff |
fix(nodes): paginate /api/nodes — surface all nodes past 500-row server cap (#1606) (#1607)
## Summary Fixes #1606 — frontend `public/nodes.js` issued a single `?limit=5000` fetch to `/api/nodes` and trusted the response as the complete node set. After PR #1540 (v3.8.3) clamped `/api/nodes` `?limit` to 500 as a DoS guard, that single fetch silently truncated to the top 500 rows by `last_seen DESC`. On the reporter's 2313-node deployment, **78% of nodes (1813) were invisible** in the Nodes page, with no UI indication anything was missing. Replaces the single fetch in `loadNodes()` with a pagination loop driven by `data.total` from the first response. Stops when `_allNodes.length >= total`, when the server returns a short page, or at a 10 000-row safety cap. `counts` is taken from the first response and refreshed on each subsequent page (last writer wins; the server returns the same `counts` payload each call). Scope is deliberately narrow per the (munger) finding in the triage comment: the three sibling call sites (`analytics.js:2080,2817`, `packets.js:791`) are **NOT** touched here. They get their own follow-up. ## Repro ```bash curl -s "https://analyzer.marwoj.net/api/nodes?limit=5000" | jq '{nodes_len: (.nodes | length), total}' # Before fix on >500-node deployment: # { "nodes_len": 500, "total": 2313 } ← frontend silently displays only 500 ``` ## Before / after evidence Unit test `test-issue-1606-pagination.js` drives `loadNodes()` against a mocked `api()` exposing 1200 fixture nodes with a 500-per-page server cap (mirrors the real `/api/nodes` clamp). | | `_allNodes.length` | `data.total` | |---|---:|---:| | Before (single fetch) | **500** | 1200 | | After (pagination loop) | **1200** | 1200 | Red commit: `700a5cc4` (test asserts `_allNodes.length === data.total`, fails 500 ≠ 1200). Green commit: `6d51da45` (pagination loop, test passes). All 611 tests in `test-frontend-helpers.js` continue to pass — the existing nodes.js WS-handler runtime tests are unaffected. ## Browser verified Mocked-API unit test only — staging currently has <500 nodes so the bug isn't reproducible there. The reporter's deployment (`analyzer.marwoj.net`, 2313 nodes) is where the visible regression occurs. The unit test reproduces the exact failure mode against a controllable fixture. ## E2E assertion added `test-issue-1606-pagination.js:170` — `assert.strictEqual(all.length, env.fixtureTotal, ...)` ## Files changed - `public/nodes.js` — `loadNodes()` single fetch → pagination loop - `test-issue-1606-pagination.js` — new regression test (sandboxed nodes.js + mock api) ## Out of scope (deferred to follow-up) Per triage's (munger) note, these three siblings have the same single-fetch bug and need their own focused PR: - `public/analytics.js:2080` (`limit=10000`) - `public/analytics.js:2817` (`limit=10000`) - `public/packets.js:791` (`limit=2000`) Closes #1606 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
1be0aec808 |
fix(frontend): reliably restore row focus on panel close (#1602)
fix for the focus-restore@800 E2E test that's currently failing on master (see runs 26990436988, 26986419081) Chromium headless is notorious for dropping synchronous or rAF-based focus restores when elements are hidden. By manually blurring the active element before hiding the panel, and staggering the focus restore with a setTimeout macrotask after the rAF, we ensure the focus call lands after the browser has completed all implicit focus resets and event handlers. Furthermore, dynamically evaluating the focus resolver directly inside the deferred focus attempt prevents the target element from becoming stale if a live WebSocket packet triggers a background table re-render in the intervening milliseconds. |
||
|
|
1f65d7811b |
fix(#1599): replay handoff no longer freezes the map (suppressLive flag) (#1603)
## Summary Partial fix for #1599 — replay from packets sidebar no longer freezes the live map. Clicking **Replay** on a packets-page row wrote the packet to `sessionStorage['replay-packet']` and navigated to `/#/live`. On init, `live.js` called `vcrPause()` to silence live WS traffic during the replay. But `vcrPause()` sets `VCR.mode = 'PAUSED'`, and `renderAnimations()` gates `anim.progress` advancement on `!isPaused` — so the replayed animation never advanced and the map appeared frozen. ## Fix Introduce a module-level `suppressLive` flag dedicated to muting live WS traffic without entering `PAUSED`. The WS handler's `LIVE` branch honors the flag (still ticking `updateTimeline` so the UI keeps reflecting traffic). The replay handoff sets the flag for ~12 s — long enough for the animation to play out — then clears it. Files changed: - `public/live.js` — module flag (`~145`), replay handoff (`~1502`), WS LIVE branch (`~897`) - `test-issue-1599-replay-freeze-e2e.js` — new Playwright E2E (seeds `sessionStorage['replay-packet']`, asserts `activeAnimations` drains after the handoff) - `.github/workflows/deploy.yml` — wire the new E2E into the deploy E2E block ## TDD trail | Commit | Role | | --- | --- | | `8a0add00` | Red — failing E2E (asserts the queued animation drains; pre-fix it never does → `FAIL: activeAnimations did NOT drain after replay handoff (count=1) — replay freeze regression`) | | `8069210d` | Green — `suppressLive` flag replaces `vcrPause()` in the handoff | | `c2a84a3e` | CI wiring | Locally reproduced both states against the e2e-fixture DB (Chromium via `CHROMIUM_PATH=/usr/bin/chromium`): - HEAD red commit: `2 pass, 1 fail` (assertion-shaped, not compile) - HEAD green commit: `3 pass, 0 fail` Browser verified: local Chromium against `corescope-server -port 13581 -db /tmp/e2e-fixture.db -public public` — `replay-packet` key is consumed by the init path, animation queues, and drains post-fix. E2E assertion added: `test-issue-1599-replay-freeze-e2e.js:111` (`activeAnimations drained to 0`). ## What this PR does NOT do The reporter explicitly called out a second, separable problem on the same issue: `renderPacketTree(packets, true)` runs with `isReplay = true`, which skips `addFeedItem` (`public/live.js:3155`), so the bottom-left feed shows "Waiting for packets…" even once the map animates. That is a UX decision (should the replayed packet appear in the feed?) and is intentionally **not** addressed here. Leaving #1599 open so the operator can decide. Hence: **"Partial fix for #1599"** — no `Fixes #` keyword. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all hard gates ✅, no warnings. --------- Co-authored-by: corescope-bot <bot@corescope> |
||
|
|
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> |
||
|
|
3df8924114 |
fix(#1218): include multi-byte prefix repeaters in 1-byte hash usage matrix view (#1591)
## Problem
`/analytics` Hash Usage Matrix 1-byte view excluded repeaters configured
for 2- or 3-byte hash prefixes. In MeshCore, 1-byte path-matching is a
first-byte equality check, so any packet routed by 1-byte hash collides
on that first byte regardless of the downstream repeater's configured
prefix size. Omitting multi-byte prefix repeaters under-reports real
conflicts in the 1-byte hash space.
## Fix
**Data layer — `cmd/server/store.go` (`computeHashCollisions`,
~L7907-L7918 before, L7907-L7941 after):**
Before — `one_byte_cells` was populated only from `prefixMap`, which
only contained repeaters with `hash_size == 1`:
```go
if bytes == 1 {
oneByteCells = make(map[string][]collisionNode)
for i := 0; i < 256; i++ {
hex := strings.ToUpper(fmt.Sprintf("%02x", i))
oneByteCells[hex] = prefixMap[hex]
if oneByteCells[hex] == nil {
oneByteCells[hex] = make([]collisionNode, 0)
}
}
} else if bytes == 2 { ... }
```
After — additionally project all `hash_size in {2,3}` repeaters to their
first byte:
```go
if bytes == 1 {
// ... (same baseline population) ...
for _, cn := range allCNodes {
if cn.Role != "repeater" { continue }
if cn.HashSize != 2 && cn.HashSize != 3 { continue }
if len(cn.PublicKey) < 2 { continue }
hex := strings.ToUpper(cn.PublicKey[:2])
if _, ok := oneByteCells[hex]; !ok { continue }
oneByteCells[hex] = append(oneByteCells[hex], cn)
}
}
```
The 2-byte view's bucketing is unchanged — that view continues to count
only repeaters configured for 2-byte prefixes (those semantics differ).
**UI — `public/analytics.js` L1459:** clarified the 1-byte view
description so the inclusion of multi-byte prefix repeaters is explicit.
## API shape
No response-shape change. `one_byte_cells[HEX]` is still
`[]collisionNode`; only the contents now include 2/3-byte prefix
repeaters in the appropriate first-byte buckets. The existing frontend
decoder is unaffected.
## Tests
-
`cmd/server/routes_test.go::TestHashCollisionsOneByteIncludesMultiBytePrefixRepeaters`
— seeds three repeaters with first byte `CC` configured for 1/2/3-byte
prefixes plus an unrelated `DD` repeater, asserts all three appear in
`one_byte_cells["CC"]`, and that the 2-byte view's `nodes_for_byte` is
unchanged.
Red commit `278bdf8d` (test only) fails on assertion ("got 1, want 3");
green commit `9127ea4e` passes.
## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ clean.
Closes #1218
---------
Co-authored-by: clawbot <bot@corescope>
|
||
|
|
373ee81641 |
fix(UI): Additional fixes for issue #1532 (#1580)
- Eliminated extra space to the right of the map filters. - Made the map filters and mesh live a single line with a divider - Resized the input and dropdowns in the map filters so they meet WCAG 2.5.5 by being at least 44px high, but appearing 30px high - Turned the filters cog and the fullscreen button into native leaflet icons that are large enough to meet WCAV 2.5.5 compliance - Increased the size of the zoom buttons to meet WCAG 2.5.5 compliance on both the live and map pages - If the top nav bar is pinned, it won't disappear during fullscreen but if it isn't pinned, it will disappear with everything else. - The cog and full screen button change color to show they're active Final Outcome in 4k <img width="2878" height="1406" alt="image" src="https://github.com/user-attachments/assets/28db46a2-f1bb-4d9c-9d77-30c444b4ef3d" /> Final Outcome in 1080p <img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/120be8ec-0279-40fc-925a-243e9c0bcc1c" /> |
||
|
|
1a2b8c48be |
feat(node-detail): link RTC-reset warning to offending packet hashes (#1094) (#1590)
## Problem Node detail's bimodal-clock warning showed only `⚠️ N of last M adverts had nonsense timestamps (likely RTC reset)` — no way to tell which packets, no way to verify the heuristic, no way to drill in. ## Fix Additive, two-sides: **Backend** (`cmd/server/clock_skew.go`) - New type `BadSample { Hash, AdvertTS, SkewSec }`. - New field `NodeClockSkew.RecentBadSamples []BadSample` (`omitempty`). - Populated from the **same** bimodal-bad classification pass that produces `RecentBadSampleCount` — no heuristic change. `tsSkewPair` carries `hash` + `advertTS` so the classifier can record per-sample evidence without a second walk; drift code is unaffected (reads only `ts`/`skew`). **Frontend** (`public/nodes.js`) - `bimodalWarning` preserves the existing count summary line, then renders a `<ul>` of bad samples: each `<li>` is `<a href="#/packets/HASH">hash[:8]</a> → formatTimestamp(advertTS)` with ISO tooltip. Defensive `Array.isArray` so older API responses still render the summary alone. ## TDD - **Red:** `cmd/server/clock_skew_issue1094_test.go::TestIssue1094_RecentBadSamples_ExposesHashAndTimestamp` — seeds 3 healthy + 2 bimodal-bad adverts, asserts `RecentBadSamples` has length 2 with the expected hashes and advert timestamps. Fails on the assertion (`len = 0, want 2`) with the stub-only commit. - **Green:** classifier populates the slice; existing #1285 and bimodal tests stay green. - Red commit: `ed501f4b` - Green commit: `54305b06` ## Cross-stack Backend + frontend ship together (`cross-stack: justified` commit). API stays backward compatible (`omitempty` server, `Array.isArray` client) but the feature only lights up with both halves present. ## Preflight Clean — PII, branch scope, red-commit, CSS vars, XSS sinks, migrations, fixture coverage all pass. ## Acceptance - [x] Warning lists specific packet hashes - [x] Each hash links to `#/packets/<hash>` - [x] Bad advert timestamp shown next to the hash - [x] Pattern is reusable — `BadSample` is a clean shape any future heuristic that flags specific packets can adopt Fixes #1094 --------- Co-authored-by: openclaw-bot <bot@openclaw.local> |
||
|
|
116efe4bd7 |
fix(#1402): gesture hints — edge-drawer mobile-only + row-swipe widening (re-fix) (#1586)
Partial fix for #1402 ## Summary Re-fix two of the four #1402 regressions on mobile after `#1452` silently reverted the prior fix (`6ec08acb`). Two predicate flips in `public/gesture-hints.js` + extended E2E coverage to prevent another silent revert. This PR is intentionally **scoped to Bug 2 and Bug 4 only**. Bug 1 and Bug 3 were also dropped by `#1452` and are NOT restored here — `#1402` remains open for the rest. ## Changes - `public/gesture-hints.js` (edge-drawer): `window.innerWidth > 768` → `window.innerWidth <= 768`. The edge-swipe drawer is the MOBILE layout's nav per #1064/#1184; `nav-drawer.js` `NARROW_MAX=768` (inclusive — narrow when width <= NARROW_MAX). Above 768 the sidebar is persistent, no edge-swipe is needed. - `public/gesture-hints.js` (row-swipe): widen route filter from `/^#\/(packets|nodes)/` to `/^#\/(packets|nodes|channels|observers)/`. Channels and observers also render swipable row tables. - `public/gesture-hints.js`: expose read-only `window.__gestureHintsDefs` test hook (frozen) for direct predicate probes (avoids race with render path). - `test-gesture-hints-1065-e2e.js`: add assertions (i)+(j) at vw=393 — edge-drawer relevant on `/#/home`, row-swipe relevant on `/#/channels`; (k) negative-direction gate at vw=1024 asserts `edge-drawer.relevant() === false` on desktop. Retarget (e) from 1024x800 → 393x800 to match the corrected mobile-only gate. ## TDD - Red commit: `1e7545d1` — test additions fail against current production code (edge-drawer relevant returns false at vw=393, row-swipe filter rejects /channels). - Green commit: `6f844d5b` — predicate flips + route widening make both assertions pass. - Polish commit (round-1 fixes): boundary <= 768, doc-header refresh, freeze the test hook, negative-direction gate (k), precondition assertion on (i). ## Acceptance criteria from #1402 - [ ] Bug 1 (`window 'load'` rescheduler + `pointer: coarse` gate) — dropped by #1452, NOT restored in this PR. Tracked in #1402. - [x] Bug 2 (edge-drawer mobile-only) — fixed here. - [ ] Bug 3 (pull-refresh touch-gate decoupling) — dropped by #1452, NOT restored in this PR. Tracked in #1402. - [x] Bug 4 (row-swipe widening → /channels + /observers) — fixed here. - [x] E2E mutation gate: assertions (i)+(j)+(k) provably fail if either predicate is reverted or re-broadened. ## Notes - Silently reverted by #1452 — re-fix here, with regression gates so the next reviewer of the next refactor will see the assertions fail rather than the production behavior change unnoticed. ## Preflight All gates pass (PII, branch scope, red commit, CSS vars, XSS sinks, etc.). --------- Co-authored-by: meshcore-bot <bot@meshcore.local> Co-authored-by: fix-1166-bot <bot@corescope.local> |
||
|
|
7533b3b67b |
feat(nodes): sortable First Seen column on Nodes table (#1166) (#1587)
## Summary Adds a sortable **First Seen** column to the Nodes table so users can spot newly observed repeaters in their region (per the reporter's use case). Closes #1166 ## Backend `/api/nodes` already exposes `first_seen` per node via `db.scanNodeRow` (sourced from the existing `nodes.first_seen` column — no schema migration, no recomputation, no extra query cost). The red test pins that contract. ## Frontend (`public/nodes.js`) - New `<th data-sort-key="first_seen" data-sort-default="desc">First Seen</th>` between Last Seen and Adverts. - Cell renders via `renderNodeTimestampHtml(n.first_seen)` — same relative-time + absolute-ISO `title=` tooltip as the Last Seen column. Empty values render as `—`. - `sortNodes` gains a `first_seen` branch with **empty-last** semantics: nodes without a `first_seen` always sort to the bottom regardless of asc/desc direction, so unknowns never clutter the top of the table. - Empty-state `colspan` bumped 7 → 8. ## TDD - **Red commit** `112442f4` — `test-issue-1166-first-seen-column.js` + `cmd/server/first_seen_1166_test.go`. The backend half passes on red (field already returned); 5 frontend assertions fail on assertions (column header missing, sort branch missing, empty-last violated). - **Green commit** `9274b36c` — only `public/nodes.js`. All 6 tests pass. Verified red is real-fail (assertion-shaped) by checking out the red commit's `nodes.js` and re-running the test: 5 failures, all on `assert.strictEqual`, none on parse/import. ## Test results ``` node test-issue-1166-first-seen-column.js → 6 passed, 0 failed node test-frontend-helpers.js → 611 passed, 0 failed go test ./cmd/server/... → ok (45.16s, all pass) ``` ## Files changed - `public/nodes.js` (+14 / −1) - `test-issue-1166-first-seen-column.js` (new) - `cmd/server/first_seen_1166_test.go` (new) ## Scope guardrails - No schema migration. - No new files outside the worktree's three allowed surfaces. - No refactor of other Nodes columns. - Empty cells handled in both render (em-dash) and sort (always last). --------- Co-authored-by: fix-1166-bot <bot@corescope.local> |
||
|
|
7292d60fbe |
feat(#1508): config-driven disabled tabs in customizer modal (#1579)
# feat(#1508): config-driven disabled tabs in customizer modal Fixes #1508. ## Why The customizer modal mixes one-shot operator chrome (`branding`, `home`, `geofilter`, `export`) with daily-use viewer toggles (`theme`, `nodes`, `display`). Non-technical users get confused by the admin tabs and skip past the controls they actually need. There's no current way to hide individual tabs server-side — only via CSS, which doesn't prevent state mutation. ## What Adds a single operator knob: `customizer.disabledTabs` in `config.json`. The named tab ids are filtered out of `_renderTabs()` in `public/customize-v2.js` before render. - `config.example.json` — new `customizer` block, default `disabledTabs: []` (zero behavior change for existing operators). - `cmd/server/config.go` — new `CustomizerConfig` type, optional pointer on `Config`. - `cmd/server/routes.go` + `cmd/server/types.go` — `/api/config/client` now surfaces `customizer.disabledTabs` (always an array, empty when unset). - `public/customize-v2.js` — `_renderTabs()` filters by id. - `cmd/server/customizer_disabled_tabs_test.go` — RED-then-green tests covering both the configured-and-defaulted shapes. ## TDD trail 1. RED commit adds the failing tests + minimal `CustomizerConfig` stub so the package still compiles; both tests fail on the assertion (`body.customizer` is `<nil>`) — not on import. 2. GREEN commit wires the field through `/api/config/client` and the frontend tab filter; both tests pass. ## Scope 5 files. No new API surface, no UI for editing the list (operator edits `config.json` directly per the issue body). Backward-compatible: missing `customizer` block defaults the list to empty. --------- Co-authored-by: bot <bot@local> |
||
|
|
545013d360 |
refactor(#1424): extract pure helpers into route-view-utils.js (#1581)
## Summary Pure refactor extracting three pure helpers out of the `public/route-view.js` IIFE into a sibling `public/route-view-utils.js`, per the triage fix path on #1424. - `escapeHtml` - `buildPacketContextBlock` - `buildSnrSparkline` All three are exposed via `window.MC_ROUTE_UTILS`, and the IIFE in `route-view.js` unpacks the namespace into locals at the top so every existing call site stays textually unchanged. `spiderFanFor` was deliberately **not** extracted: it consumes Leaflet types (`mapRef.latLngToLayerPoint`, `mk.getLatLng` / `setLatLng`, `L.point`) and mutates marker state. A one-line comment was added at its definition explaining the reason (matches the dijkstra caveat from the triage comment). ## Changes - `public/route-view-utils.js` — new file, 151 LoC. Single IIFE exporting `window.MC_ROUTE_UTILS = { escapeHtml, buildPacketContextBlock, buildSnrSparkline }`. Body is byte-equivalent to the originals. - `public/route-view.js` — three function definitions removed, replaced with an 8-line namespace unpack stanza. `spiderFanFor` keeps a NOT-extracted comment. Net: `-126/+12`, file now 1473 LoC (was 1588). - `public/index.html` — adds `<script src="route-view-utils.js?v=__BUST__">` immediately before the existing `route-view.js` script tag. Repo-wide grep confirmed `index.html` is the only HTML loader for `route-view.js`. ## TDD exemption justification Pure refactor: no test files modified; existing CI suite green without test edits. Test files diff vs `origin/master`: **none**. Local full-suite (`sh test-all.sh`) is identical between this branch and `origin/master@9b36b7c4` — same single pre-existing `channels.js sidebar links to #/analytics` failure on both, **zero new regressions** introduced by this PR. Route-view-specific guards all green: ``` test-issue-1418-polish-review.js passed: 22 failed: 0 test-issue-1418-spider-fan.js passed: 25 failed: 0 test-issue-1418-edge-weights.js passed: 18 failed: 0 test-issue-1418-cb-preset-ramp.js passed: 19 failed: 0 test-issue-1418-raw-hex-extraction.js passed: 39 failed: 0 test-issue-1418-deeplink-hops-channels.js passed: 27 failed: 0 ``` ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → **clean** (all gates and warnings pass). ## Out of scope - No bundler / build step (no-build is a project constraint, per triage) - DOM-touching helpers stay inside the IIFE (they rely on closure state) - `spiderFanFor` stays (Leaflet types — not pure) Closes #1424 Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local> |
||
|
|
9b36b7c487 |
feat(#1518): add branding.homeUrl override for embedded deployments (#1576)
Red commit:
|
||
|
|
892eb2c02a |
fix(#1509): expose --nav-active-bg as a themeable token (#1571)
Red commit: 07a69e48ebc976caf4bf15f7a937e378a42ef718 (CI run: pending — PR triggers first run) Fixes #1509 ## Problem `--nav-active-bg` is defined in `public/style.css` (line 105) and used by every active-state nav link (`.nav-link.active`, `.nav-more-menu .nav-link.active`, plus the responsive blocks), but the customizer has never mapped it into `THEME_CSS_MAP`. Result: presets, per-operator overrides, and server-side `theme.*` config can recolor every other nav token (`navBg`, `navBg2`, `navText`, `navTextMuted`) — but the active-pill background stays stuck on the hardcoded `rgba(74, 158, 255, 0.15)` (light) / dark-mode equivalent. Themes look broken on the one element users stare at. ## Fix Triage-specified path, no scope creep: - Add `navActiveBg: '--nav-active-bg'` to `THEME_CSS_MAP` in `public/customize-v2.js`. - Surface in the Theme tab's advanced color list (`THEME_COLOR_KEYS` derives from the map; adding to `ADVANCED_KEYS` makes it render in the panel). - Add label + hint so the input is self-explanatory. - Seed defaults on the default preset's `theme` + `themeDark` so the rendered value matches today's hardcoded rgba and dark mode doesn't bleed the light value. - Document the new field in `config.example.json` per AGENTS.md config rule. ## TDD Red commit `07a69e48` adds `test-issue-1509-nav-active-bg.js` and wires it into the CI unit-test step. Assertions fail on master (`THEME_CSS_MAP.navActiveBg` is `undefined`; `applyCSS` does not write the variable). Green commit `29d22ff5` makes the assertions pass without touching any other test. ## Verification - `node test-issue-1509-nav-active-bg.js` → 3/3 pass on this branch, 0/3 on master - `node test-customizer-v2.js` → 59/60 (the 1 failure is pre-existing on master, not caused by this PR — same failure with the diff stashed) - pr-preflight: clean (all gates pass) --------- Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com> Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> |
||
|
|
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
|
||
|
|
5fd8900cfc |
feat(packets): add Path symbols legend disclosure (closes #1504) (#1570)
## Summary Closes #1504. Adds a tiny, dismissible "Path symbols" legend next to the Path column header on the Packets page (and reused on the Nodes page's "Paths Through This Node" card), explaining the three otherwise-undiscoverable path glyphs: - `⚠N` — regional conflict count (multiple candidates for the hop's prefix in this region) - `⚠️` — unreliable name resolution (best-guess pubkey couldn't be confirmed) - dashed underline — ambiguous / global-fallback resolution ## Rationale (from triage) - **Tufte**: integrate words and graphics. A hidden per-row tooltip violates "don't make the viewer cross-reference." A small, persistent inline key next to the column header is dense, on-data, and dismissible. - **Avoid a modal** — chartjunk for a 3-glyph vocabulary. - **Munger** rejected the reporter's option #2 (hover overlay that pauses live updates): a power-user table must not stall from accidental hovers. - Single shared constant on `HopDisplay` so the Nodes page reuses the same vocabulary without drift. ## Files - `public/hop-display.js` — export `PATH_SYMBOLS_LEGEND` constant + `renderPathSymbolsLegend()` helper (no changes to existing badge rendering logic) - `public/packets.js` — wire renderer into the Path `<th>` header - `public/nodes.js` — reuse renderer on `#fullPathsSection` h4 - `public/style.css` — minimal styling (subtle dotted-underline trigger + floating disclosure panel, all via theme vars) - `test-frontend-helpers.js` — 5 new assertions (TDD red→green) ## TDD red → green - RED commit `46741267` — adds 5 assertion-shaped tests; all fail on the assertion (not on import/build). - GREEN commit `fab27ec5` — implements the constant, renderer, wiring, and CSS; all 607 frontend-helper tests pass. ## Tested via - DOM-grep assertions on the rendered `<details>` markup (`<summary>Path symbols</summary>`, all three glyphs present, dashed-underline description). - Static grep that `packets.js` invokes the shared renderer adjacent to the Path column. - Full `test-frontend-helpers.js`, `test-packet-filter.js`, `test-aging.js` pass. ## Hard rules honored - No modal, no pause-on-hover, no changes to `hop-display.js`'s badge rendering logic. - No `<img>`/SVG additions, no new CSS vars (uses existing theme vars), no Go changes. - PII grep clean on every commit and on this body. Browser verified: manual smoke pending — disclosure is closed-by-default and uses standard `<details>` semantics; renders inline with column header. E2E assertion added: `test-frontend-helpers.js` — `#1504: renderPathSymbolsLegend returns <details> disclosure with "Path symbols" summary + all glyphs` (and 4 sibling assertions). --------- Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> Co-authored-by: clawbot <bot@openclaw.local> |
||
|
|
2b45f7872c |
fix(live): corner-cycle button clears drag state (#1567) (#1568)
## Summary Fixes the move-panel corner-cycle button silently no-op'ing after a panel is dragged on `/live`. Two coexisting positioning systems were mutating disjoint state: - `public/drag-manager.js` sets inline `top/left/right/bottom/transform/position`, stamps `data-dragged="true"`, and persists `localStorage['panel-drag-<id>']`. - `public/live.js` `applyPanelPosition()` only flips the `data-position` attribute (selecting a `.live-overlay[data-position="…"]` rule with `top/left/right/bottom`). Inline styles win the cascade, so after any drag the corner button updated the glyph but the panel never moved. The fix has `onCornerClick` clear drag state (attribute, inline coords, localStorage) before calling `applyPanelPosition`. ## Commits - Red: `ea2f8009` — `test(live): failing E2E for corner-cycle button after drag (#1567)` — Playwright test injects DragManager-shaped drag state on `#liveFeed`, clicks `.panel-corner-btn`, asserts `data-dragged`/inline styles/`localStorage` are cleared AND `getBoundingClientRect()` matches the CSS corner anchor (not the dragged coords). Fails on master at the post-click assertion. - Green: `abb5a21f` — `fix(live): corner-cycle button clears drag state (#1567)` — 11-line change in `onCornerClick`, plus new E2E wired into the workflow. ## Files - `public/live.js` — `onCornerClick` clears `data-dragged`, inline `top/left/right/bottom/transform/position`, and `localStorage['panel-drag-<id>']` before `applyPanelPosition`. - `test-issue-1567-corner-clears-drag-e2e.js` — new Playwright E2E (drag-state injection + post-click rect assertion). - `.github/workflows/deploy.yml` — runs the new E2E next to `test-drag-manager-e2e.js`. ## E2E E2E assertion added: `test-issue-1567-corner-clears-drag-e2e.js:108` (post-click drag-state + anchor-match assertions). Browser verified: red-on-master gated by assertion (`'data-dragged must be cleared after corner click'`) — green commit makes it pass. ## Scope - No changes to `drag-manager.js` (out of scope per triage fix path). - No config / API surface changes. - Desktop drag path only; mobile / coarse-pointer path unchanged (drag is gated off there at `live.js:1941`, so the button was always the only repositioning affordance on touch — preserved). Partial fix for #1567 — addresses the corner-button-no-op symptom called out in triage; leaves the issue open for the user to verify in the browser and close. --------- Co-authored-by: Kpa-clawbot <bot@openclaw.local> Co-authored-by: mc-bot <bot@meshcore.local> |
||
|
|
a7ad2be142 |
fix(observers): show "Last updated" timestamp on aggregate header (closes #1562) (#1563)
Closes #1562. Follow-up to #1551 and #1552. ## Problem On CDN-fronted deployments (e.g. meshcore.meshat.se), the observers page header rendered totals computed entirely client-side from a possibly-stale `/api/observers` response. Operators saw e.g. `0 Online / 43 Stale / 37 Offline` while a cache-busted request returned `44 Online / 0 Stale / 36 Offline` — the aggregate row was the first thing they looked at to assess mesh health, so wrong numbers meant wrong actions. #1551 added `Cache-Control: no-store` on `/api/*` responses, but the client also has its own in-memory cache (`api(path, { ttl })`), and there was no UI signal at all that the rendered counts could be stale. ## Fix scope (Option 3 + light Option 2) Per the issue's three options, this PR implements **Option 3** (timestamp label) and a light **Option 2** (manual-refresh button bypasses client cache). Option 1 (a new server-side `/api/observers/summary` endpoint) is **deferred** as a follow-up — it's the most correct fix, but a bigger lift than what's needed to stop operators from acting on silently-wrong numbers. ## Changes - **`public/observers.js`** - New `window.ObserversSummary` pure helper exposing `computeCounts(observers)` and `renderHeader(counts, fetchedAt)`. Pure functions = easy to unit test. - Track `_fetchedAt` (ms) on each successful `loadObservers()` response. - `render()` delegates header HTML to `ObserversSummary.renderHeader(counts, fetchedAt)`. Existing aggregate display (`Online / Stale / Offline / Total`) is preserved exactly — the only visible additions are the "Last updated: Xs ago" label and a warning class when the timestamp is >60s old. - Manual refresh button now passes `{ bust: true }` to `api()` so the operator can force a fresh fetch when they suspect staleness. - **`public/style.css`** - New `.obs-updated` and `.obs-updated-stale` rules using existing `--text-muted` / `--warning` CSS variables (no new colors). - **`test-issue-1562-observers-summary.js`** + **`.github/workflows/deploy.yml`** - Unit tests for `computeCounts` (mixed ages → 1/1/1 + total), `renderHeader` (label presence + stale-warning class), plus DOM-grep checks that observers.js still tracks `_fetchedAt` and bypasses the cache on manual refresh. ## TDD Red commit asserts `ObserversSummary` doesn't exist / no `_fetchedAt` tracking / no `obs-updated-stale` CSS → fails. Green commit adds the implementation → passes. ## What this PR does NOT touch - **Observer health thresholds** — owned by #1552, untouched here. - **`healthStatus()` per-row classification** — untouched. The same function still gates per-row colors AND aggregate counts; the fix is about freshness visibility, not classification logic. - **No new server endpoint** — Option 1 deferred. Will file a follow-up if anyone wants that tracked. --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: mc-bot <bot@meshcore.local> |
||
|
|
d7cd9203ca |
Fixes #1165: add OSM/Stamen tile providers with per-provider Leaflet layer control. (#1533)
List of changes too long to describe, so I'll hit high level. - Config now supports the json map tiles that were suggested by @Kpa-clawbot. - Leaflet map layer button appears in the top right of live.js and map.js (because all the work was already done on live.js... Added bonus) - Allows users to enter creds for OSM and Stamen to get enterprise related perks, in the config file - Added a default light map under customizer. Still suggest removing them all together and relying on the config - You can enable OSM and Stamen in the config without a license, but at your own risk!!! - Config comment explains where to register and the providers for osm, as well as the general limits per X interval - Updated tests (28) to address the changes made to the maps ### TDD Exemption **Reason**: Net-new UI surfaces (per `AGENTS.md`) This PR introduces a net-new UI surface (the multi-provider map tile selector). Under the `AGENTS.md` exemption for net-new UI surfaces, the absence of an initial failing (red) commit is permitted, as the UI was built first. However, the underlying public APIs are fully covered. The following tests serve as the first assertions for these new APIs: - `window.MC_createLayerControl`: Asserted in `MC_createLayerControl handles Auto mode and explicit layers correctly` - `window.MC_setDarkTileProvider` & `window.MC_getDarkTileProvider`: Asserted in `MC_setDarkTileProvider persists to localStorage...` - `window.MC_setLightTileProvider` & `window.MC_getLightTileProvider`: Asserted in `MC_setLightTileProvider persists to localStorage...` - `window.MC_initTileRegistry`: Asserted in `MC_initTileRegistry(true) dispatches mc-tile-provider-changed` - `applyTileFilter`: Asserted in `applyTileFilter sets invert CSS for inverted dark provider...` - Cross-tab synchronization: Asserted in `Cross-tab storage event re-dispatches mc-tile-provider-changed` |