Commit Graph
2401 Commits
Author SHA1 Message Date
OpenClaw Bot 976ccf6db5 style(live): revert auto-format whitespace churn from #1490 (#1514 S1)
PR #1490 reintroduced cosmetic whitespace flips that commit baeb49dc was
supposed to clean up:

- function() <-> function () spacing
- slice(0,3) <-> slice(0, 3) and similar binary-operator spacing
- '0':0x7E <-> '0': 0x7E object-literal spacing
- {} <-> { } empty block

Net behavioral change: zero. 85 lines whitespace-only.

Refs #1514
2026-05-31 22:44:04 +00:00
Kpa-clawbot f142f0ebde ci: update go-server-coverage.json [skip ci] 2026-05-31 22:13:08 +00:00
Kpa-clawbot 1931953ccb ci: update go-ingestor-coverage.json [skip ci] 2026-05-31 22:13:07 +00:00
Kpa-clawbot 34a2f73854 ci: update frontend-tests.json [skip ci] 2026-05-31 22:13:06 +00:00
Kpa-clawbot 2253d6db34 ci: update frontend-coverage.json [skip ci] 2026-05-31 22:13:05 +00:00
Kpa-clawbot 40898f6cb2 ci: update e2e-tests.json [skip ci] 2026-05-31 22:13:04 +00:00
878d162b71 fix(live): persist nav-pin state across refresh (#1510) (#1515)
## What was broken

The nav-pin button state was not persisted across page loads. Every
refresh reset the nav to unpinned regardless of what the user had set,
forcing them to re-pin on every visit.

## What was added

- On init: reads `localStorage.getItem('live-nav-pinned')` and restores
the pinned state into `_navCleanup.pinned` before the button is created;
if pinned, the button gets the `pinned` class, `aria-pressed="true"`,
and `nav-autohide` is removed from the nav.
- On click: after toggling, writes
`localStorage.setItem('live-nav-pinned', _navCleanup.pinned)` inside a
`try/catch` (quota guard, consistent with other live.js localStorage
writes).

localStorage key: `live-nav-pinned`

Closes #1510

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:54:24 -07:00
3850600130 perf(server): TTL-cache /api/stats observations aggregate — eliminate per-request full-table scan (#1460) (#1516)
## Problem

`GetStoreStats` ran a `SUM(CASE WHEN timestamp > ?)` over the full
`observations` table on **every** `/api/stats` call. The staging pprof
analysis (#1460) identified this as rank #9 CPU consumer:
`GetStoreStats.func2` at 920ms cumulative = ~10% of all server CPU.

The query:
```sql
SELECT
  COALESCE(SUM(CASE WHEN timestamp > ? THEN 1 ELSE 0 END), 0),
  COALESCE(SUM(CASE WHEN timestamp > ? THEN 1 ELSE 0 END), 0)
FROM observations WHERE timestamp > ?
```
scans ~1.9M rows each time `/api/stats` is polled (every 15s from the
dashboard).

## Fix

Add a **30-second TTL cache** on `PacketStore` for `PacketsLastHour` and
`PacketsLast24h`:
- Cache hit → skip the observations goroutine entirely, use stored
values
- Cache miss → run the query, update cache with result
- The node/observer `COUNT(*)` query is unchanged and always runs fresh

The hour/24h counts are display-only values; 30s accuracy is sufficient.

## Changes

`cmd/server/store.go`:
- 4 new fields on `PacketStore`: `statsCacheMu sync.Mutex`,
`statsCacheTime time.Time`, `statsLastHour int`, `statsLast24h int`
- `GetStoreStats`: check cache before launching goroutines; conditional
`wg.Add`; update cache after successful query

Builds clean. No tests changed.

Closes #1460 (P1#1 from staging CPU profile).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:54:21 -07:00
Kpa-clawbot 5c2693465c ci: update go-server-coverage.json [skip ci] 2026-05-31 21:13:24 +00:00
Kpa-clawbot 6e46fb862a ci: update go-ingestor-coverage.json [skip ci] 2026-05-31 21:13:23 +00:00
Kpa-clawbot 8a9465b2c0 ci: update frontend-tests.json [skip ci] 2026-05-31 21:13:22 +00:00
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
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
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
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
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
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
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 MuehlsteinandGitHub 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