Commit Graph
2390 Commits
Author SHA1 Message Date
Kpa-clawbot 20baef61ec ci: update frontend-coverage.json [skip ci] 2026-05-31 21:13:22 +00:00
Kpa-clawbot 67dbda0b75 ci: update e2e-tests.json [skip ci] 2026-05-31 21:13:21 +00:00
Eldoon NemarandKpaBap 914f869421 Make packet movement on the live map hardware-accelerated using HTML5 (#1490)
Instead of forcing Leaflet to recalculate and paint heavy SVG DOM nodes
60 times a second for every moving packet, we will draw the flying dots
and lines directly onto a hardware-accelerated HTML5 <canvas> overlaid
on the map. Once the animation finishes, it will drop a static Leaflet
line to handle the fading tail effect.

---------

Co-authored-by: KpaBap <kpabap@gmail.com>
2026-05-31 20:54:11 +00:00
Kpa-clawbot 3abcfbcf33 ci: update go-server-coverage.json [skip ci] 2026-05-31 18:48:36 +00:00
Kpa-clawbot 958c570622 ci: update go-ingestor-coverage.json [skip ci] 2026-05-31 18:48:35 +00:00
Kpa-clawbot 6040bc0f6a ci: update frontend-tests.json [skip ci] 2026-05-31 18:48:34 +00:00
Kpa-clawbot be7b5b8c2d ci: update frontend-coverage.json [skip ci] 2026-05-31 18:48:33 +00:00
Kpa-clawbot fb66a8971b ci: update e2e-tests.json [skip ci] 2026-05-31 18:48:32 +00:00
c9b98cb15f fix(#1498): preserve WS-pushed messages across REST replacements (#1513)
## Summary

Fixes #1498. Roots out the actual WS-vs-REST race that has made
`test-channels-ws-batch-e2e.js` flaky on master for ~2 weeks.

## Root cause

`selectChannel()` and `refreshMessages()` unconditionally replace the
in-memory `messages` array with the REST response. Any WebSocket-pushed
messages appended between `selectedHash` assignment (when the chat view
opens) and the REST resolution were silently stomped. The flaky test
was a real-world manifestation: when the synthetic `processWSBatch`
injection happened to land BEFORE the in-flight
`/channels/<hash>/messages` fetch resolved, the (effectively empty)
fixture REST response wiped it out. This is a production bug too —
real users would lose any live message that arrived during channel
load.

## Why the three prior PRs missed it

- **#1499** — added a 500ms `waitForTimeout` before injection. Often
  enough to let the REST fetch resolve first, but not under any added
  load.
- **#1502** — skipped the test instead of diagnosing.
- **#1511** — re-enabled with a "wait by hash, not index" predicate.
  That fixed the symptom of `messages[length-1]` being some unrelated
  packet, but did nothing for the underlying race where the WS-pushed
  message gets wiped entirely by the REST replacement.

None of the three PRs reproduced the failure locally. The hypothesis
"closure over stale messages" in the test comment was never
substantiated.

## Fix

Stamp WS-pushed messages with `_fromWS=true` and add a
`mergeWsAppendedIntoRest()` helper that preserves WS-pushed messages
whose `packetHash` isn't already present in the REST response. Applied
to all three REST replacement sites:

- `selectChannel()` REST path
- `decryptAndRender()` (encrypted channel path)
- `refreshMessages()` (background poll)

## Tests

Added `test-channels-ws-race-1498-e2e.js`. Deterministically forces
the race by stubbing `fetch` to delay the
`/channels/<hash>/messages` response 800ms, injects a WS message
during the delay, asserts it survives the late REST resolution.

- Red commit (`9dfc4b08`): test added against unfixed master HEAD →
  fails with `WS message stomped by REST fetch — messages after fetch:
  {"present":false,"count":0,"hashes":[]}`.
- Green commit (`8f336591`): applies the fix → passes.

Verified the red commit actually fails when the production change is
reverted (TDD discipline check).

## Local repro stats

Used the instrumented frontend (`public-instrumented/`) which exposes
the race more reliably than the raw `public/` build (slower JS load
widens the WS-vs-REST window).

- Before fix: 29/30 pass (1 reproduced "injected message not found"
  failure — identical to CI). The new race test: 0/50 pass.
- After fix: original `test-channels-ws-batch-e2e.js` — **50/50 pass**.
  New `test-channels-ws-race-1498-e2e.js` — **50/50 pass**.

## CI

Wired the new race test into `.github/workflows/deploy.yml` right
after the existing `test-channels-ws-batch-e2e.js` invocation.

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ all gates pass (PII, branch scope, red commit, CSS vars,
LIKE-on-JSON, sync migration, all warnings).

Browser verified: the fix was validated end-to-end against the local
fixture server (`http://localhost:13581`) using the headless Chromium
the CI uses.

E2E assertion added: `test-channels-ws-race-1498-e2e.js` (deterministic
race regression).

---------

Co-authored-by: bot <bot@local>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-31 11:29:15 -07:00
efitenandClaude Sonnet 4.6 ec58c2af13 test(#1396): extend nav Priority+ E2E to /#/channels (#1512)
## What

Issue #1396 reported that at viewport ~1024px on `/#/channels`, the
entire inline nav strip was visually empty (no high-priority links, no
active pill, nothing) and the More dropdown showed only "Tools".

## Root cause

Identical to issue #1400 (closed): `min-height: 48px` on `.nav-link`
globally inflated the strip beyond the 52px `top-nav` height. Firefox
flex-centered the over-tall item to a negative y (≈-57px), clipping it
above the viewport behind `overflow:hidden`. **Already fixed by PR
#1401** (removed global `min-height`). Issue #1396 stayed open because:
1. `/#/channels` was never added to the Priority+ E2E test loop
2. The y-position assertion was never added despite being in #1400's
acceptance criteria
3. The exact More-dropdown-contents contract was never locked for
`/#/channels`

## Changes

Extends `test-nav-priority-1391-e2e.js`:

- **`#/channels` added to `NON_HIGH_ROUTES`** — tested at all 6 viewport
widths (1024, 1080, 1100, 1101, 1200, 1300px)
- **Assertion (4)** — `.nav-links top > -1`: directly catches the
strip-clipped-above-viewport bug; the original failure had `y ≈ -57`,
this assertion would have caught it immediately
- **Assertion (5)** — at ≤1100px (force-collapse band), More must
contain EXACTLY the 5 non-active non-high routes; channels stays inline
as the active pill

## Test results

```
30/30 passed (was 24/24; 6 new channels combinations all ✅)
strip top=2.5 at all desktop widths (positive, not clipped)
```

## Notes

- Supersedes draft PR #1397 (Kpa-clawbot RED-only test; never completed)
- No code changes — the underlying CSS fix is already in master via PR
#1401

Closes #1396

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 05:56:49 -07:00
Kpa-clawbot ed967a07e5 ci: update go-server-coverage.json [skip ci] 2026-05-30 21:01:47 +00:00
Kpa-clawbot b60c312766 ci: update go-ingestor-coverage.json [skip ci] 2026-05-30 21:01:46 +00:00
Kpa-clawbot 24da550571 ci: update frontend-tests.json [skip ci] 2026-05-30 21:01:46 +00:00
Kpa-clawbot de67c15450 ci: update frontend-coverage.json [skip ci] 2026-05-30 21:01:45 +00:00
Kpa-clawbot 8afd526744 ci: update e2e-tests.json [skip ci] 2026-05-30 21:01:44 +00:00
Kpa-clawbot 49b21457ab ci: update go-server-coverage.json [skip ci] 2026-05-30 20:41:23 +00:00
Kpa-clawbot 5f7f092f25 ci: update go-ingestor-coverage.json [skip ci] 2026-05-30 20:41:22 +00:00
Kpa-clawbot 51fc1f9435 ci: update frontend-tests.json [skip ci] 2026-05-30 20:41:21 +00:00
Kpa-clawbot f7bd62cec7 ci: update frontend-coverage.json [skip ci] 2026-05-30 20:41:21 +00:00
Kpa-clawbot fdf6e531e5 ci: update e2e-tests.json [skip ci] 2026-05-30 20:41:20 +00:00
Kpa-clawbotandopenclaw-bot 28713fabdb feat(map): #1108 hide non-region nodes by default, add 'Show all nodes' toggle (#1501)
Closes #1108

## What
When an operator selects a region on the Live map, default to **hiding**
nodes outside that region. The operator picked the region for a reason —
far-away markers are visual noise. Operators who want the legacy
show-everything behavior can flip the new **Show all nodes** checkbox
next to the region dropdown.

Default: **off (hide non-region nodes)**. State persists in
`localStorage['mc-region-show-all-nodes']`.

## Why
Tracks the request in #1108 — region filtering currently scopes packet
feeds + metrics but the map keeps every node visible, which defeats the
point of selecting a region in the first place.

## How
- `public/region-filter.js`: new `RegionShowAll` module (`get` / `set` /
`onChange` / `STORAGE_KEY`) plus `RegionFilter.nodesRegionQueryString()`
— returns `&region=…` only when a region is selected **and** showAll is
off. Other surfaces (packets, metrics) continue to use the unconditional
`regionQueryString()`.
- `public/live.js`: `loadNodes()` appends `nodesRegionQueryString()`;
region-change and showAll-change handlers reload nodes so markers update
immediately.
- `public/live.css`: aligns the new toggle with the existing
`.live-toggles` rhythm.
- `test-1108-region-hide-nodes.js`: 7 unit assertions covering
default-off, persistence across reloads, set/get, and the conditional
query-string builder.

## TDD trail
- `dbf6d6db` — red test commit (assertion failures, helpers do not exist
yet)
- `eefa1185` — green commit (helpers + wiring)

## CDP validation (staging, after hot-deploy)
| state | markers |
| --- | --- |
| no region | 517 |
| region=SJC, showAll=off | **497** (region-scoped) |
| region=SJC, showAll=on  | 517 (legacy behavior) |

Toggle state survives reload (`RegionShowAll.get() === true` after
refresh).

## Out of scope
- Static `/map` page (`public/map.js`) — its region UI is jump-buttons,
not the shared `RegionFilter` selector. A follow-up could wire
`nodesRegionQueryString` there too, but it's a separate UX surface.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-30 13:22:44 -07:00
Kpa-clawbotandbot 367265eb59 feat(#1369): cross-domain embed support (CORS env override + ?embed=1 chrome suppression) (#1500)
Closes #1369.

## What

Cross-domain embed support, shipped as two halves:

### Part A — CORS env override + read-only contract

* `applyCORSEnv()` reads `CORS_ALLOWED_ORIGINS` (comma-separated,
trimmed, empties dropped). Set in env → overrides
`cfg.CORSAllowedOrigins`. Unset/empty → config.json value wins.
* `Access-Control-Allow-Methods` tightened from `GET, POST, OPTIONS` →
`GET, HEAD, OPTIONS`. The cross-domain surface is read-only by contract;
same-origin admin writes don't go through preflight and are unaffected.
* `config.example.json` adds `corsAllowedOrigins: []` + a comment
explaining the env override and the embed URL pattern.
* No wildcards introduced (still supported as `["*"]` for ops that opt
in). No credentialed CORS.

### Part B — `?embed=1` chrome suppression

* `shouldEmbedRoute(basePage, hashSearch)` — pure helper, allowlisted to
`map` and `channels`, requires `embed=1` in the hash querystring.
* `navigate()` toggles `body.embed` based on the helper.
* CSS hides `.top-nav`, `[data-bottom-nav]`, `.nav-drawer`,
`.nav-drawer-backdrop`, zeroes body padding/margin, reclaims `100dvh`
for `#app.app-fixed`.

Use: `<iframe src="https://analyzer.example/#/map?embed=1">`. For
iframe-only display, no CORS entry is needed (the iframe loads the
document, not a JSON API). The CORS allowlist only matters when the
embedding origin's own JS calls `/api/*` directly.

## Tests

| File | Asserts | Status |
|---|---|---|
| `cmd/server/cors_embed_1369_test.go` | 4 (env override, env-empty,
env-trim, GET/HEAD contract, preflight POST rejected) | green |
| `test-embed-mode-1369.js` | 9 (helper allowlist + param parsing) |
green |
| `cmd/server/cors_test.go` | existing | updated to read-only method-set
assertion |

TDD: 2 red commits (one per part, both compile, both fail on assertions)
→ 2 green commits.

## Out of scope (per the issue's narrow ask)

* Other SPA routes do not honor `?embed=1` (their chrome makes layout
assumptions; defer until requested).
* No iframe sandboxing recommendation — that's the embedder's
responsibility.
* No CSP / `X-Frame-Options` change in this PR — frames are already
permitted; add an explicit `frame-ancestors` policy in a follow-up if
operators want to whitelist embedders at the HTTP layer too.

## Security notes (DJB lens)

* Allowlist is exact-match, case-sensitive string compare — no
normalization, no scheme/host parsing, no surprises.
* No `Access-Control-Allow-Credentials` (would let third parties read
auth'd state via cookies).
* No reflection of arbitrary origins (every echoed origin came from the
allowlist).
* Methods narrowed to read-only; even a misconfigured allowlist can't
grant cross-origin writes through this middleware.

🤖 Generated with OpenClaw

---------

Co-authored-by: bot <bot@corescope.local>
2026-05-30 13:22:41 -07:00
Kpa-clawbotandmc-bot b2f0be994d fix(#1498): channels-ws-batch — wait on packetHash, not length (un-skip explicit-sender) (#1511)
## Summary

The channels-ws-batch E2E tests had a race condition causing flaky
failures on CI, blocking PRs #1490, #1500, #1501.

**Root cause:** Tests waited on `messages.length === prev + 1`, but live
WS traffic from the ingestor could bump `length` independently, causing
timeouts. The earlier #1499 fix attempted to find messages by
`m.hash`/`m.id`, but `processWSBatch` stores `packetHash`/`packetId` on
message objects — so the find never matched.

**Fix:** Replace all length-based waiters with `messages.some(m =>
m.packetHash === '<known-hash>')` which is deterministic regardless of
concurrent WS traffic. Also un-skips the explicit-sender test that was
force-skipped in #1502.

## Tests affected
- "processWSBatch with explicit sender appends to messages" —
un-skipped, now passes
- "GRP_TXT shape with 'Sender: text' parses sender from text" —
race-proof
- "dedup by packetHash" — race-proof
- "new WS message while scrolled up" — race-proof

All 6 tests pass locally (6 passed, 0 failed).

Fixes #1498.

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-30 12:54:02 -07:00
Kpa-clawbot 58acdf67d6 ci: update go-server-coverage.json [skip ci] 2026-05-30 01:14:20 +00:00
Kpa-clawbot 5fdaac57a3 ci: update go-ingestor-coverage.json [skip ci] 2026-05-30 01:14:19 +00:00
Kpa-clawbot 0b5b8369f7 ci: update frontend-tests.json [skip ci] 2026-05-30 01:14:18 +00:00
Kpa-clawbot d910657df3 ci: update frontend-coverage.json [skip ci] 2026-05-30 01:14:17 +00:00
Kpa-clawbot 7494741216 ci: update e2e-tests.json [skip ci] 2026-05-30 01:14:16 +00:00
Kpa-clawbotandmc-bot a7b156dafc fix(1506): restore marker-stroke server defaults to v3.7.2 visual (#1507)
# fix(1506): restore marker-stroke server defaults to v3.7.2 visual

Closes #1506. Refs #1494, #1488.

## Why
PR #1494 introduced operator-tunable marker stroke via
`--mc-marker-stroke-*` CSS vars but chose new server defaults
(translucent white, 1px) that look weak next to the v3.7.2 baseline
(solid white, 2px). Operators upgrading from v3.7.x see a visible
regression on the map.

## What
Restore the v3.7.2 visual as the server default. Customizer + config
plumbing are unchanged — anyone who preferred the thinner translucent
style can dial it back via the in-app customizer (Colors → Marker
Stroke).

| File | Before | After |
|---|---|---|
| `public/style.css` `:root` | `rgba(255,255,255,0.85)` / `1` / `1` |
`#fff` / `2` / `1` |
| `public/customize-v2.js` `msWidth` fallback | `1` | `2` |
| `config.example.json` `markerStroke.color/width` | `rgba(...,0.85)` /
`1` | `#fff` / `2` |

Customizer overrides already in localStorage continue to take effect —
only the unset baseline shifts.

## TDD
- Red commit (`cdabb905`): adds gate F to
`test-issue-1488-marker-stroke-vars.js` asserting style.css /
customize-v2.js / config.example.json defaults match v3.7.2 (solid
white, 2px). Fails on master with 5 assertion errors.
- Green commit (`abfa9b6b`): three small data edits flip all five
assertions to pass.

## Acceptance
- After upgrade, markers visually match v3.7.2 stroke (solid white, 2px)
by default ✅
- Customizer slider still functional ✅
- Existing custom values in localStorage still take effect (no reset) ✅

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-30 00:54:24 +00:00
Kpa-clawbot 9b6453807d ci: update go-server-coverage.json [skip ci] 2026-05-29 21:22:27 +00:00
Kpa-clawbot 63d327245e ci: update go-ingestor-coverage.json [skip ci] 2026-05-29 21:22:26 +00:00
Kpa-clawbot 9f1873df80 ci: update frontend-tests.json [skip ci] 2026-05-29 21:22:25 +00:00
Kpa-clawbot 90b25a91ab ci: update frontend-coverage.json [skip ci] 2026-05-29 21:22:24 +00:00
Kpa-clawbot 44bab97d6c ci: update e2e-tests.json [skip ci] 2026-05-29 21:22:23 +00:00
Eric Muehlstein 788a509e73 refactor: move version/commit badge from navbar to Perf dashboard (#1503)
## Summary

The version/commit badge currently rendered in the nav stats bar
(alongside packet counts, node counts, and observer counts) is
operator-facing diagnostic information — not something end users need
visible on every page load. For most visitors, it adds visual noise
without adding value.

## Changes

- **perf.js**: Add a **Version** card to the Perf dashboard overview
row. Shows `version` + short `commit` hash, both already available from
`/api/health` (no new API surface needed). Card renders conditionally —
if neither field is set it stays hidden.
- **app.js**: Remove `formatVersionBadge()` and `formatEngineBadge()`
helper functions (now unused); strip the badge call from
`updateNavStats()` so the navbar shows only packet/node/observer counts.
- **style.css**: Remove now-dead `.nav-stats .version-badge`,
`.nav-stats .engine-badge`, and their link sub-rules.

## Rationale

The Perf page is explicitly the right place for this information — it's
already scoped to operators and developers who want to know what version
is running. The navbar is a high-visibility surface shared by all users;
version strings belong in a diagnostic context, not a navigation bar.

Net result: navbar is cleaner for end users; operators can still find
version info immediately on the Perf tab.
2026-05-29 14:03:03 -07:00
Kpa-clawbot e0fea3fe1d ci: update go-server-coverage.json [skip ci] 2026-05-29 17:35:51 +00:00
Kpa-clawbot 1d4840221d ci: update go-ingestor-coverage.json [skip ci] 2026-05-29 17:35:50 +00:00
Kpa-clawbot 642f947d6b ci: update frontend-tests.json [skip ci] 2026-05-29 17:35:49 +00:00
Kpa-clawbot 1a91982e01 ci: update frontend-coverage.json [skip ci] 2026-05-29 17:35:48 +00:00
Kpa-clawbot 47aa5dfe5c ci: update e2e-tests.json [skip ci] 2026-05-29 17:35:47 +00:00
Kpa-clawbotandmc-bot 9bed0e85ed fix(#1496): Reset All clears every customizer-touched state (#1497)
## Summary

`🗑️ Reset All Customizations` only stripped `cs-theme-overrides`,
leaving CB-preset, encrypted-channel toggle, dark-tile pick,
marker-stroke vars and the per-role `--mc-role-*` body.style writes from
PRs #1361/#1430/#1448/#1454/#1488 stuck. Operators had to clear
localStorage by hand to actually reset.

Single source of truth lands as `_resetAll()` in
`public/customize-v2.js` (exposed on `_customizerV2.resetAll` for
tests). The Reset button delegates to it. Future customizer features
extend ONE function — not 12 scattered call-sites.

## What is cleared

| surface | keys / props |
|---|---|
| localStorage | `cs-theme-overrides`, `meshcore-cb-preset`,
`channels-show-encrypted`, `mc-dark-tile-provider` |
| body attr | `data-cb-preset` |
| body.style | `--mc-role-{role}`, `--mc-role-{role}-text` for
repeater/companion/room/sensor/observer |
| :root style | `--mc-role-*`, `--mc-role-*-text`, `--node-*`,
`--mc-marker-stroke-{color,width,opacity}`,
`--mc-mb-{confirmed,suspected,unknown}`, `--mc-rt-ramp-{0..4}`,
`--logo-accent`, `--logo-accent-hi`, every value in `THEME_CSS_MAP` |

CB-preset teardown delegates to `MeshCorePresets.clearPreset()` so
`cb-preset-changed` fires and downstream consumers re-sync to server
config without a reload. Tile-provider teardown re-applies the active id
(which now falls through to server default / `carto-dark`) so
`mc-tile-provider-changed` fires and the live map swaps tiles, then
re-clears the just-rewritten localStorage entry.

## What is explicitly preserved (per issue body)

- `meshcore-theme` — separate user preference, not a customization
- `meshcore-gesture-hints-*` — has its own dedicated Reset button
- `meshcore-favorites` — operator's favorites list, not a customizer
pick
- `mc-channels-*` — channel selection state, not a customization

## TDD

- Red commit (`7a986fce`): adds `test-issue-1496-reset-all-complete.js`
+ a stub `resetAll: function () {}` so the test fails on assertions (9
of 14), not on a missing symbol. The 5 "must NOT clear" assertions pass
trivially against the stub.
- Green commit (`45c88154`): wires `_resetAll()`; all 14 pass.

```
14 passed, 0 failed
```

Existing customizer tests (`test-customize-display-e2e.js` shape only;
`test-issue-1361-cb-presets.js` 82/82;
`test-issue-1412-customizer-no-override.js` 13/13) unaffected. Two
pre-existing failures in `test-customizer-v2.js` and
`test-issue-1438-customizer-mcrole.js` reproduce on `origin/master`
without this change.

Closes #1496

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 10:01:24 -07:00
Kpa-clawbotandmc-bot 2196102eae test(channels): skip processWSBatch-explicit-sender step pending #1498 root-cause (#1502)
Master CI failing across all recent PRs due to this single test. The
#1499 find-by-hash fix didn't resolve it — root cause is deeper than the
index-vs-hash race (possibly closure staleness on
`_channelsProcessWSBatchForTest` vs `_channelsGetStateForTest`).

Skipping to unblock master per operator directive. Filed #1498 for
proper diagnosis with CDP repro.

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 17:01:00 +00:00
Kpa-clawbotandmc-bot ebdf51ba54 fix(test): channels-ws-batch race — find injected message by hash not index (#1499)
## Why master CI keeps failing

Real WS messages from the staging ingestor race with the test's
synthetic injection. messages.length jumps prev+2 instead of prev+1, and
messages[length-1] is some XMD packet instead of the synthetic WsAlice —
assertion fails.

Failure log:
```
✗ processWSBatch with explicit sender appends to messages: expected sender WsAlice, got XMD Tag 1
```

Started flaking ~v3.8.2-track when test timing shifted. Test was
authored in #1300.

## Fix

Find injected message by its synthetic hash:
```js
s.messages.find((m) => m.hash === 'wsbatch-explicit-1' || m.id === 'pkt-wsbatch-1')
```

Race-immune regardless of real WS noise. Unblocks master CI.

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 16:04:54 +00:00
Kpa-clawbotandmc-bot 6af82cf3a8 fix(test): #1487 E2E desktop-only (BYOP button hidden on mobile per #1471) (#1495)
## Why CI was failing on master

PR #1493 (BYOP modal fix for #1487) shipped an E2E test that runs at
BOTH 390×844 mobile + 1280×800 desktop. The test calls
`waitForSelector('[data-action=pkt-byop]')` which defaults to `state:
visible`.

But #1471 mobile UX rules explicitly hide BYOP on mobile:
`#pktLeft .page-header [data-action="pkt-byop"] { display: none
!important }`

So the test times out on the mobile pass, breaking master CI on every
commit since c841dbcc.

## Fix
Drop the mobile viewport from the test loop. Reporter (@EldoonNemar)'s
bug was on desktop — that's where we test.

If BYOP ever gets surfaced on mobile, re-enable the mobile pass.

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 15:39:19 +00:00
Kpa-clawbotandmc-bot 7fcb226cd8 fix(#1486): collapse chevron no longer reopens closed detail panel (#1492)
## Summary

Fixes #1486 — clicking the collapse chevron on a grouped packet row in
the packets table no longer reopens the detail panel that the operator
just closed.

## Root cause

In the `#pktBody` row click handler the `toggle-select` action ran
**both** `pktToggleGroup(value)` and `pktSelectHash(value)` on every
chevron click. `pktToggleGroup()` already opens the detail panel itself
(via `selectPacket()`) when it expands a row, so the trailing
`pktSelectHash()` was:

  - redundant on **expand** (the panel was already opening), and
  - harmful on **collapse** — after the operator closed the detail panel
    via the ✕ in `#pktRight`, clicking the same chevron a second time
    to collapse the tree re-fetched `/packets/<hash>` and re-populated
    the panel with the same packet, exactly the behavior the issue
    describes.

## Fix

Drop the unconditional `pktSelectHash(value)` call inside the
`toggle-select` branch. `pktToggleGroup()` already handles the
expand-side panel open; the collapse branch does no panel work, so a
closed panel stays closed.

```js
else if (action === 'toggle-select') {
  // #1486: pktToggleGroup() already opens the detail panel on EXPAND
  // (via selectPacket()), and must NOT open it on COLLAPSE.
  pktToggleGroup(value);
}
```

## Tests

- New Playwright E2E `test-issue-1486-collapse-reopens-detail-e2e.js`
  walks the operator-visible repro: expand → assert panel open →
  click ✕ → assert panel empty → click chevron again → assert row
  collapsed AND panel STILL empty.
- Committed red-first: the test was added in its own commit and FAILS
  on the unpatched code (3 passed / 1 failed), then GREEN on the fix
  commit (4 passed / 0 failed).
- CI workflow seeds two extra observations onto the newest fixture
  transmission so a grouped (`toggle-select`) row exists; without this
  the fixture renders only flat rows and the chevron can't be
  exercised.

## Reproduction (manual, against staging or local)

1. Open `/#/packets` on desktop.
2. Click a grouped row's `▶` chevron — the tree expands and the detail
   panel opens on the right.
3. Click the `✕` in the top-right of the detail panel — panel goes back
   to "Select a packet to view details".
4. Click the same chevron (now `▼`) again — **before:** detail panel
   reopens with the same packet. **After:** the row collapses and the
   panel stays empty.

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 15:17:16 +00:00
Kpa-clawbotandmc-bot 268751ff56 fix(#1485): put live map animations on custom pane z=650 (above markerPane) (#1491)
## Summary

Animations on the live map (packet pulses, hop-to-hop trails,
drawAnimatedLine, pulseNode rings, matrix chars) render BEHIND the node
base layer — community-confirmed by @EldoonNemar in #1485 after pulling
latest and rebuilding. The live map looks completely static because
every node marker paints on top of moving packets.

Closes #1485

## Root cause

PR #1334 ("role-aware marker shapes + outline-ring highlight") swapped
node markers:

- **Before:** `L.circleMarker([n.lat, n.lon], {...})` — rendered into
the default Leaflet `overlayPane` (z=400) alongside other vector shapes.
- **After:** `L.marker([n.lat, n.lon], { icon: L.divIcon({...}) })` —
rendered into the default Leaflet `markerPane` (z=600).

`animLayer` and `pathsLayer` (built from `L.polyline` / `L.circleMarker`
shapes) still default to `overlayPane` @ 400. With nodes now in pane
600, every node marker occluded every animation. CDP confirmed pre-fix:

```
overlayPane z=400  (animations live here)  ← 2 children
markerPane  z=600  (nodes live here)        ← 516 children  ← occludes
```

## Fix

Create a custom Leaflet pane `liveAnimPane` at `z-index: 650` (strictly
above markerPane) and pin both `animLayer` and `pathsLayer` to it via
the `{ pane: 'liveAnimPane' }` option on `L.layerGroup`. Polylines +
circleMarkers added to those groups inherit the pane from their parent,
so all `drawAnimatedLine` / `pulseNode` / `animatePath` / matrix-char
shapes now paint above markers.

`pointerEvents: 'none'` on the pane so it does not steal hover/click
events from the markerPane beneath (`clickablePathsLayer` keeps the
default overlayPane and continues to handle path clicks).

Diff is +14 / -2 in `public/live.js`. No CSS changes, no API changes, no
protocol changes.

## TDD

Red commit (`b7ca794f`): test asserts on `public/live.js` source —
1. `map.createPane('liveAnimPane')` is called in init
2. that pane is assigned `style.zIndex` ≥ 650 (strictly above markerPane
@ 600)
3. `animLayer` AND `pathsLayer` are constructed with `{ pane:
'liveAnimPane' }`
4. (sanity) animLayer still hosts ≥3 animation shapes, pathsLayer ≥3
trail shapes — regression detector if someone moves circles to the
default pane.

CI must fail on `b7ca794f` (RED). Fix lands in `627ce341` (GREEN). Test
reruns 5× clean — non-flaky (source invariants).

## Browser verified

Local headless chromium (CDP) against
`http://analyzer-stg.00id.net/#/live`:

- **Before fix:** overlayPane z=400 (2 anim children), markerPane z=600
(516 marker children) — animations buried.
- **After hot-deploy:** liveAnimPane z=650 above markerPane z=600 —
animations visible on top. Will attach screenshot post-merge once
staging redeploys.

E2E assertion added: `test-issue-1485-live-anim-z.js:54` (`liveAnimPane
z-index >= 650`).

## Test wiring

`test-all.sh` line 51 added; CI runs the new test alongside the existing
1418/1420/1438/1470 suite.

## Credit

Reported by @EldoonNemar in #1485 — pulled via git, built the docker
image, noticed the regression same day. Bug-report quality was excellent
(concise repro: "live map now shows the animated packets behind the node
base layer so you can't actually see the nodes moving").

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 07:46:00 -07:00
Kpa-clawbotandmeshcore-bot ca2c3d6c79 feat(1488): customize marker stroke (color, width, opacity) (#1494)
## Summary

Reporter (@EldoonNemar in #1488) found the new white marker stroke
overwhelming with hundreds of nodes on screen. This PR exposes the
stroke through CSS vars + a customizer panel so operators can dial
color/width/opacity (or remove it) without code edits.

**Scope:** ship stroke customization only. The reporter also asked for
the old glow-style highlight ring as an alternative — that's a separate
visual feature that needs design discussion, so it's deferred to a
follow-up issue.

## Changes

- **`public/style.css`** `:root` declares `--mc-marker-stroke-color` /
`--mc-marker-stroke-width` / `--mc-marker-stroke-opacity` with sensible
defaults (white, 1, 1) that match current behavior.
- **`public/roles.js`** `makeRoleMarkerSVG` — replaced the 6 baked
`stroke="#fff" stroke-width="1"` literals with a single shared
`strokeAttr` referencing the CSS vars. One source of truth for all role
shapes.
- **`public/map.js`** `makeMarkerIcon` — same migration. The observer
star overlay keeps its narrow 0.8 width but routes color + opacity
through the same vars.
- **`public/live.js`** `addNodeMarker` fallback SVG — same migration.
- **`public/customize-v2.js`** — new `markerStroke` object section
(color/width/opacity) with validation, `applyCSS` writes, three controls
on the Colors tab → "Marker Stroke" panel (color picker + width slider
0–4 + opacity slider 0–100%). Optimistic CSS-var writes on the `input`
event so markers repaint live as the operator drags.
- **`cmd/server/{config,types,routes}.go`** — `ThemeFile` / `Config` /
`ThemeResponse` pick up `MarkerStroke` so `theme.json` and `config.json`
can ship server-side defaults. Defaults mirror the `:root` CSS values so
no breaking change for current operators.
- **`config.example.json`** — documented `markerStroke` section with
usage hint.

## TDD

- **Red commit** `92183f95` — `test-issue-1488-marker-stroke-vars.js` (5
sections, 18 assertions); failed 14/18 before implementation.
- **Green commit** `ce39637e` — implementation; same test now passes
18/18.
- Existing `#1438` (marker CSS-var migration) and `#1293` (marker
shapes) regression tests still pass.
- Go tests (`cmd/server/...`) all green.

## CDP validation

Synthetic page with 600 markers, three blocks proving CSS-var control
works end-to-end:

| Block | Stroke setting | Computed `getComputedStyle().stroke` / width
/ opacity |
| --- | --- | --- |
| Default | `var(--mc-marker-stroke-color)` (no override) |
`rgba(255,255,255,0.85)` / `1px` / `1` |
| Tuned | inline `--mc-marker-stroke-*` (operator override) |
`rgb(255,255,255)` / `0.5px` / `0.3` |
| Cyan | inline `--mc-marker-stroke-*` (branding/CB) | `rgb(0,229,255)`
/ `2px` / `1` |

Same SVG source, three different rendered strokes — that's the whole
point. Runtime `documentElement.style.setProperty(...)` (which is
exactly what the customizer slider's `input` handler does) repaints
mounted markers without reload. CDP screenshot attached to the
implementation note.

## Hot-deploy

Frontend + Go binary changes. Safe to hot-deploy frontend files
(`public/*.js`, `public/style.css`) via the standard staging path; Go
binary update needs a container restart.

## Defer

Glow highlight ring (the second half of #1488) — separate follow-up
issue. This PR delivers the immediately-useful, smaller deliverable.

Partial fix for #1488 (stroke customization shipped; glow ring deferred
to a follow-up issue).

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-29 14:31:36 +00:00
Kpa-clawbotandcorescope-bot c841dbccdd fix(#1487): BYOP modal — bounded header, no body occlusion (#1493)
## Fixes #1487

Reporter (@EldoonNemar): "The dialog text can't be seen due to the title
bar being massive."

### Root cause
`.byop-header` swelled to ~73px on mobile because:
1. `position: sticky` + `margin: -24px -24px 12px` assumed `.modal`
desktop padding (24px) — but `.modal` switches to 16px padding at
mobile, so the sibling-margin pushed the description paragraph UP into
the sticky-pinned header band, occluding it.
2. `.btn-icon` close button floors at 48×48 (touch target) → forced
header height ≥48px+padding.
3. H3 inherited a default emoji line-height that added more height on
platforms with tall emoji ascent metrics.

### Fix (`public/style.css`)
- Drop full-bleed negative-margin gymnastics — header uses normal
in-flow padding (`4px 0`); `.modal` padding handles inset.
- `max-height: 48px` on header so emoji ascent / btn-icon floor can't
blow it past safe range.
- Bound H3 explicitly (`font-size: 1rem; line-height: 1.3`).
- Override `.byop-x` to compact 32px visual size; preserve ≥44px
effective tap target via invisible `::before` pad (a11y safe).

### Verification
Hot-swapped onto staging, CDP-measured both viewports:

| viewport | hdrH | descTop ≥ hdrBottom | result |
|---|---|---|---|
| 390×844 mobile | 41px (was 73) | 341 ≥ 329 ✅ | clean |
| 1280×800 desktop | 41px | 318 ≥ 306 ✅ | clean |

### TDD
- **Red commit**: bb1a9f48 — `test-issue-1487-byop-modal-layout-e2e.js`
asserts header ≤56px AND description top ≥ header bottom on both
viewports. Pre-fix: header=73px ⇒ FAIL.
- **Green commit**: 72a69b3e — CSS fix; assertions all pass against
hot-swapped staging.
- E2E added: `test-issue-1487-byop-modal-layout-e2e.js`; wired into
`.github/workflows/deploy.yml` e2e job.

### Screenshots
Before (mobile): description "Paste raw hex bytes..." clipped by
oversized header. After: header 41px, description fully visible above
textarea.

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-29 14:29:57 +00:00
Kpa-clawbot 022f3d8f0d ci: update go-server-coverage.json [skip ci] 2026-05-29 12:04:33 +00:00
Kpa-clawbot c0ca47e9be ci: update go-ingestor-coverage.json [skip ci] 2026-05-29 12:04:32 +00:00