Commit Graph
255 Commits
Author SHA1 Message Date
dborup 31f3e77bf6 fix: show resolved scope name directly in the packets-tab badge label
The transport badge previously only surfaced a known scope in the
title tooltip, requiring a hover. Now the label itself reads
"T·#region" so it's visible at a glance, matching the "T?" unknown
case which was already inline. Badge gets a max-width + ellipsis so
long region names (e.g. "#dk-trekantsomraadet") don't blow out the
Type column — full name stays in the title.
2026-07-17 15:56:16 +02:00
dborup aaef080f94 feat: surface scope on the Packets tab's transport badge
The packet detail pane already showed scope (with an "unknown scope"
fallback for empty scope_name), but the packets table itself gave no
at-a-glance signal — the existing transportBadge() "T" marker only
encoded route type.

transportBadge() now takes an optional scopeName argument: a resolved
region enriches the tooltip, an empty scope_name (transport-eligible
but unmatched/ambiguous) renders as "T?" with a distinct muted badge
style instead of the confident amber, so it's visually distinguishable
from a resolved scope without relying on color alone. Existing callers
that don't pass scopeName (live.js) are unaffected.

QueryGroupedPackets (SQLite + in-memory) didn't select scope_name at
all — added it, since the Packets tab defaults to the grouped view.
2026-07-17 15:47:54 +02:00
Kpa-clawbotandcorescope-bot 8c3e397d39 fix(1846): drop 1200px cap on .observers-page (#1847)
Red commit: aabe8143d1

## Summary
Drop `max-width: 1200px` on `.observers-page` in `public/style.css`. The
observers table is column-dense and already runs `data-priority`
auto-hide on narrower viewports, so the 1200px cap crushed columns on
wide monitors while the mobile hide-column CSS had already dropped
columns for narrower widths — the exact "starts auto-hiding but never
expands back" behavior described in #1846.

Removes the cap so the browser layout uses available viewport width (as
`.analytics-page` does).

## Change
- `public/style.css:2113` — remove `max-width: 1200px` from
`.observers-page`.
- `test-issue-1846-observers-width.js` — new regression guard. Parses
`.observers-page` rule from `style.css`; fails if a `max-width` cap
`<1600px` is re-introduced (matches the `.analytics-page` convention
referenced in triage).
- `.github/workflows/deploy.yml` — wire the new test into the frontend
unit test job.

## Test discipline
- Red commit `aabe8143`: test only, no CSS change — CI fails on
assertion (`max-width 1200px must be >= 1600px`).
- Green commit `665e88a6`: CSS fix — test passes.

## Browser verification
Browser tool unavailable in this session (gateway restart required).
Change is a pure CSS width-cap removal on a single class; no JS, no
HTML, no theming implications. The regression guard makes silent
reintroduction impossible.

## E2E assertion
Regression guard is source-grep, not DOM-level:
`test-issue-1846-observers-width.js:41` (`.observers-page` block
max-width allowlist). Justification: bug is a single declarative CSS
attribute; a DOM E2E for it would just assert
`getComputedStyle(...).maxWidth === 'none'`, which mirrors the source
assertion at higher cost. If a follow-up wants a Playwright
viewport-resize check, easy to add.

## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— clean on the green HEAD.

Fixes #1846

---------

Co-authored-by: corescope-bot <bot@corescope.dev>
2026-07-12 22:41:01 -07:00
707d70c738 fix(packets): clamp .col-details to one line on mobile (#1770 S path) (#1805)
## Summary

Partial fix for #1770 (S quick-fix path only; L refactor remains as
follow-up).

The packets-view virtual-scroller assumes a constant
`VSCROLL_ROW_HEIGHT`, but the base rule at `public/style.css` L1097 lets
`td.col-details` wrap on narrow viewports (`white-space: normal;
word-break: break-word`). Wrapped rows produce variable row heights →
visible jitter when scrolling past ~900px on iOS.

**Quick-fix (S path):** under the existing `@media (max-width: 640px)`
block in `public/style.css`, clamp `.col-details` to a single line:

```css
.data-table td.col-details {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
```

Trade-off accepted in triage: Details column truncates on mobile in
exchange for smooth scrolling. The base rule keeps wrapping on desktop
(≥641px) so nothing changes there.

**Out of scope:** the full L-path fix (per-row measurement,
`_rowHeightsPx[]`, cumulative offsets, re-measure on hop-resolver
finalize) — tracked separately on #1770.

## TDD

- **Red commit** `7f58bedc` — adds
`test-issue-1770-mobile-row-clamp.js`, a CSS-grep test (same pattern as
`test-issue-1364-pill-no-clamp.js`) that walks every `@media (max-width:
640px)` block in `public/style.css` and asserts a `.col-details` rule
declares `white-space: nowrap`, `overflow: hidden`, and `text-overflow:
ellipsis`. Verified to FAIL on master (assertion failure, not a parse
error) and PASS after the CSS change.
- **Green commit** `d46271b8` — applies the 5-line CSS clamp inside the
existing mobile breakpoint at L2362.

## Files touched

- `public/style.css` (+13)
- `test-issue-1770-mobile-row-clamp.js` (+101, new)

## Preflight

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

---------

Co-authored-by: clawbot <bot@clawbot.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-28 07:48:39 -07:00
735d9eb516 fix(#1715): dark-theme role swatches via per-theme CSS tokens (#1757)
## Summary

Dark-theme variants of the neighbor-graph role swatches (`#ngRoleChecks`
labels on `/analytics?tab=neighbor-graph`) still failed WCAG AA after
#1720's light-theme fix because the swatches used inline
`style="color:#..."` from `customize.js` `DEFAULTS.nodeColors`
(palette-700) — bypassing the theme tokens entirely.

Measured before:

| Role | Color | vs `#1a1a2e` (dark) |
|---|---|---|
| repeater | `#dc2626` | 3.53:1 ❌ |
| companion | `#2563eb` | 3.30:1 ❌ |
| observer | `#8b5cf6` | 4.02:1 ❌ |

## Fix

- Defines `--role-{repeater,companion,room,sensor,observer}` in `:root`
(palette-700, ≥4.5:1 on white) and overrides them in both dark blocks
(`[data-theme="dark"]` + the `@media (prefers-color-scheme: dark)`
mirror) with palette-400/500 shades that clear AA on `#1a1a2e`.
- Refactors the neighbor-graph swatch DOM in `public/analytics.js` from
inline `style="color:${hex}"` to class-based `<span class="role-swatch
role-swatch--{role}">`, with matching CSS rules that read the tokens.
- Removes all 5 `#1715` entries from `tests/a11y-allowlist.yaml` per the
issue's acceptance criteria.

After:

| Role | Light (vs `#fff`) | Dark (vs `#1a1a2e`) |
|---|---|---|
| repeater | `#dc2626` 4.83:1 | `#ef4444` 4.53:1 |
| companion | `#2563eb` 5.17:1 | `#3b82f6` 4.64:1 |
| room | `#15803d` 5.02:1 | `#16a34a` 5.18:1 |
| sensor | `#b45309` 5.02:1 | `#d97706` 5.35:1 |
| observer | `#7c3aed` 5.70:1 | `#a78bfa` 6.27:1 |

## Tests

- `test-a11y-1715-dark-role-swatches.js` — CSS-driven WCAG AA probes for
the 5 per-theme `--role-*` tokens plus markup invariants (no inline
color span; class names present in `#ngRoleChecks` block).
- Red commit: `a09ec21c` — fails on assertion with 12
below-threshold/markup probes.
- Green commit: `f87dcd64` — all probes PASS.
- `tests/a11y-allowlist.yaml` shed all 5 entries; the umbrella
`test-a11y-axe-1668.js` (CI) is now the live-browser net for those
cells.

## Preflight overrides

- `check-xss-sinks.sh` flags `public/analytics.js:2502` (label
"observer" appears in an `innerHTML=\`tpl\`` line). The flagged token is
a hardcoded literal string — no user-controlled data flows into that
template. No template content changed in this PR; the flag is
preexisting noise from the heuristic scan and the gate ultimately marks
✅ pass.

Fixes #1715

---------

Co-authored-by: clawbot <clawbot@kpa.local>
Co-authored-by: clawbot <bot@example.com>
2026-06-20 16:15:58 -07:00
a344ae0a12 fix(#1719): contrast root causes — active-btn / skew-badge / role-swatch / status-green (#1720)
## Summary

Fixes the four recurring color-contrast root causes #1719 identifies
behind ~320 axe violations on PR #1707's expanded gate. All fixes are
token-based; no hardcoded hex introduced.

## TDD

- **Red:** `151db732` — `test-a11y-1719-contrast-root-causes-e2e.js`
asserts WCAG AA on all 4 patterns; failed with 12 sub-threshold probes.
- **Green:** `dd26554e` — fixes below; test now reports 11/11 PASS.

## Patterns + measured contrast (before → after)

| # | Surface | Before | After | Note |
|---|---|---|---|---|
| P1 | `.rf-range-btn.active` / `.clock-filter-btn.active` /
`.subpath-jump-nav a` / `#ptCheckBtn` / `#ptGenBtn` | `#fff` on
`--accent` (#4a9eff) = **2.75:1** | `--text-on-accent` on
`--accent-strong` = **4.95:1** | Consolidated into ONE grouped
`.btn-active-accent, ...` rule; inline buttons now use the shared class
|
| P2 | `.skew-badge--no_clock` (dark theme) | `#fff` on `--text-muted`
(#d1d5db) = **1.47:1** | `#fff` on `--skew-badge-no-clock-bg` (#4b5563)
= **7.56:1** | New dedicated token, both themes |
| P3 | Neighbor-graph role swatches, light theme on white | room 3.30:1
/ sensor 3.19:1 / observer 4.23:1 | room **5.02** / sensor **5.02** /
observer **5.70** | `customize.js` defaults bumped to
palette-{green/amber/purple}-700 |
| P4 | `.analytics-stat-card` text in `--status-green` on white |
**2.28:1** | new `--status-green-text` = #15803d → **5.02:1** |
`--status-green` background token unchanged (still #22c55e); inline text
usages routed to the new token |

## Why this unblocks #1707

#1707's 320 axe color-contrast hits decompose into:
- 137× single-rule `.skew-badge--no_clock` → P2.
- ~N×4 active-button surfaces (rf-health / clock-health / subpaths /
prefix-tool) → P1.
- Role-swatch text on `/#/analytics?tab=neighbor-graph` (light) → P3.
- `.analytics-stat-card` text on `/#/analytics?tab=nodes` (light) → P4.

After this merges, the next CI run on #1707 should see the expanded gate
go green (or down to a small ≤5 residual the operator can triage
separately per the issue's acceptance criteria).

## Local axe gate

`BASE_URL=… node test-a11y-axe-1668.js` was **NOT** run locally — the
sandbox's bundled chromium fails to boot Playwright (known issue). CI on
this PR runs the same gate against the staging fixture; relying on that.

The dedicated test `test-a11y-1719-contrast-root-causes-e2e.js` is
CSS+JS-parse-driven (no browser) and runs in <100ms — it's the
regression net for these 4 patterns specifically.

```
$ node test-a11y-1719-contrast-root-causes-e2e.js
  PASS [P1] theme=light .rf-range-btn.active  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P1] theme=light .clock-filter-btn.active  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P1] theme=light .subpath-jump-nav a  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P1] theme=dark .rf-range-btn.active  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P1] theme=dark .clock-filter-btn.active  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P1] theme=dark .subpath-jump-nav a  fg=#f9fafb bg=#2563eb  ratio=4.95:1
  PASS [P2] theme=light .skew-badge--no_clock  fg=#fff bg=#4b5563  ratio=7.56:1
  PASS [P2] theme=dark .skew-badge--no_clock  fg=#fff bg=#4b5563  ratio=7.56:1
  PASS [P3] theme=light nodeColors.room on white  fg=#15803d bg=#ffffff  ratio=5.02:1
  PASS [P3] theme=light nodeColors.sensor on white  fg=#b45309 bg=#ffffff  ratio=5.02:1
  PASS [P3] theme=light nodeColors.observer on white  fg=#7c3aed bg=#ffffff  ratio=5.70:1
  PASS [P4] theme=light .analytics-stat-card text color (--status-green-text)  fg=#15803d bg=#ffffff  ratio=5.02:1
  PASS [P4] theme=dark .analytics-stat-card text color (--status-green-text)  fg=#22c55e bg=#232340  ratio=6.65:1

PASS: all 4 root-cause patterns ≥ 4.5:1 in both themes (issue #1719)
```

## Out-of-scope (intentional)

- Other text-on-light `var(--status-green)` usages in `nodes.js`
(Critical/Valuable labels): different surface, not the
analytics-stat-card pattern #1719 calls out. Tracked under analytics
audit umbrella.
- Hardcoded `var(--status-green, #2ecc71)` fallback in `nodes.js` lines
648/666: same scope deferral.
- Allowlist entries: none added per the issue's acceptance criteria.

Fixes #1719.

---------

Co-authored-by: clawbot <clawbot@kpa.local>
Co-authored-by: Kpa-clawbot <bot@openclaw.local>
2026-06-13 14:47:57 -07:00
293efdb647 fix(#1705): subpath-selected hop-prefix contrast BLOCKER (dark, 1.87:1 → ≥4.5:1) (#1708)
## Summary

Fixes the BLOCKER half of #1705: `.subpath-selected .hop-prefix`
contrast in `public/style.css`.

| | Before | After |
|---|---|---|
| background | `var(--accent)` = `#4a9eff` | `var(--accent-strong)` =
`#2563eb` |
| color (primary) | `#fff` | `var(--text-on-accent)` = `#f9fafb` |
| color (hop-prefix) | `rgba(255,255,255,0.6)` | `var(--text-on-accent)`
|
| measured contrast (hop-prefix) | **1.87:1** (composite over
`--accent`, dark) | **4.95:1** (light + dark) |

Pure token swap onto the existing `--accent-strong` / `--text-on-accent`
pair already used by `.badge-selected`, `.filter-bar .btn.active`,
`.dropdown-item:hover` etc. No new hex literals. Light and dark themes
both pass WCAG AA body text (≥4.5:1).

## TDD trail

- Red: `033f8e4c` — `test-a11y-1705-subpath-hop-prefix-e2e.js`. Parses
`public/style.css`, resolves the relevant tokens per theme, composites
the alpha-bearing text over the rendered background, asserts WCAG
contrast ≥ 4.5:1. Failed with `ratio=1.87:1` on both themes — the exact
value cited in #1705.
- Green: `db6b9dd0` — CSS fix. Test now reports `composite=#f9fafb,
ratio=4.95:1` on both themes.

Why a dedicated test (not just `test-a11y-axe-1668.js`):
`.subpath-selected` is a click-state class, so the umbrella axe gate
never sees it during initial-paint scans. This is the canonical
"state-only" a11y regression class — the umbrella gate is structurally
blind to it.

## Out of scope (documented in #1705 for separate follow-up)

- The **a11y audit probe correctness fix** (alpha-composite + parent-bg
walk) lives in workspace tooling
(`workspace-meshcore/a11y-audit/audit.py`), not in this repo. The
probe-correctness write-up is captured in #1705 itself; this PR is
exclusively the CSS BLOCKER + regression test.
- "Other rgba-based dark-mode contrast surfaces" — per the issue's
Out-of-scope section, those get filed separately if discovered.

## Local verification

```
$ node test-a11y-1705-subpath-hop-prefix-e2e.js
  PASS theme=dark  bg=#2563eb text=#f9fafb ratio=4.95:1
  PASS theme=light bg=#2563eb text=#f9fafb ratio=4.95:1
```

The full `test-a11y-axe-1668.js` gate could not be exercised on this
sandbox (Chromium SIGTRAPs against the host kernel — unrelated to this
change). CI runs it on Ubuntu where the umbrella ruleset already
enforces the 0-violation policy.

## Browser verified

CSS-only change in a CSS-variable swap. Computed values are
deterministic from the stylesheet and asserted by the new test; no JS /
DOM / render-path is touched.

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— all gates clean.

Fixes #1705

---------

Co-authored-by: clawbot <bot@clawbot.local>
Co-authored-by: Kpa-clawbot <bot@clawbot>
2026-06-13 08:19:05 -07:00
d954ea7444 feat(#1668): axe-core CI gate for WCAG AA color-contrast (M5) (#1696)
Partial fix for #1668 (M5 of 6).

After M1 (audit), M2 (color tokens, #1676), M3 (typography floor,
#1679), and M4 (per-route polish, #1681) cleared ~95% of
contrast/typography violations, M5 **locks in the wins** by adding an
axe-core CI gate that fails the build on any new WCAG AA color-contrast
regression.

## What's in the box

- `test-a11y-axe-1668.js` — Playwright + `@axe-core/playwright`. Runs
every major CoreScope route × `{dark, light}` at 1200×900 desktop,
injects axe, runs only the `color-contrast` rule, asserts net violations
=== 0.
- `test-a11y-axe-1668-selftest.js` — fast, deterministic, browser-free
unit test that exercises the YAML allowlist parser, the
`violationAllowed` matcher, and the route/theme metadata. Runs in the JS
unit block (no browser needed).
- `tests/a11y-allowlist.yaml` — operator-flagged false-positive
allowlist. **0 entries at M5 baseline.**

## Allowlist format

Each entry MUST cite a GH issue # and an `expires_at` date. Missing
fields = refused. Expired `expires_at` = refused (warning logged). This
**forces a periodic revisit** — no permanent suppressions.

```yaml
- route: /analytics?tab=channels
  selector: ".some-known-stale-element"
  rule: color-contrast
  issue: 1234
  expires_at: 2026-09-01
```

## Routes covered (19 × 2 themes = 38 cells)

`/`, `/packets`, `/nodes`, `/channels`, `/live`, `/map`, `/observers`,
`/compare`,
`/analytics?tab={overview,rf,topology,channels,hashsizes,collisions,roles,airtime}`,
`/audio-lab`, `/customize`, `/replay`.

## TDD red→green

- **RED** (`08adafdb`) — adds the gate + deliberately regresses
`--text-muted` from `palette-gray-700` (~10:1) to `#9ca3af` (~2.4:1).
axe-core fails on every light-theme cell.
- **GREEN** (`f62fb1e0`) — restores the M2 token. Net violations = 0
across all 38 cells.

## Scope discipline

- Only `color-contrast` (matches M2/M3/M4 scope). M6 owns `image-alt`,
`aria-required-attr`, `label`, mobile viewports, and letsmesh A/B.
- No new design tokens.
- M2-M4 tokens untouched.

## CI wiring

- `.github/workflows/deploy.yml:155` — selftest in JS unit block.
- `.github/workflows/deploy.yml:367` — real axe browser run in the
Playwright E2E block after the fixture server is up.

## Deps

`@axe-core/playwright@4.11.3` + `axe-core@4.12.1` added to
`devDependencies`. Pinned versions.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-06-12 20:00:35 -07:00
6dfe589b57 fix(#1668): per-route polish — hash cells, badges, /live, modals (M4) (#1681)
Partial fix for #1668 (M4 of 6).

After M2 (color tokens, PR #1676, ~85% BLOCKER) and M3 (typography
floor, PR #1679, ~87% MAJOR), what's left are route-specific structural
issues that token/floor passes can't reach. M4 closes those with
surgical carve-outs — no new top-level tokens, no semantic encoding
flattened.

## Route × selector × fix

| Route | Selector | Before | After |
|---|---|---|---|
| `/analytics?tab=hashsizes` `/analytics?tab=collisions` |
`td.hash-cell` + `-collision/-taken/-possible` (302+ M1 violations) |
11px/400; collision-fg 3.61, taken-fg 2.5, possible-fg 1.9 on respective
bg | 12px base, 12px/700 on semantic cells. Bg palette preserved
(green/yellow/orange still distinct). Inline style in analytics.js
bumped 11→12. |
| `/packets` `/live` `/nodes` (everywhere `<span class="badge
badge-*">`) | All 14 TYPE_COLORS badges (ADVERT, REQUEST, RESPONSE, …) |
`${color}20` translucent wash with `color: ${color}` — ratio **1.0–4.25,
all BLOCKER** | `syncBadgeColors` rewritten: pick readable fg by
luminance, darken bg in 8% steps until AA (≥4.5:1). All 14 PASS
(4.57–7.94). TYPE_COLORS itself unchanged — map dots / live-feed dots
keep full hue. |
| `/live` | `.vcr-live-btn` ("LIVE") | `rgba(239,68,68,0.2)` +
status-red fg = **1.0:1** | Solid `--status-red` + #fff = 5.25:1;
12px/700 |
| `/live` | `.vcr-scope-btn.active` (1h/6h/12h/24h selected) |
`--accent-bg` wash + `--text` = 2.98:1 BLOCKER | `--accent-strong` +
`--text-on-accent` (M2 tokens, AA) |
| `/live` | `.vcr-btn` `.vcr-scope-btn` | 0.9rem/400, 0.75rem/400
(thin-small) | 14px/500, 12px/500 desktop; 12px/600 ≤640px |
| `/live` | `.live-feed-empty` | 12px/400 (thin-small) | 12px/500 |
| `/packets` (path hops) | `.path-hops .hop-named` | font-size inherited
(variable) | explicit 12px/600 |

## TDD & gating

- **RED** `341f47f1` — 23 assertion failures (9 typography + 14
badge-contrast). New gate `test-issue-1668-m4-per-route.js` executes
`syncBadgeColors` in a VM sandbox and asserts each emitted `.badge-*`
rule clears WCAG AA; also checks rule-level font-size/font-weight
floors.
- **GREEN** `6ef17491` — both axes 0/0.
- Test wired into `.github/workflows/deploy.yml:144` alongside M3.
- Anti-tautology proven locally: `git stash public/roles.js` returns the
test to FAIL with the badge assertions; pop restores GREEN.

## Re-scan findings
`a11y-audit/m4-rescan.jsonl` — `/live` (timed out in M1) now probes
cleanly: 29 dark / 39 light residuals all caught by this PR. Channel-add
and customize modals probed clean (M2 tokens already cover; nothing
chip-level needed).

## Out of scope
M5 (axe CI gate) and M6 (letsmesh side-by-side A/B) are next milestones.

---------

Co-authored-by: agent <agent@openclaw.local>
Co-authored-by: meshcore-bot <bot@meshcore>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-06-12 22:14:23 +00:00
6aa5146b93 fix(#1660): FE warm-up banner reads X-Corescope-Load-Status + polls /api/healthz (#1683)
## Summary

Partial fix for #1660 — adds an FE-only global warm-up banner that
surfaces server-side load state to users instead of letting "data may be
incomplete" look like silent breakage.

Implements sub-deliverables **(1)** and **(3)** from the triage.
Sub-deliverable (2) (per-card "recomputing" pill) is deferred — it
depends on a new server-side `recomputer.first_pass_done` flag that
pairs with #1659.

## What it does

- New `public/warmup-banner.js` mounts a sticky `role="status"` live
region at the top of `<body>`. Pure helper `getWarmupMessages()` is
fully unit-tested in isolation.
- Consumes both signals the server already exposes:
- `X-Corescope-Load-Status` response header (set by
`cmd/server/chunked_load.go:446` on every API response) — captured via a
thin `window.fetch` wrapper.
- `GET /api/healthz` — polled every 30s while not in steady-state, torn
down once `ready=true` AND `from_pubkey_backfill.done=true`.
- Messages per acceptance criteria:
  - `loading` → "⏳ Loading historical data — counts may be incomplete."
- `from_pubkey_backfill.done=false` → "Backfilling pubkey index: 12,400
/ 87,500 (14%)"
- `ingest_liveness.<src>.lastReceiptUnix` older than 5 min → "No packets
from `<src>` in N min."
- Banner fades out (opacity + max-height transition) once steady-state
is reached.

## Files

- `public/warmup-banner.js` — new module (pure helpers + DOM mount +
poll + fetch interceptor).
- `public/style.css` — `.warmup-banner` rules; all colors via existing
`--warn-bg` / `--warn-text` / `--warning` CSS variables
(customizer-safe, no inline hexes).
- `public/index.html` — loads `warmup-banner.js` immediately before
`app.js` so the fetch wrapper is installed before other modules issue
requests.
- `test-warmup-banner.js` — 8 tests: 6 pure-helper + 2 vm-DOM E2E that
stub `/api/healthz` returning `ready:false` → asserts banner visible,
then flips to `ready:true` → asserts the `warmup-banner--hidden` class
is applied (sub-deliverable 3).

## TDD red → green

- **Red:** `ca5f9837` — `test(#1660): RED — failing tests for warmup
banner message derivation` — stub `getWarmupMessages` returns `[]`; CI
fails on 3 assertion failures (compiles cleanly, fails on
`assert.ok(msgs.length >= 1)` etc — not on import/build).
- **Green:** `0d07efdf` — `feat(#1660): GREEN — warmup banner reads
X-Corescope-Load-Status + polls /api/healthz` — implementation lands;
all 8 tests pass.

## Test output

```
warmup-banner.js (#1660):
  ✅ exports getWarmupMessages and shouldShowBanner
  ✅ loading header alone produces a "historical data" message
  ✅ from_pubkey_backfill.done=false produces a progress message with pct
  ✅ stale ingest source >5min produces a "No packets from" message
  ✅ steady-state ready=true + backfill done + fresh ingest → no banner
  ✅ isSteadyState reflects ready+backfill predicate
  ✅ E2E: stub /api/healthz ready=false → banner visible
  ✅ E2E: flip /api/healthz to ready=true → banner fades (hidden class)
passed=8 failed=0
```

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— **clean** (PII / branch scope / red commit / CSS-var defined / CSS
self-fallback / LIKE-on-JSON / sync migration / async-migration gate /
XSS sinks all PASS, no warnings).

## Performance

- Poll runs every 30s and only while `ready=false ||
from_pubkey_backfill.done=false`. Stops immediately on steady state. No
hot-path impact.
- Fetch wrapper adds one `.then()` per response to read a single header
— O(1).
- Banner DOM is one `<div>` with a `<ul>` of ≤3 `<li>`s. Re-render is a
single innerHTML set.

## Out of scope (explicit)

- Sub-deliverable (2) — per-card "↻ Recomputing…" pill. Requires a new
`recomputer.first_pass_done` field on `/api/healthz` (small
`cmd/server/analytics_recomputer.go` addition) and is grouped with the
#1659 recomputer redesign. Not in this PR.
- No backend code changed.

Partial fix for #1660.

---------

Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-06-12 11:38:35 -07:00
Kpa-clawbotandopenclaw-bot 626900a22a fix(#1668): typography pass — 14px body / 12px+500 chip floor (M3) (#1679)
Red commit: 91fc49f98a (CI run: pending
until pushed)

**Partial fix for #1668 (M3 of 6).**

M2 cleared ~85% of BLOCKER contrast violations (v3.9.1). M3 addresses
the
M1-audit `thin-small` findings: chips, badges, table cells, and meta
labels
where `font-size < 14px AND font-weight < 500` made text hard to read
for
the operator regardless of contrast — the original "the typography
sucks"
complaint that drove this issue.

## Design (operator-locked)

- Body text floor: **14px**
- Chip / badge / meta floor: **12px AND weight ≥ 500** (or 14px if 400)
- Visual hierarchy preserved — H1/H2/H3 untouched
- No palette changes (M2 owns colors) · no layout (M4 owns that)

## What changed

New `:root` weight tokens: `--fw-{normal,medium,semibold,bold}`.

| Selector | Before (px/weight) | After |
|---|---|---|
| `.nav-link` | 12.8 / 400 | 12-14 fluid / **500** |
| `.tab-btn` | 13 / 400 | 13 / **500** |
| `.alab-pkt` (audio-lab) | 12 / 400 | 12 / **500** |
| `.ch-item-time` | 11 / 400 | **12** / **500** |
| `.ch-item-preview` | 12 / 400 | **13** / **500** |
| `.payload-bar-label` | 12 / 400 | 12 / **500** |
| `.stat-label` | 12 / 400 | 12 / **500** |
| `.col-hidden-pill` | **10** / 700 | **12** / 700 |
| `.skew-badge` | **10** / 600 | **12** / 600 |
| `.filter-group .btn` | 12 / 400 | 12 / **500** |
| `.timestamp-text` (new explicit rule) | inherits 12 / 400 | **13** /
**500** |
| `.data-table` (incl. mobile override) | 12-11 / 400 | **13** / **500**
(mobile 12) |
| `.mono` | 12 / 400 | **13** / **500** |

Estimated thin-small violations cleared: **~5,800 of 6,313 MAJOR** in
the
M1 dataset (timestamps + table cells + nav links are the bulk).

## Letsmesh bar

Letsmesh ships 12.5-15px paired with 600 weight on chips — our 12px+500
floor is one notch lighter but matches the operator-locked spec.

## Tests

TDD: `test-issue-1668-m3-typography.js` (new) parses `style.css` +
`audio-lab.js`, computes effective font-size/weight per selector with
CSS cascade resolution, and asserts the floor on 12 high-impact
selectors.

Red commit fails 10/12 on master; green commit makes all 12 pass. Anti-
tautology verified: reverted `.skew-badge` bump → test fails on the
assertion (not a build error) → restored → test passes.

## Verified on staging (hot-patch)

Computed styles AFTER patch: `.timestamp-text` 13/500, `.skew-badge`
12/600, `.nav-link` 12.8/500.

## Next

- M4 — per-route polish + map legend
- M5 — CI gate that re-runs the M1 probe and fails on regressions
- M6 — A/B verification with the operator

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-12 04:34:37 -07:00
Kpa-clawbotandopenclaw-bot f0addfdabf fix(#1668): palette indirection + WCAG AA token bumps (M2 + #1671) (#1676)
Red commit: d761516d60 (no CI run —
branch-only push does not trigger workflows on this repo; verified
locally: `node test-issue-1668-m2-contrast.js` fails on assertion at
HEAD~1, passes at HEAD)

Partial fix for #1668 (M2 of 6). Fixes #1671.

## What changed

**Two-tier CSS tokens** introduced in `public/style.css`:

1. **Tier-1 (`--palette-*`)** — 38 raw colour stops, theme-independent,
in a single
`:root` block at the top of the file. Source: Tailwind v3 default
palette (MIT,
   battle-tested for WCAG-graded luminance steps).
   - gray ×9, blue ×9, green ×5, amber ×5, red ×5, purple ×5
- Single source of truth: no rule outside this block uses raw
`#hex`/`rgb()`.
2. **Tier-2 (semantic)** — existing `--text`, `--text-muted`,
`--surface-*`, etc.
re-plumbed to point at palette stops in both theme blocks. Behaviour
preserved
   where contrast was already AA.

**WCAG AA bumps for M1 BLOCKER tokens**:

| Token / surface | Before | After | Theme |
|---|---:|---:|---|
| `--text-on-accent` on `--accent-strong` (was `#fff` on `--accent`) |
2.75:1 | **4.95:1** | dark + light |
| `--text-muted` on `--surface-1` | ~3.5:1 | **11.58:1** | dark |
| `--text-muted` on `--card-bg` | ~5.0:1 | **10.28:1** | dark |
| `--text-muted` on `#ffffff` | 5.74:1 | **10.31:1** | light |
| `--text-muted` on `--surface-0` | 5.32:1 | **9.45:1** | light |

**New tokens**: `--accent-strong` (= `--palette-blue-600` = `#2563eb`),
`--text-on-accent` (= `--palette-gray-50` = `#f9fafb`), `--text-subtle`.

Rules migrated to the new accent pair (all were `#fff` on
`var(--accent)` =
2.75:1 in the M1 audit): `.skip-link`, `.tab-btn.active`,
`.filter-bar .btn.active`, `.filter-group .btn.active`,
`[data-theme="dark"] .filter-bar .btn.active`, `.path-hops .hop-named`.

## Operator-reported chip (2026-06-12 — `.hop-named.hop-link`)
Dark-blue text on dark-blue chip background. Patched via `.path-hops
.hop-named`
+ `--accent-strong`. Now reads as `#f9fafb` on `#2563eb` = 4.95:1 (AA
pass) in
both themes. Before/after screenshots: see

`a11y-audit/m2-screenshots/{before,after}-packets-{dark,light}-1200x900.jpg`.

Letsmesh's UI uses chip text at 6.77:1 on equivalent surfaces; this PR
closes
most of the gap.

## TDD trail
- Red commit `d761516d` — assertion-based contrast test, fails on
missing palette.
- Green commit `e5e87309` — palette + remap + bumps + AA pass.
- Anti-tautology: reverting dark `--text-muted` back to `#6b7280`
reproduces
  `text-muted on surface (dark): contrast 3.53:1 < 4.5:1`.

## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ all
gates pass (incl. CSS-var-defined: 1928 var() refs, 0 undefined).

## Not in this PR (intentional)
- M3 typography (`14px` floor + weight 500 for chips/badges) — own PR
- M4-M5 per-route polish — own PRs
- M6 axe CI gate — own PR
- Shipping an alternate palette — deferred, indirection enables it

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
2026-06-11 22:25:44 -07:00
Kpa-clawbotandBot 89eade6e7b M6: emoji → Phosphor — final sweep, lint gate, carry-forwards (#1648) (#1654)
Red commit: fe7468d473 (CI run: will
appear in this PR's Checks tab — emoji lint test fails on the red
commit, passes on green)

**Fixes #1648.**

Closes the 6-milestone emoji→Phosphor migration started in #1649.

## Sweep results (real UI icons swapped this PR)

| File:line | Before | After |
|---|---|---|
| `public/index.html:140` | ⭐ | `ph-star-fill` |
| `public/mobile-page-actions.js:154` | ⭐ | `ph-star-fill` |
| `public/geofilter-builder.html:76` | ⬇ | `ph-download-simple` |
| `public/analytics.js:2103` | ⏱️ | `ph-clock` |
| `public/analytics.js:2288` | ⏰ | `ph-clock` |
| `public/analytics.js:4080` | ⏰ | `ph-clock` |
| `public/nodes.js:1066` | ⏰ | `ph-clock` |
| `public/observer-detail.js:284` | ⏰ | `ph-clock` |
| `public/channel-qr.js:133` | 📋 | `ph-clipboard-text` |
| `public/channel-qr.js:139` | ✓ | `ph-check` |
| `public/packets.js:1472` | ⏸ | `ph-pause` |

## Carry-forwards addressed

- **M5 CDP — live `.cust-emoji-preview` re-render** —
`public/customize-v2.js:2434` wires `renderConfigGlyph` to the `input`
event so previews update without Save+reload. (commit `9e698a04`)
- **M5 CDP — `.modal-close` 44×44 mobile** — `public/style.css` adds a
≤640px breakpoint bumping both `.modal-close` and `.ch-modal-close` to
WCAG-minimum hit targets. (commit `9e698a04`)
- **M4 CDP — route-hop fallback color** — `public/route-render.js` now
reads `var(--status-info)` (new token added to `:root` and dark-mode
blocks in `style.css`) instead of baked `#3b82f6`. (commit `9e698a04`)
- M2 carry-forward set (⭐ favStar / ▾ More / ⚠️ clock / 🌱⚡ welcome
cards) verified already addressed by re-running M3 emoji scan — all
green.

## Lint gate (M6 headline)

- Test: `test-issue-1648-m6-final-sweep.js` — full repo scan across
`public/**.{js,html,css}` and `cmd/(server|ingestor|decrypt)/*.go`.
- Self-test: `test-issue-1648-m6-lint-self.js` — exercises the lint
engine + anti-tautology probe.
- Allowlist: `tests/emoji-allowlist.txt`. Format: `path` (glob),
`path:line`, `path:line:U+XXXX`, or `/regex/`. Add intentional emojis
here with a `# why` comment.
- Wired into `test-all.sh` alongside M1/M2/M3 scans.

## routes.go smoke check

Server-side defaults in `cmd/server/routes.go:567-574` confirmed
`ph:bluetooth`/`ph:radio`/`ph:broadcast`/`ph:repeat` (M5 landed).
Operator-customized configs on staging/prod still carry their legacy
emoji overrides — per M5 design call those are preserved and NOT touched
by this PR.

## PR closing list

Fixes #1648. M1 #1649 ✅, M2 #1650 ✅, M3 #1651 ✅, M4 #1652 ✅, M5 #1653 ✅,
this PR ✅.

---------

Co-authored-by: Bot <bot@corescope>
2026-06-11 05:44:37 -07:00
Kpa-clawbotandopenclaw-bot 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>
2026-06-11 05:04:29 -07:00
Kpa-clawbot b812a98a71 M3: emoji → Phosphor Icons — detail panes & badges (#1648) (#1651)
Red commit: 537fbbc6b0 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2F1648-m3-details-badges)

Partial fix for #1648 (M3 of 6). Do NOT close the tracking issue.

M3 covers detail panes, status pills, role/payload-type badges per the
tracking-issue M3 checklist. Builds on M1 sprite + M2 chrome.

## Per-file swap counts

| file | swaps (sprite refs) |
| --- | --- |
| public/home.js | 26 |
| public/channels.js | 27 |
| public/route-view-utils.js | 11 |
| public/route-view.js | 4 |
| public/app.js | 4 |
| public/hop-display.js | 3 |
| public/path-inspector.js | 1 |
| **total** | **76** |

Plus 6 new Phosphor SVG symbols vendored into
`public/icons/phosphor-sprite.svg` (regular weight, alphabetical):
`ph-bluetooth`, `ph-camera`, `ph-hexagon`, `ph-paper-plane-tilt`,
`ph-plant`, `ph-rocket`.

## Status-token integration (design decision 3)

`.status-{ok,warn,err,muted}` rules added in `public/style.css` (lines
22-35). Each threads a `--status-*` color via `color:` and Phosphor
sprites inside inherit via `currentColor` — no fill colors baked into
sprite refs. Verified by an emoji-scan assertion
(`test-issue-1648-m3-emoji-scan.js` `assertStatusTokenCss()`) and an E2E
computed-style probe.

## TDD evidence

- Red commit `537fbbc6` adds the failing scan
(`test-issue-1648-m3-emoji-scan.js`) alone — see branch CI history.
- Green commit `4a0cd89a` implements the swaps.
- Anti-tautology: reverting one sprite swap in `path-inspector.js`
reproduces the assertion failure; restored.

## E2E assertions added

`test-issue-1648-m3-icons-e2e.js:1-188` — Playwright behavioral checks
for `/home` (welcome cards), `/channels` (sidebar + modal),
`/nodes/<pk>` (detail pane), `/analytics`, `/live`, plus `.notdef`
resolver and `.status-ok` computed-color probe. Registered in
`.github/workflows/deploy.yml`.

## Test updates (drift)

`test-frontend-helpers.js` updated 6 assertions to match sprite-rendered
HTML: hop-display unreliable-badge (`#ph-warning`), `#1504`
`PATH_SYMBOLS_LEGEND` (`glyph` → `glyphHtml`), `#781`/`#811`
channel-lock affordance (`#ph-lock`). Pre-existing 2 `favStar` failures
from M2 baseline remain unchanged (out-of-scope here).

## Out of scope (next milestones)

- `public/customize.js` / `public/customize-v2.js` operator-customizable
emoji config → **M5**
- `cmd/server/routes.go` server-rendered onboarding config → **M6**
- `public/route-view-v2.js` route-overlay glyph logic → **M4** (this PR
touches only `route-view-utils.js` payload taxonomy + `route-view.js`
sidebar, not the overlay)
2026-06-11 02:06:32 -07:00
Kpa-clawbotandopenclaw-bot 3062745437 M2: emoji → Phosphor Icons — page headers & table chrome (#1648) (#1650)
Red commit: df6a406a89 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2F1648-m2-headers-tables)

Partial fix for #1648 (M2 of 6). Do NOT close the tracking issue.

M2 covers page headers + table chrome: section glyphs, refresh/action
buttons, status pills, payload-type icon maps. Heavy on analytics.js.

## Per-file swap counts

| file | swaps |
| --- | --- |
| public/analytics.js | 89 |
| public/nodes.js | 29 |
| public/packets.js | 30 |
| public/live.js | 30 |
| public/map.js | 11 |
| public/perf.js | 9 |
| public/audio-lab.js | 5 |
| public/node-analytics.js | 4 |
| public/table-sort.js | 1 |
| public/traces.js | 1 |
| **total** | **209** |

Plus 48 new Phosphor SVG symbols vendored into
`public/icons/phosphor-sprite.svg`
(regular weight, alphabetical): arrows-out, battery-high, battery-low,
bomb,
book-open, buildings, caret-down, caret-up, cell-signal-high,
chart-line, chats,
check-circle, clipboard-text, clock, crosshair, dice-five, envelope,
flame,
gear, globe, graph, handshake, house-line, info, key, link,
list-numbers,
lock-open, map-pin, microphone, path, piano-keys, prohibit, pulse,
push-pin,
question, radio, repeat, ruler, share-network, shuffle, signpost,
speaker-high,
target, thermometer, trend-up, trophy, x-circle. Total sprite now 82
symbols, ~35 KB.

## Tests

- Static scan: `test-issue-1648-m2-emoji-scan.js` asserts ZERO emoji
  codepoints (U+1F300–1FAFF, U+2600–27BF) and zero misc-icon chars
  (◆●■▲★☆○✓✗⚠✉) in each M2 file, plus a minimum `<use href="…#ph-…">`
  ref count per file.
- E2E: `test-issue-1648-m2-icons-e2e.js` — 15 Chromium assertions
  (test-issue-1648-m2-icons-e2e.js:31–245) covering /analytics, /packets
  filter row, /nodes table chrome, /live audio + feed buttons, /map
  controls h3 + toggle, /traces, /perf, /audio-lab loop button, plus a
  sprite-resolution check (every rendered `<use>` resolves to a defined
  `<symbol>` — i.e. no `.notdef` glyph fallback). E2E assertion added:
  `test-issue-1648-m2-icons-e2e.js:96`.
- Both wired into `.github/workflows/deploy.yml` E2E block.

Anti-tautology proof: reverting the audio-lab.js Packet Data h3 swap
(restoring `📦`) flips the static scan from PASS to assertion failure
`actual: 1, expected: 0` (audio-lab.js emoji-hit count check). Verified
locally before push.

## Browser verification

Local Chromium against `corescope-server -port 13581` + e2e fixture DB.
Screenshots of /analytics, /nodes, /packets, /live, /map at 1200×900 and
375×812. No `.notdef` glyphs; theme toggle preserved; sprite resolves on
every page.

## Out of scope (carried forward)

- customize.js / customize-v2.js NODE_EMOJI + PACKET_TYPE_EMOJI configs
**[M5]**
- `cmd/server/routes.go` L567-574 onboarding-tile emoji **[M6]**
- home.js welcome cards 🌱 ⚡ etc. **[M3]**
- route-view overlays (route-view-utils.js, route-view.js,
hop-display.js, path-inspector.js) **[M4]**
- channels.js modals + footer 💬 📋 🔒 **[M5]**
- roles.js NODE_SHAPE_EMOJI (used by route-view, not M2) **[M4]**
- packets.js L2169 expand caret swapped (was `▶/▼`); other ▶ in
audio-lab
  alabPlay button left as-is — out of M2 range (U+25B6 ≠ emoji).

Adheres to rule 34: no `Fixes #1648`, no auto-close.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-11 00:42:33 -07:00
Kpa-clawbotandopenclaw-bot 55e4d957b1 M1: emoji → Phosphor Icons — top-nav, mobile nav, Compare (#1648) (#1649)
Red commit: 12e921d9ba (CI run: pending —
see Actions tab on this branch)

Partial fix for #1648 (M1 of 6). **Do NOT close the tracking issue** —
only top-nav, mobile nav, drawer, mobile-page-actions, and the Compare
entry points are migrated here. M2-M6 (page headers, table chrome,
detail panes, map overlays, settings, lint gate) follow in subsequent
PRs.

## What changed

Replaces emoji glyphs used as UI iconography with vendored Phosphor SVG
sprite refs. Pattern: `<svg class="ph-icon"><use
href="/icons/phosphor-sprite.svg#ph-NAME"/></svg>`. Inherits color via
`currentColor`, sizes to `1em`, no FOUT, no CDN, no webfont.

Sprite: `public/icons/phosphor-sprite.svg` — 34 symbols, 13 KB, regular
weight (plus `circle-fill`/`star-fill`/`square-fill` for status dots).

## Surfaces swapped (M1)

| File | Before (emoji) | After (ph-NAME) |
|---|---|---|
| `public/index.html` L123 | 🔴 | `ph-broadcast` |
| `public/index.html` L129 | ⚡ | `ph-lightning` |
| `public/index.html` L130 | 🎵 | `ph-music-note` |
| `public/index.html` L143 | 🔍 | `ph-magnifying-glass` |
| `public/index.html` L144 | 🎨 | `ph-palette` |
| `public/index.html` L149/L150 | ☀️/🌙 | `ph-sun` / `ph-moon` |
| `public/index.html` L153 | ☰ | `ph-list` |
| `public/bottom-nav.js` TABS | 🏠📦🔴🗺️💬☰ | `house package broadcast
map-trifold chat-circle list` |
| `public/bottom-nav.js` MORE_ROUTES | 🖥️🛠️👁️📊⚡🎵 | `monitor wrench eye
chart-bar lightning music-note` |
| `public/bottom-nav.js` L265 | 🌙/☀️ | `ph-moon` / `ph-sun` |
| `public/nav-drawer.js` ROUTES | (mirror of MORE_ROUTES) | same
Phosphor mapping |
| `public/mobile-page-actions.js` | 🔍🎨 | `ph-magnifying-glass`
`ph-palette` |
| `public/observers.js` Compare obs | 🔍 | `ph-magnifying-glass` |
| `public/observers.js` Compare-selected | ⚖️ | `ph-scales` |
| `public/observers.js` refresh | 🔄 | `ph-arrow-clockwise` |
| `public/observers.js` packetBadge | 📡⚠ | `ph-broadcast` + `ph-warning`
|
| `public/observers.js` row health-dot | ●/▲/✕ | `ph-circle-fill` /
`ph-triangle` / `ph-x` |
| `public/observer-detail.js` Compare | 🔍 | `ph-magnifying-glass` |
| `public/observer-detail.js` health-dot | ● | `ph-circle-fill` |

Also: misc-symbols (`●▲✕`) on observer health dots and the box-drawing
role shapes (per #1648 surprise #3) are migrated here because they live
on the same lines as M1 emoji.

## Tests

E2E assertion added: `test-issue-1648-m1-icons-e2e.js:104` (asserts each
bottom-nav tab renders a `.ph-icon` with non-zero
`getBoundingClientRect`)

Two new tests, both committed RED first then GREEN:

- `test-issue-1648-m1-emoji-scan.js` — static file scan; fails if any M1
file contains emoji or misc-icon codepoints.
- `test-issue-1648-m1-icons-e2e.js` — Playwright; loads top-nav +
bottom-nav + `/observers`, asserts `.ph-icon` children render with
non-zero size and the rendered nav DOM has zero emoji codepoints.

Existing tests untouched and still green (e.g.
`test-observers-headings.js`, frontend-helpers).

## Browser verified

Local Chromium against `python3 -m http.server` from `public/`.
Screenshots taken at 375 / 768 / 1200 × dark/light — all icons render
via `currentColor`, theme toggle recolors, no `.notdef` glyphs, no
layout shift vs pre-fix master.

## Out of scope (deferred to M2-M6)

- Home-page chooser cards (📱⚡), per-page header `<h2>` glyphs,
packet-table chrome — M2.
- Status pills, role/packet-type badges, payload-type icon maps — M3.
- Map popups + route overlays — M4.
- Customize panel emoji configs, channel modals, settings — M5.
- Server-rendered onboarding strings in `cmd/server/routes.go`, `make
lint-no-emoji` gate — M6.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-10 22:54:44 -07:00
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>
2026-06-11 05:44:00 +00:00
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>
2026-06-10 17:02:47 -07:00
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>
2026-06-08 22:13:02 -07:00
efiten 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.
2026-06-08 12:35:12 +00:00
efitenandClaude Opus 4.8 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>
2026-06-08 13:11:06 +02:00
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
f12911dc feat(#1290): exclude listener-only observers from path-hop disambiguator
5f7fdb96 test(#1290): red — assert listener-only observers excluded from path-hop candidates
```

Full server + ingestor + dbschema + migrate test suites pass locally.

## Acceptance checklist (from #1290)

* [x] Ingestor parses `repeat` field (boolean OR string `on|off`)
* [x] Field persisted on `observers` table (new `can_relay BOOLEAN`
column, idempotent migration via `internal/dbschema`)
* [x] Server's disambiguator (`pm.resolveWithContext`) excludes
`can_relay=false` observer-nodes from path-hop candidate set
* [x] UI badge on observers list + node detail page indicating
"listener" vs "repeater"
* [x] Backward compat: legacy observers default to `can_relay=true`
* [x] Test: `repeat:off` → NOT a candidate
* [x] Test: `repeat:on` → IS a candidate
* [x] Test: legacy → IS a candidate

## Out of scope (preserved per issue)

Backfilling already-resolved paths is left as a follow-up. No
firmware/broker changes.

---------

Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-06-08 01:27:13 -07:00
Kpa-clawbotandKpa-clawbot 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>
2026-06-06 22:45:02 -07:00
Eldoon Nemar 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"
/>
2026-06-04 19:46:11 -07:00
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>
2026-06-04 10:30:35 -07:00
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>
2026-06-04 08:30:06 -07:00
Eldoon Nemar 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`
2026-06-04 06:53:30 -07:00
Eldoon Nemar 2e70bcb671 UI accent partial fix for issue #1528 (#1530)
Made the suggested changes as listed in the fix path provided by
@Kpa-clawbot

Fix path:

`style.css:1244` `.field-table .section-row td` → `color: var(--text)`
(or new `--section-header-fg`).
`style.css:2620-2631` `.copy-link-btn` → `color: var(--text);`
background/border via `--accent-bg` / `--accent-border` tokens with safe
defaults.
`live.css:987` `.vcr-scope-btn.active` → same token swap; ensure text
remains `--text` on the tinted bg.
`nodes.js:212` `.multibyte-badge` → move inline styles to style.css,
`color:var(--text)`, keep `--accent-bg` background.

When creating the defaults for `--accent-bg` and `--accent-border`, I
chose to go with the default style values embedded in nodes.js, as that
was the safest bet.

We should probably extend the custom themes to include these variables
as well as not to confuse users if they see it. This also causes the
delima of, sometimes the `--accent` is use as the background for
objects, and not `--accent-bg`, example:
`btn active` has background set to` --accent` and border set to
`--accent`.

If we don't extend the config to accept accent-bg and accent-border, we
risk users still making accents of light blue that will be drown out
with the defaults we've set.


Also updated the badge above the multi-byte badge that contains X bytes
of the nodes public key, where X is determined by the path byte length.
This was done because it had styles set that were easy to add to the
styles.css file, to clean up coe. The node-type badge above it is
unfortunately driven by javascript in the nodes.js page, and requires
syling.

**Note:** Accidentally added ghost changes into this push for a second
time. They can be ignored as they were previously merged and shouldn't
have been seen as new.
2026-06-02 12:54:11 -07:00
Eldoon Nemar 0273f1546e fix(live/ui): Fixed a nav-right pin bug (#1526)
## Summary
Fixes a visual bug on the Live page where the navigation bar layout
would break, causing the right-side icons (search, theme toggle,
hamburger menu) to be pushed into the middle of the screen.

## Cause
The Live page dynamically injects a "📌" button to let users lock the
auto-hiding header. However, `live.js` was appending this button as a
direct child of the outer `.nav-bar` container.

Because `.nav-bar` uses flexbox with `justify-content: space-between` to
separate the left, center, and right sections, adding a 4th top-level
child threw off the distribution of space, squeezing `.nav-right` toward
the center.

## Changes
- **DOM Placement (`live.js`)**: Modified the injection logic to target
`.nav-right` and use `appendChild()` so the pin button is cleanly nested
at the far right of the existing right-side cluster (past the hamburger
menu).
- **CSS Cleanup (`live.css`)**: Removed `margin-left: auto;` from
`.nav-pin-btn` as it is no longer necessary and could cause spacing
issues inside the `.nav-right` flex container.

## Verification
- Verified the pin button renders seamlessly on the far right of the
Live page.
- Confirmed the outer `.nav-bar` layout strictly maintains its
left/center/right alignment.
- Confirmed there are no test regressions (the E2E test
`test-issue-1510-live-nav-pin-e2e.js` selects by ID and continues to
pass flawlessly).
2026-06-01 20:53:33 -07:00
efitenandClaude Sonnet 4.6 24a840d199 fix(nodes): align --card-bg with --surface-2 in dark mode — low-contrast card fix (#1470) (#1517)
## Problem

In dark mode, `.node-full-card` and `.node-stats-table` (and all other
`var(--card-bg)` consumers) rendered with a background only ~11 RGB
units away from the page background:

- Page bg: `--surface-0` = `#0f0f23` (RGB 15,15,35)  
- Card bg: `--surface-1` = `#1a1a2e` (RGB 26,26,46)  
- Delta: ~11 units per channel → appears near-white on
OLED/high-contrast LCD screens

## Fix

Align `--card-bg` to `--surface-2` (`#232340`) in dark mode — the same
value already used for `--detail-bg` throughout the app. Delta from page
bg increases to ~35 units per channel, which reads clearly as an
elevated dark surface rather than a washed-out off-white card.

Both dark-mode variable blocks updated in sync (`@media
prefers-color-scheme: dark` + `[data-theme="dark"]`). Light mode is
unchanged.

## Impact

All `var(--card-bg)` consumers in dark mode get the corrected colour:
node full cards, stats tables, analytics cards, packet detail panels,
dropdowns, etc. The value now matches `--detail-bg` so cards and detail
panels use a consistent surface colour.

Closes #1470.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:55:27 -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 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
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-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-clawbotandopenclaw-bot e11ce54059 fix(#1480): update E2E #534 to click navbar mirror; simplify CSS (#1484)
Sequence of errors:
- #1475: hid in-page button with visibility:hidden \u2192 Playwright
won't click visibility:hidden \u2192 broke E2E #534
- #1482: tried opacity:0 instead \u2192 Playwright won't click opacity:0
either \u2192 still broken
- This PR: UPDATE THE TEST instead of fighting Playwright. The mobile UX
since #1471 is: operator-visible Filters control = navbar mirror
(.filter-toggle-btn-mirror). The test should click THAT, not the
now-hidden in-page button.

Test now tries the mirror first, falls back to in-page button for any
test rig without the mirror script. CSS simplified to display:none.

Unblocks #1480 (#1478 naive-TS observer UI surface) CI. Also any other
PR inheriting this same regression.

Hot-deploy candidate (CSS + test only).

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-29 07:38:20 +00:00
Kpa-clawbotandopenclaw-bot b6e005009c fix(#1475 followup): opacity:0 not visibility:hidden so E2E #534 click works (#1482)
Regression I introduced in #1475. Playwright's elementHandle.click()
refuses to act on elements with visibility:hidden — the in-page Filters
button became unclickable, breaking E2E test #534 'Mobile filter toggle
expands filter bar on packets page'.

Caught by CI on #1480.

Switch to opacity:0 + 0×0 + position:absolute. Element renders zero
pixels for the user but stays 'visible' per Playwright's actionability
check — E2E #534 click works, no duplicate Filters button visible.

Hot-deploy candidate (CSS-only).

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-29 07:15:43 +00:00
Kpa-clawbotandopenclaw-bot f0da38f435 fix(#1471 followup): hide duplicate in-page Filters button on mobile (#1475)
**Problem:** Operator on prod reports two Filters buttons rendering on
mobile — the navbar mirror (#1467/#1471) AND the original
`.filter-toggle-btn` inside `.filter-bar`. Both are clickable, both
toggle filters, confusing UI.

**Root cause:** Commit `f88c413d` from #1471 deliberately kept
`.filter-bar` visible to satisfy E2E #534 (which queries
`.filter-toggle-btn` and clicks it). The in-page button stayed
display:flex while the navbar mirror was added — duplicate.

**Fix:** Switch the in-page button to `visibility: hidden` + 0×0 size +
`position: absolute` on mobile. Element stays in DOM,
`page.$('.filter-toggle-btn').click()` still works (visibility:hidden
elements are clickable in Playwright), but takes zero visual space.
Navbar mirror is the visible affordance.

**Test:** existing E2E #534 should pass unchanged (verifiable by running
test-e2e-playwright.js locally after this lands).

Hot-deployable (CSS only).

Closes the regression introduced in #1471.

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-29 03:57:12 +00:00
Kpa-clawbotandclawbot 93b2f4b6bb fix(#1473): treat 0x00 and 0xFF as reserved prefixes (matrix + generator) (#1474)
## Summary

Two CoreScope surfaces treated `0x00` and `0xFF` as ordinary node
prefixes, but the MeshCore firmware actively rerolls any identity whose
public-key first byte is `0x00` or `0xFF` (see
[`examples/simple_repeater/main.cpp:64`](https://github.com/meshcore-dev/MeshCore/blob/6b52fb32301c273fc78d96183501eb23ad33c5bb/examples/simple_repeater/main.cpp#L64)):

```cpp
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00
                   || the_mesh.self_id.pub_key[0] == 0xFF)) {
  // reserved id hashes
  the_mesh.self_id = radio_new_identity(); count++;
}
```

As a result the analyzer was steering new operators toward identities
the firmware will silently refuse — `0xFF` is also used as a wildcard
flood marker in parts of the routing flow, so this isn't cosmetic.

Reporter: **@halo779** (community).

## What this PR does

* **`public/prefix-reserved.js`** — small new module, single source of
truth. Exposes `isReservedPrefix`, `filterReserved`, `reservedCount`,
`markReservedCells`. Firmware citation lives in the file header.
* **Hash matrix (1-byte view)** — cells `00` and `FF` get the
`.prefix-reserved` class, lose `.hash-active` so the matrix click
handler skips them, and pick up an `aria-disabled` + a tooltip
explaining why.
* **Prefix generator** — random sampling, enumeration fallback, and the
"available count" all filter out reserved prefixes. A visible note under
the generator card cites `simple_repeater/main.cpp:64` directly.
* **Prefix checker** — pasting a reserved prefix or full pubkey now
surfaces a red `⚠️ Reserved prefix` alert above the per-tier breakdown.
* **`public/style.css`** — `.prefix-reserved` greys + strikes through
the cell and sets `pointer-events: none`.
* **`public/index.html`** — loads `prefix-reserved.js` before
`analytics.js`.

## Tests

Red-then-green visible in commit history:
* `test-issue-1473-reserved-prefixes.js` — `isReservedPrefix()`
semantics (case + multi-byte) and `markReservedCells()` behavior on a
mock 256-cell matrix.
* `test-issue-1473-prefix-generator.js` — `filterReserved`,
`reservedCount` per byte length, RNG-bias simulator showing the
generator never returns a reserved prefix, enumeration-first-free skips
`00`, and an assertion that `analytics.js` actually wires
`PrefixReserved` into the generator.

Both added to `test-all.sh`.

Fixes #1473

---------

Co-authored-by: clawbot <bot@openclaw.invalid>
2026-05-28 18:43:03 -07:00
Kpa-clawbotandopenclaw-bot d964c27964 feat(mobile): packets UX overhaul + nav surface + map inset + channel synthesis fixes (#1471)
## Summary

Mobile UX overhaul for the packets surface plus two discoverable defects
found along the way. All UI changes are mobile-only (`@media (max-width:
900px)` or `isMobile()` gates) — desktop unchanged.

## Closes
- #1415 — packets layout cross-viewport jank
- #1458 — Tufte mobile packets critique (P0s)
- #1461 — Tufte v2 mobile packets critique (P0/P1)
- #1467 — Favorites/Search/Customize unreachable on mobile
- #1468 — client-side "unknown" channel synthesis
- #1470 — node-detail map inset doesn't honor customizer dark provider

## Commits

1. `fix(#1468): drop client-side "unknown" channel synthesis` —
`channels.js`
2. `feat(#1470): node-detail map inset honors customizer dark-tile
provider` — `nodes.js`, `roles.js`
3. `feat(mobile): packets UX overhaul + bottom-nav More controls (#1415,
#1458, #1461, #1467)` — `style.css`, `index.html`,
`mobile-page-actions.js` (new)

## Mobile-list view changes
- Kill empty chevron rail
- Slim sticky THEAD (24px, retains sort affordance per operator
preference)
- Hide entire page-header on mobile
- Mirror pause + Filters pill into navbar via new
`mobile-page-actions.js`
- Convert group-header `toggle-select` → `select-hash` on mobile (no
dead-end expand)

## Mobile detail-panel changes
- Drop redundant src→dst line (identity already in sticky header)
- Hide boxed "decoded message" duplication card
- Hide PAYLOAD TYPE row (already in header badge)
- 2-col label/value grid (cuts panel height ~40%)
- Sticky in-sheet header for packet identity
- Kill iOS-style drag handle (conflicts with browser pull-to-refresh)
- Make ✕ close visible + always reachable
- Outer sheet `overflow:hidden`, inner content `overflow-y:auto`
(scrollable region distinct, scrollbar visible)
- Bottom-nav clearance (`padding-bottom: 60px`)
- Close detail sheet on route change away from /packets
- Tap-to-toast popovers for score tooltips (`title=` doesn't fire on
touch)

## Mobile nav surface
- Mirror Favorites ⭐ / Search 🔍 / Customize 🎨 into bottom-nav More sheet
(#1467)
- Brand stays in top nav; per-page controls (pause, Filters) injected
into `.nav-left`

## Other fixes shipped together
- **#1468**: drop CHAN messages with no decoded channel name (eliminates
fake "unknown" channel row)
- **#1470**: `_applyTilesToNodeMap` helper — node-detail inset map reads
from `MC_TILE_PROVIDERS[active]` instead of hardcoded OSM; honors
customizer's dark-tile provider pick + applies invert filter for
inverted variants
- `getTileUrl()` + new `getActiveTileProvider()` in `roles.js` now
consult `MC_TILE_PROVIDERS`

## CDP verification (local chromium)

Tested on staging at viewport 390×844 + 1206×928.

| Surface | Before | After |
|---|---|---|
| Chrome above first data row | 231px (27% viewport) | ~80px (10%
viewport) |
| Packets visible above fold | 10 | 17 |
| Detail panel duplications | 3× identity | 1× (header only) |
| Mobile group-expand UX | dead-end (no chevron) | converts to
select-hash |
| Score tooltips on touch | broken (title= silent) | tap → toast popover
|
| Node detail map inset (dark mode) | always OSM light tiles | honors
customizer provider + invert filter |
| Bottom-nav More controls | Dark mode only | + Favorites, Search,
Customize |

## What's NOT in this PR
- Paths-through-node sort fix lives in #1431 (parallel PR for #1145)
- Detail-panel hex byte-grid behind disclosure — operator wants it;
follow-up
- Group-header row sizing (some render 200–700px tall) — existing
behavior, follow-up

## Test plan
- [ ] Existing frontend tests stay green
(`test-issue-1415-packets-layout.js`,
`test-issue-1420-tile-providers.js`,
`test-issue-1454-channels-toggle.js` all pass locally on this branch)
- [ ] Existing Playwright E2E stays green
- [ ] CDP on local chromium: 390×844 mobile + 1024×768 tablet + 1440×900
desktop — no regressions

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-28 16:11:25 -07:00
Kpa-clawbotandopenclaw-bot b2d654bf61 fix(#1415, #1458): packets layout + mobile chrome + semantic-first detail (#1459)
## Closes #1415 — packets cross-viewport jank
## Closes #1458 — Tufte mobile-packets P0 findings (folded into same
branch)

Single PR covers both issues — they touch the same files
(`public/packets.js`,
`public/style.css`) and a split would invite merge thrash.

### #1415 — column priority + chrome compaction

Locked column-priority tiers (operator spec):

| Tier | Viewport | Columns |
|---|---|---|
| 1 | always (mobile through desktop) | expand · time · type · details |
| 2 | tablet+ (>768px) | path |
| 3 | desktop only (>1024px) | hash · observer · rpt |

Enforced via existing `data-priority` system in `TableResponsive.apply`
(priorities 3 → hide ≤1024, 5 → hide ≤768).

CSS:
- `.col-expand` pinned to `width/min-width/max-width: 32px` at every
viewport
  — kills the 50–180px dead column that pushed every data column right.
- `.col-details` capped at `max-width: 480px` so wide viewports stop
wasting
  hundreds of px on the last column.
- `@media (max-width: 480px)` hides page-header BYOP, shrinks the h2,
and
  tightens row padding → pre-table chrome drops from ~280px to ~140px.

### #1458 — Tufte mobile P0 findings

**P0-A: semantic-first detail panel.** Was: `"Packet Byte Breakdown (134
bytes)"`
title + giant neon hex grid above the meaningful fields. Now: type badge
+
decoded summary + hop count + `src → dst` lead the panel, followed by
the
existing `.detail-meta` dl (reordered: Payload Type → Path → Timestamp →
Observer).

**P0-B: raw-bytes disclosure.** Hex legend / hex dump / field table
wrapped in
`<details class="detail-technical">`. Disclosure copy reads "Show raw
bytes".
Collapsed by default on phones (`window.innerWidth ≤ 480`), expanded on
tablet+.

**P0-C: mobile filter-zone collapse.** The always-on filter-expression
input
above `.filter-bar` is now wrapped with `.pkt-filter-expr` and hidden
under
the `@media (max-width: 480px)` block. Reveals when the existing
"Filters ▾"
toggle adds `.filters-expanded` to the sibling `.filter-bar` (CSS
`:has()`
selector — one tap reveals both chrome rows together).

### TDD

`test-issue-1415-packets-layout.js` — pure source-grep, no browser:
- col-expand class on first `<th>` + `<td>` + CSS 32px pin
- locked column-priority tier values per column
- `.col-details` max-width ≤ 480px
- mobile @media block: hides BYOP, hides `.pkt-filter-expr` (revealed by
  `.filters-expanded`)
- detail-meta order: Payload Type before Observer
- `<details class="detail-technical">` wrapper exists with "Show raw
bytes"
  summary
- detail-title leads with a type badge; `.detail-srcdst` emitted
- old "Packet Byte Breakdown (N bytes)" title literal removed

Red commit `d4372d82` (8 assertion failures, no compile errors), green
commit `4fab9dbd` (#1415 work), follow-up commit `a5218035` (#1458 work)
keeps everything green. 26 assertions, 0 failed.

---------

Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-28 05:38:28 -07:00
Kpa-clawbotandopenclaw-bot 7abe2dd56b fix(#1065): remove stray CSS-eater text that killed .gesture-hint parent rule (#1453)
After #1452 merged with width:fit-content + max-width on .gesture-hint,
CDP showed the rule was still missing from CSSOM. Tracked it down to
line 4024 of style.css which had a raw '(feat(#1062): green — implement
gesture system)' string OUTSIDE any comment, after the #1062 closing
marker. The parser ate forward through the .gesture-hint parent rule.

One-character fix removes the parenthesized commit fragment. Verified
via CDP: rule now appears in CSSOM and width:fit-content takes effect.

Final follow-up to #1452.

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-28 06:32:21 +00:00
Kpa-clawbotandopenclaw-bot 58282c91d8 fix(#1065): gesture hints touch-gate + width:fit-content + CSS-parse safety (#1452)
## Summary
Three follow-up fixes for #1065 gesture-hint discoverability:

1. **Touch-capability gate.** New `hasTouchCapability()` helper probes
`'ontouchstart' in window`, `navigator.maxTouchPoints`, and `(pointer:
coarse)`. Every `HINTS[*].relevant()` predicate now returns `false`
immediately on mouse-only viewports, so desktop browsers no longer get
"swipe a row left" tips.
2. **`width: fit-content` on the pill wrap.** The `.gesture-hint` block
previously had no explicit width and defaulted to block-level
full-width. Combined with `translateX(-50%)` on `.gesture-hint-bottom`
this rendered as a 100vw-wide bar centered with a negative-X transform,
i.e. pushed off-screen-left on narrow viewports (384px wrap on 390px
viewport).
3. **CSS-parse safety.** Moved the in-body comment (which contained an
em-dash) outside the rule block. An earlier attempt to add `width:
fit-content` together with an in-body em-dash comment caused the parent
`.gesture-hint` rule to vanish from the CSSOM in Chrome (children
`.gesture-hint-*` remained). Putting the comment above the block
sidesteps the parser bug.

## Test
`test-issue-1065-gesture-hints-gates.js` — pure source-file assertions,
no browser required. Red commit first (7 fails), green commit second
(10/10 pass). Wired into `test-all.sh`.

## Verification
After hot-deploy on staging:
- Desktop (no touch):
`document.querySelectorAll('.gesture-hint').length` === 0
- Mobile emulated (touch): hint rendered, `getBoundingClientRect().x >=
0`, `width <= 360`, `width < viewport_width`
- CSSOM: parent `.gesture-hint` rule present with `width: fit-content` +
`max-width: 360px`

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-27 23:21:18 -07:00
Kpa-clawbotandopenclaw-bot b5a1642024 fix(#1450): preserve custom logo aspect ratio (svg/img CSS split) (#1451)
## Summary
Custom navbar logos via `branding.logoUrl` were rendered squished. The
CSS rule `.brand-logo { width: 125px }` was pinned to the default
inline-SVG wordmark's viewBox aspect (~3.08:1), and when customize-v2
swapped the inline `<svg>` for an `<img>`, that `<img>` inherited the
same fixed 125px width — stretching every non-3.08:1 image into a pill.

## Root cause
- `public/style.css:520` — `.brand-logo { width: 125px }` applied
regardless of element type.
- `public/customize-v2.js:75-77` — `_setBrandLogoUrl` additionally
hardcoded `width="125" height="36"` attributes on the created `<img>`,
overriding any CSS aspect rescue.
- Mobile media query (`style.css:1729`) had the same issue with `width:
112px`.

## Fix
Split the CSS rule by element type:
- `svg.brand-logo` — keeps 125×36 pin for the default wordmark (no
regression).
- `img.brand-logo` — `width: auto`, `max-width: 200px`, `object-fit:
contain` so the operator image's natural aspect is preserved with a sane
cap so very-wide logos can't blow nav layout.
- Mobile `@media` mirrors the split (svg 112×32 pinned, img auto width
with 180px cap).
- Drop the hardcoded `width=125`/`height=36` attrs from the `<img>`
created in `customize-v2 _setBrandLogoUrl`.

## TDD
Red commit `a20b7d7`: 4 assertions, all fail on master.
Green commit `533f464`: same 4 assertions, all pass.

```
✓ img.brand-logo CSS rule exists and uses width:auto (not pinned)
✓ svg.brand-logo CSS rule still pins width:125px (no default regression)
✓ mobile media-query splits the .brand-logo rule into svg/img variants
✓ customize-v2 _setBrandLogoUrl does NOT hardcode width/height attrs on the IMG
```

## Verification plan post-merge
Hot-deploy to staging and CDP-verify:
1. Default SVG wordmark still renders at 125×36 (no default regression).
2. Square 100×100 data-URI logo renders as ~36×36 (was 125×36 pill).
3. Tall 100×300 data-URI logo renders as ~12×36 (was 125×36 pill).

Closes #1450

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-27 22:42:53 -07:00
Kpa-clawbotandopenclaw-bot ddf14d1954 feat(#1446): CB preset is an end-user opt-in (closes #1446, fixes #1444 cascade) (#1447)
## Summary

Reframes the CB-preset feature as an **end-user opt-in** layered above
operator
config — not the canonical color source for the app. Implements the
cascade
defined in #1446's acceptance test and fixes the #1444 cascade trap as a
side effect.

**Cascade (top wins):**

```
user per-role override  >  active CB preset  >  server config.nodeColors  >  built-in :root defaults
```

Red commit: f59c0c5e (8 scenarios, 9 assertions red on master)
Green commit: 21f9b80c (all 16 assertions pass; reverting any one of the
four
source files brings the test back red).

## Changes

| File | What |
|---|---|
| `cb-presets.js` | `currentPreset()` returns `null` on no-stored-preset
(was `'default'`). `initFromStorage()` no longer auto-applies Wong cold.
New `clearPreset()` API. |
| `style.css` | Drop the `body[data-cb-preset="default"]` block. Wong
remains `:root` baseline; that block was masking server config in the
"no preset" state. |
| `roles.js` | `setRoleColorOverride` writes to `body.style` with
`!important` so user picks win on equal-specificity cascade against
`body[data-cb-preset="X"]` (root cause of #1444). |
| `customize-v2.js` | `applyCSS`: when no preset active, server-config
nodeColors get `--mc-role-{role}` too. UI re-ordered (Node Role Colors
first, preset section labelled "Optional"). Wires `cb-preset-changed`
listener so `clearPreset()` re-applies server config live. |

## Backward compat

- Visitors with a stored CB preset in localStorage continue to see it on
load.
- Visitors without one: now see operator's `config.json` colors (or
built-in
Wong if config has no `nodeColors`). Visually identical for default
deploys.

## Acceptance scenarios (verified in
`test-issue-1446-cb-preset-cascade.js`)

1. Cold boot, no localStorage → no `data-cb-preset` attr, no
`--mc-role-*` clamp
2. Server `nodeColors.repeater = #aaaaaa`, no preset →
`--mc-role-repeater = #aaaaaa`
3. User picks `#ff00ff` while `deut` active → body inline `!important`
wins
4. Clear override while `deut` active → reverts to `#FE6100` (deut)
5. Clear preset (server config present) → reverts to server config
6. Stored preset auto-applies on boot (backward compat)
7. Customizer UI: Node Role Colors block precedes preset block
8. `style.css`: no body data-cb-preset rule re-defines Wong (would mask
server)

Post-merge CDP verification on staging will run the 5 issue-acceptance
scenarios.

Closes #1446
Fixes #1444 (cascade)

E2E assertion added: `test-issue-1446-cb-preset-cascade.js:124`
(scenario 3 — user override beats active preset on body inline with
!important).
Browser verified: pending hot-deploy + CDP run post-merge (per task
brief).

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-27 20:24:58 -07:00
Kpa-clawbotandopenclaw-bot 89410d58b4 fix(#1413): nav-left + nav-stats overlap at vw~1200 — flex sizing fix (#1417)
## What

Fix the horizontal overlap between `.nav-more-btn` (in `.nav-left`) and
`.nav-stats` (in `.nav-right`) at viewport widths roughly 1101..1599px.
At vw=1200 the count number in the stats badge rendered on top of the
"More ▾" text.

## Root cause

`.top-nav` uses `display: flex; justify-content: space-between;` but had
**no column gap** between its children, and `.nav-links` had **no
flex-grow**. So `.nav-left` only consumed its content's intrinsic width
and `.nav-right` (with `flex-shrink: 0`) was free to abut it. Worse, the
Priority+ measurement loop in `app.js` (`applyNavPriority` → `fits()`)
compared intrinsic widths against `window.innerWidth` while `.top-nav {
overflow: hidden }` masked the actual collision — so the loop happily
declared "fits" while pixels overlapped.

CDP measurement on master at vw=1200 (`/#/packets`):

- `.nav-more-btn` rect: x=499..557 (w=58)
- `.nav-stats` rect: x=496..962 (w=466)
- Gap: **−60.7px** (overlapping)

Fix candidates tested via Chrome DevTools Protocol (`Runtime.evaluate` +
`Emulation.setDeviceMetricsOverride`) across vw=1101, 1200, 1366, 1440,
1600, 1920 (plus 768, 900, 1024, 1080, 1100, 1300, 1500, 1700, 1800 as a
sanity sweep). Winner:

```css
.top-nav   { column-gap: 16px; }
.nav-links { flex: 1 1 auto; min-width: 0; }
```

Per-viewport gap (`stats.left - more.right`) baseline → fix:

| vw   | baseline | fix      |
|------|----------|----------|
| 1101 | −144.0   | **16.0** |
| 1200 |  −60.7   | **16.0** |
| 1300 |    8.4   | **16.0** |
| 1366 |   64.2   | 64.2     |
| 1440 |    0.0   | **44.5** |
| 1600 |   24.2   | 24.2     |
| 1920 | more hidden (no overflow) — n/a | n/a |

Single-candidate variants (`.nav-left { flex: 1 1 auto }` alone,
`.top-nav { justify-content: space-between }` alone — already on, no
effect, `.nav-links { flex: 1 1 auto }` alone, margin/padding hacks on
`.nav-right`/`.nav-stats`) all still produced ≤8px gap at vw=1200. Only
the combo (column-gap on parent + flex-grow on `.nav-links`) cleanly
resolves all six required widths.

## TDD

Red commit: `3d374b4c93319805e89e46d8fdc8a8ea8c6c1479` (CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26482870401)

- `test-issue-1413-nav-overlap-e2e.js` — Playwright at vw 1101, 1200,
1366, 1440, 1600, 1920 on `/#/packets`. Asserts `.nav-more-btn.right + 8
<= .nav-stats.left` (when both visible) and that `.top-nav` does not
horizontally scroll. Wired into `.github/workflows/deploy.yml` alongside
the other `test-nav-*-e2e.js` entries.
- Red commit ships ONLY the test (+workflow line); CI fails on the
assertion at vw=1101..1300 and vw=1440 (gap below 8px threshold).
- Green commit applies the two CSS rules above and turns CI green.

## Manual verification

1. Open `http://analyzer-stg.00id.net/#/packets` in a desktop browser.
2. Resize the viewport to ~1200px wide.
3. Confirm the "More ▾" button and the stats badge are visibly separated
(≥16px gap) and the badge count is not stacked on the button text.
4. Repeat at 1101, 1300, 1440, 1600, 1920px — gap ≥16px at all widths
where stats is visible.
5. At ≤1100px confirm `.nav-stats` is still hidden (display:none,
unchanged).

## Scope guards

- No changes to the Priority+ algorithm (`applyNavPriority` / `fits()`
in `app.js`). #1391, #1311, #1139, #1148, #1102, #1055 logic untouched.
- No changes to the More dropdown (`position: fixed`, #1406).
- No changes to `.nav-left { overflow }` (#1405 stayed dropped).
- Mobile (<768px) hamburger layout unchanged.

Fixes #1413

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-26 17:38:47 -07:00
Kpa-clawbotandopenclaw-bot 52b6dd82ac fix(#1407): cb-preset propagation via live ROLE_COLORS getter + per-role text color for WCAG AA (#1408)
WIP — RED commit only. Tests demonstrate two bugs from #1407:

1. `window.ROLE_COLORS` is a static literal (legacy April palette), not
synced to `--mc-role-*` CSS vars.
2. Achromat preset pairs `#1a1a1a` text with 3 dark grays → WCAG 1.4.3
fails (1.27 / 2.55 / 4.43).

Expect CI red on `test-issue-1407-cb-preset-propagation.js` assertion
failures (not compile errors). GREEN follows.

Refs #1407

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-26 16:42:47 -07:00
Kpa-clawbotandopenclaw-bot 9b0a4ee054 fix(nav): .nav-more-wrap contain:layout — open dropdown inflated parent flex line, clipped nav offscreen (#1406)
ACTUAL root cause of the recurring nav-vanishing bug, validated live via
Chrome CDP probe on staging at vw=1030.

## What happens

When the More dropdown opens:
- BEFORE: nav_links.y = 2.67, nav_left.scrollHeight = 47, nav visible ✅
- OPEN: nav_links.y = -46.67, nav_left.scrollHeight = 279, nav clipped
offscreen ❌

The .nav-more-menu is position:absolute but its content extents inflate
.nav-more-wrap.scrollHeight. .nav-left { display:flex;
align-items:center } then centers a 279px content line in a 52px
container, putting everything above the visible band.

## Fix

Add contain:layout to .nav-more-wrap — isolates its layout box from the
parent flex calculation. No more bubble-up.

CDP verification with the fix applied: dropdown opens, all 6 items
render at proper y (56, 93, 130, 166, 203, 240), nav_links_y stays at
2.67, nav_left.scrollHeight stays at 47.

## Why prior 22 fixes didn't catch it

Every prior fix treated symptoms — Priority+ algorithm tweaks, overflow
flag toggles, min-height drops, etc. None instrumented the CLOSED→OPEN
state transition that reveals the flex-line bug. Required Chrome
DevTools Protocol on a real broken viewport to see the inflate happen
live.

Fixes #1406 and likely supersedes #1391, #1396, #1400, #1404.

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-26 23:03:32 +00:00
Kpa-clawbotandopenclaw-bot ae77d58ec5 fix(#1403): drop .nav-left overflow:hidden — root cause of nav vanishing + truncated More dropdown (#1405)
Root cause of the recurring nav-vanishing family of bugs — confirmed
live via operator console probe at vw=1030 on /#/channels (also
reproduces on /#/home, /#/packets, all routes).

## Symptoms

1. All `.nav-links` (Home, Packets, Map, Live, Channels, Nodes) and
brand + More button render OFFSCREEN above the visible top-nav band.
`.nav-left` reports y=0..52 but every child reports y=-47.5.
2. More dropdown when opened shows only ONE item ("Tools") instead of
the 6 expected (Channels, Tools, Observers, Analytics, Perf, Audio Lab).

## Root cause

`.nav-left { overflow: hidden }` at `public/style.css:509`. With flex
children whose effective layout exceeds the container box, Firefox clips
children to negative y. The same `overflow: hidden` ALSO clips the
descendant `.nav-more-menu` dropdown contents.

## Fix

Drop `overflow: hidden` from `.nav-left`. The original
horizontal-overflow guard from #1066 is preserved at the `.top-nav`
level (which still has `overflow: hidden`).

## Verification

Operator console probe after applying the same `overflow: visible`
in-page:
- All 6 visible nav links render at y >= 0 inside the top-nav.
- More dropdown contains all 6 expected items (Channels, Tools,
Observers, Analytics, Perf, Lab).
- Both bugs collapse into ONE root cause.

## Why prior fixes didn't catch this

- #1400 fixed `.nav-link { min-height: 48px }` overflow — reduced
children from 56px to 47px tall. Helped slightly but didn't address the
`.nav-left { overflow: hidden }` interaction.
- #1391, #1394 fixed the active-pill-in-overflow algorithm. Different
layer.
- #1311, #1148, #1106, #1102, #1097, #1067, #1055 — every prior
Priority+ fix treated overflow as an algorithmic question, never as a
CSS clipping bug at the container level.

22nd nav fix in this saga. This one targets the actual cause.

Refs #1391, #1396, #1400. Operator probe transcript available on
request.

Fixes #1403

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-26 19:37:51 +00:00