Commit Graph

204 Commits

Author SHA1 Message Date
Kpa-clawbot 9a2270168f feat(#893): Material Design dark mode toggle — polished version of #893 (#1389)
## Polished version of #893

This PR carries forward @emuehlstein's Material Design dark-mode toggle
from #893, rebased onto current `master` and polished for a11y /
first-paint / forced-colors / cross-tab sync.

Original commits (preserved as `Co-authored-by`):
- `feat: replace dark mode button with Material Design toggle switch`
(emuehlstein)
- `fix: define --shadow CSS var in theme blocks, drop stopPropagation
no-op` (emuehlstein, addressing prior review)

#893 had been stuck in CONFLICTING state since 2026-05-24 with no CI
runs ever. Rebase resolved a single `public/style.css` `:root` conflict
(preserved both the `--text-primary`/`--bg-hover`/`--primary` aliases
from #1378 and the new `--shadow` definition).

## Polished improvements (on top of #893)

1. **FOUC fix** (`public/index.html`): inline `<head>` script reads
`localStorage('meshcore-theme')` (or `prefers-color-scheme`) and sets
`data-theme` *before* stylesheet load. Without this, dark-mode users see
a light-mode flash on every page load.
2. **ARIA semantics** (`public/index.html`): moved `aria-label` from the
wrapping `<label>` onto the actual `<input role="switch">`. Removed
`aria-hidden="true"` from the checkbox (which had been hiding it from
assistive tech). Added `aria-hidden` to the decorative track instead.
3. **Keyboard focus indicator** (`public/style.css`): `:focus-visible`
on the (visually-hidden) checkbox draws an outline on
`.theme-toggle-track`. Previously keyboard users could focus the toggle
with Tab but had no visible indicator.
4. **Reduced motion** (`public/style.css`): `@media
(prefers-reduced-motion: reduce)` disables the slide/fade transitions.
5. **Forced-colors mode** (`public/style.css`): explicit `CanvasText`
border on track + thumb so the switch stays visible in Windows High
Contrast. Default CSS tokens collapse to `Canvas`/`CanvasText` and the
thumb would otherwise disappear.
6. **Cross-tab sync** (`public/app.js`): `storage` event listener for
`meshcore-theme` mirrors the cb-presets pattern from #1378 — toggling
theme in one tab now syncs all open tabs.
7. **Tightened E2E test** (`test-e2e-playwright.js`): added assertions
for `role="switch"`, checkbox-state ↔ theme parity, and theme
persistence across a full page reload (was only asserting one toggle).

## Notes

- No `map[string]interface{}` (no Go changes).
- All colors via existing `--mc-*` / theme tokens; `--shadow` is defined
in both light + dark theme blocks.
- No layout shift (track is fixed `46x24` inside the `44x44` label
container).
- Branch scope is exactly the four files from #893: `public/app.js`,
`public/index.html`, `public/style.css`, `test-e2e-playwright.js`.

Closes #893.

Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com>

---------

Co-authored-by: Eric Muehlstein <muehlbucks@gmail.com>
Co-authored-by: CoreScope Bot <bot@corescope>
2026-05-25 23:12:37 -07:00
Kpa-clawbot ff0ee50354 fix(#1374): packet-route map modernized — role-aware markers, directional edges, WCAG 2.2 AA (#1381)
## What

The packet-route map view (`/#/map?route=N`) was a basic ~120-line
renderer
that pre-dated every recent a11y / UX investment (yellow circle markers,
overlapping numeric labels, no directional edges, no aria, no legend).
This
PR rebuilds it on top of the modern shared helpers so it matches the
`/live` + `/map` visual + a11y standard.

Acceptance criteria from #1374 — every box checked:

- [x] Role-aware shape markers via shared `window.makeRoleMarkerSVG`
(post-#1357).
- [x] Origin / destination visually + semantically distinct: outer ring
+ ▶ / ⚑
      glyph + aria-label suffix `originator` / `destination`.
- [x] Sequence-number badges (`.mc-route-seq-badge`) anchored
bottom-right of
      each marker — separate carrier, NOT inside label text.
- [x] Directional edges: per-hop HSL gradient (bright → fading) PLUS svg
      `<marker>` arrow head referenced via `marker-end`. Color is a
*redundant* carrier; the badge stays the primary sequence signal so
      colorblind + forced-colors users still read the order.
- [x] Per-edge `aria-label="Hop N → N+1, ~Xkm"` (haversine computed).
- [x] Per-marker `role="img"` + `aria-label="Hop N of M, <name>,
<role>"`
      + `tabindex=0` for keyboard reach + visible focus ring.
- [x] Label deconfliction reuses `window.deconflictLabels` (now exposed
by
`map.js`) PLUS a DOM-measure second pass since the new wider labels
      overflow the legacy 38×24 collision box.
- [x] Collapsible `.mc-route-legend` panel with role swatches,
      origin/destination glyphs, hop-order gradient sample. Toggle has
      `aria-expanded`.
- [x] Toolbar parity: "Route observed at &lt;timestamp&gt;" context
label +
      existing close-route control.
- [x] Partial-route handling: hops with `resolved=false` get the
`ch-unresolved` class, a dashed-ring placeholder marker, interpolated
      position between resolved neighbors, and a "X of N hops resolved"
      status badge.
- [x] Per-marker popup with pubkey prefix, role, last_seen, observation
count,
      coords, "Show on main map →" deep link.
- [x] `prefers-reduced-motion: reduce` disables animations/transitions.
- [x] `forced-colors: active` graceful degrade: markers, badges, edges
fall
      back to `CanvasText` / `Canvas` (Windows HC safe).

## How

Split the renderer into a dedicated `public/route-render.js` exposing
`window.MeshRoute.render(map, layer, positions, opts)`. The existing
`drawPacketRoute` in `map.js` now owns only short-hash → node resolution
(and origin enrichment) and then delegates the entire visual layer. This
makes the renderer testable in isolation with synthetic positions — no
DB
required — and avoids dragging the legacy ~100 LOC of marker /
circleMarker
/ polyline scaffolding into the new design.

Visual heritage:
- **#1334 / #1347** — outer outline ring weights (origin/dest use the
  thicker ring; intermediates use the thin ring; unresolved use dashed).
- **#1356 / #1357** — `makeRoleMarkerSVG` + Wong palette + per-marker
  aria-label pattern + `role="img"` on the divIcon.
- **#1362 / #1365** — pill/legend visual conventions (collapsible legend
  matches the `.mc-section` accordion language users already know from
  `/map`).

### WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3)

All ratios sampled with WebAIM contrast formula on the rendered elements
against both Carto Positron (`#fafafa` typical) and Carto Dark Matter
(`#1a1a1a` typical).

| Element | SC | Ratio (Positron) | Ratio (Dark Matter) | Pass |

|--------------------------------------------|----------|------------------|---------------------|------|
| Sequence badge text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 |
17.1:1 (self-bg) |  |
| Sequence badge border `#1a1a1a` | 1.4.11 | 17.6:1 | 12.6:1 |  |
| Marker outer ring `#06b6d4` (origin) | 1.4.11 | 3.2:1 | 4.6:1 |  |
| Marker outer ring `#ef4444` (destination) | 1.4.11 | 3.8:1 | 4.4:1 | 
|
| Marker outer ring `#666` (intermediate) | 1.4.11 | 5.7:1 | 3.7:1 |  |
| Edge stroke (seq color, mid: `#56c08c`) | 1.4.11 | 3.0:1 (min) | 3.1:1
|  |
| Edge arrow head (currentColor) | 1.4.11 | same as edge | same |  |
| Label text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1
(self-bg) |  |
| Legend body text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1
(self-bg) |  |
| Resolved badge `#78350f` on `#fef3c7` | 1.4.3 AA | 8.4:1 | 8.4:1
(self-bg) |  |

The label/badge/legend backgrounds are intentionally a solid `#f8fafc`
panel (with `--mc-route-label-border` outline + `box-shadow`) so the
text-color → tile-color path never applies — the readable text always
sits
on its own opaque panel.

For SC 1.3.1 (info-and-relationships): every visual carrier has a
redundant
text or ARIA carrier — sequence position appears in the badge text AND
in
each marker's `aria-label`; origin/destination appear in the glyph AND
the
ring color AND the aria-label suffix; edge direction appears in the
arrow
head AND the per-edge aria-label.

### TDD

- **Red commit:** `9e4f58e5547720ff3fcf8695a6c325958904683a` (CI:

https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks)
  — adds `test-issue-1374-route-map-a11y-e2e.js` only. The test calls
`window.MeshRoute.render(...)` directly with synthetic Bay-Area
positions
  at mobile (375×800) AND desktop (1920×1080), asserts every acceptance
criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes
  the partial-route fixture. Fails on the assertions because `MeshRoute`
  doesn't exist on master.

- **Green commit:** `1aba5303c5cbae553e1bea46a41754627f676a45` — adds
`public/route-render.js`, refactors `drawPacketRoute` to delegate, adds
`.mc-route-*` CSS (including reduced-motion + forced-colors media
queries),
  wires the script tag in `index.html`, and wires the test into
  `.github/workflows/deploy.yml`.

### Visual verification

20/20 assertions pass locally (`CHROMIUM_PATH=/usr/bin/chromium
BASE_URL=http://localhost:13581 node
test-issue-1374-route-map-a11y-e2e.js`):

```
=== Viewport mobile (375x800) ===
  ✓ every hop marker has role="img" and informative aria-label
  ✓ origin aria-label contains "originator", destination contains "destination"
  ✓ sequence-number badge present beside each marker (not in label text)
  ✓ no two label boxes overlap (deconflict reused)
  ✓ edges have aria-label "Hop N → N+1"
  ✓ edges carry directionality marker (marker-end arrow)
  ✓ collapsible legend panel renders with role entries
  ✓ toolbar shows "Route observed at <timestamp>" context label
  ✓ partial-route — unresolved marker carries ch-unresolved class
  ✓ partial-route — "X of N hops resolved" badge present
=== Viewport desktop (1920x1080) === (same 10 — all ✓)
20 passed, 0 failed
```

Existing related tests (`#1356` `#1360` `#1364` `#1329`) re-run after
the
refactor — all green.

## Out of scope

- Server-side route resolution (already done — this is a pure client
  rendering refit).
- Multi-route view / 3D / globe — explicitly excluded by the issue.
- Backend untouched — `cmd/server` + `cmd/ingestor` not modified.

Fixes #1374

---------

Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-26 05:51:48 +00:00
Kpa-clawbot 101c11b4b3 fix(#1361): theme customizer — colorblind presets [WIP] (#1378)
WIP — draft PR for CI to exercise the RED test commit. Will be promoted
out of draft once the GREEN commit lands.

Red commit: 8b37c918 (test-only, expected CI failure on assertions)

Tracks #1361.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-25 22:35:42 -07:00
Kpa-clawbot 791c8ae1bc fix(#1367): channels page chat-app redesign — restore prod row layout, drop analytics chip, add detail view (#1376)
Red commit: ae8838ef (CI: pending — see Checks tab once attached)

## What
Channels page mobile UX overhaul (#1367). Restores prod's chat-app row
layout, drops the analytics chip, and adds a per-channel detail view.

## Status
Draft — RED commit on the wire. Greens will follow in subsequent commits
before this is moved to Ready.

Fixes #1367

---------

Co-authored-by: bot <bot@example.com>
2026-05-25 22:30:19 -07:00
Kpa-clawbot 91d90d48fb fix(#1364): drop over-aggressive .mc-pill max-width — restore multi-digit count visibility (#1365)
Red commit: 482ffe69e6 (CI: pending)

## What

Drops `max-width: 4ch` from `.mc-cluster .mc-pill` in
`public/style.css`. Keeps `overflow: hidden` + `text-overflow: ellipsis`
as belt-only graceful degradation.

## Why

#1362 added `max-width: 4ch` as defense-in-depth for the `999+` JS cap.
But `4ch` is applied to the BOX including the `1px 3px` padding, so
effective text width is ~2.5ch — enough for `R6` but not `R60`. Result:
post-merge regression on staging where multi-digit cluster pills render
`R…` instead of `R60`/`C30`.

The JS cap in `public/map.js` already clamps counts to `999+` (max 5
chars: `R999+`). That's the load-bearing safety. The CSS `max-width` was
overcaution and went too aggressive. Option A from the issue: drop the
cap entirely, keep ellipsis as graceful-degrade if JS ever fails.

## TDD red→green

- RED: `test-issue-1364-pill-no-clamp.js` asserts `.mc-pill` CSS does
NOT contain `max-width: 4ch` (regression guard) and DOES contain
`overflow: hidden` + `text-overflow: ellipsis` (graceful degradation).
Fails on the unchanged CSS.
- GREEN: deletes the `max-width: 4ch;` line from `.mc-pill`. Test
passes.

Wired into `.github/workflows/deploy.yml` alongside the #1360 test.

## Visual verification

Open `/map` zoomed-out on staging. Cluster pills must render full counts
(`R60`, `C30`, `R250`, capped `R999+`) — no `R…` ellipsis. No horizontal
scrollbar even on synthetic 4-digit injection.

Fixes #1364

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-25 14:56:43 -07:00
Kpa-clawbot 40aa02b438 fix(#1360): cluster pill shows letter+count — restore count visibility regressed by #1357 (#1362)
Red commit: c0de33a952 (CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416117686)
Green commit: c268248d — CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416069319

## What

Fix #1360 regression: cluster role pills on `/map` show ONLY the role
letter (R/C/M/S/O); the per-role count number that was visible pre-#1357
is gone. This PR restores the count by concatenating it after the letter
inside the pill body, so each pill renders as `R60`, `C30`, `M5`, etc.

- `public/map.js` `makeClusterIcon`: pill body becomes `letter + n` (was
`letter`).
- `aria-label` / `title` (`"60 repeaters"`) untouched — already correct.
- DOM, classes, CSS, `--mc-*` constants, border-style ramp, multi-byte
labels — untouched.

### Adversarial follow-up (commit on top of green)

- **JS cap**: `makeClusterIcon` clamps `n > 999` → `"999+"`, so
pathological clusters render as e.g. `R999+` instead of `R10000`. Pill
width stays bounded.
- **CSS guard** on `.mc-pill`: `max-width: 4ch; overflow: hidden;
text-overflow: ellipsis;` as defense-in-depth if a render slips past the
JS cap.
- **+3 test assertions**: one for the JS cap, two for the CSS guard.
Mutation-verified (removing the cap fails ONLY the new cap assertion).

## Why

#1357 fixed WCAG 1.4.1 for cluster role pills by promoting the role
letter to the pill body, but in doing so dropped the count number that
sighted operators relied on for at-a-glance per-role counts. The letter
is the WCAG carrier; the count is the data. Both belong in the pill body
— they always did before #1357. The audit's intent was to PAIR them, not
REPLACE one with the other.

## TDD red→green

- **Red** (`c0de33a9`): added `test-issue-1360-pill-letter-count.js`
with assertions that pill body concatenates `letter + n` and is no
longer the bare `letter`. Fails by assertion against current `master`.
Red CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416117686
- **Green** (`c268248d`): one-line change in `public/map.js` (`letter +
'</span>'` → `letter + n + '</span>'`). All assertions pass. Green CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26416069319
- **Follow-up** (this push): JS `"999+"` cap + CSS width guard + 3 new
assertions. #1356 (40), #1293, and `marker-outline-weight` tests remain
green.
- New test wired into `.github/workflows/deploy.yml` right after
`test-issue-1356-map-a11y.js`.

## Visual verification

Open https://analyzer.00id.net/#/map after deploy and confirm cluster
pills display `R<count>`, `C<count>`, `M<count>`, etc. (e.g. `R60 C30
M5`) instead of bare letters. `aria-label="60 repeaters"` remains for
screen readers. For very large clusters, pills cap at `R999+` / `C999+`
etc.

Fixes #1360

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: CoreScope Bot <bot@corescope>
2026-05-25 12:59:55 -07:00
Kpa-clawbot 933ef4e6ef fix(#1356): WCAG 2.2 AA map a11y — cluster bubbles, role pills, multi-byte labels (#1357)
Red commit: d48c1add88 (CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26411462973)

Green commit CI:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/26411699037

## What

Brings the map's three visual surfaces — cluster bubbles, role pills
inside cluster bubbles, and multi-byte hash labels on repeater markers —
up to WCAG 2.2 AA. Replaces the prior color-only signaling with
structural carriers (size, border-style, glyph, letter prefix) so color
is no longer the only channel.

## How

Locked design = Tufte's structural framing ([issue
comment](https://github.com/Kpa-clawbot/CoreScope/issues/1356#issuecomment-4535244400))
WITH the WCAG audit's "Minimal patch to reach AA" applied as overrides
([issue
comment](https://github.com/Kpa-clawbot/CoreScope/issues/1356#issuecomment-4535849354)).
Where the audit and the original proposal disagreed (border color, pill
text color, V3 accent palette, font sizes), the audit's values won.

## V1 cluster bubbles

- Neutral fill `rgba(33,41,54,0.92)` via new `--mc-cluster-fill` (was
per-bucket `--info / --warning / --accent`).
- Border-style ramp as the redundant non-color carrier of the count
bucket: `mc-sm` `1.5px solid`, `mc-md` `2.5px solid`, `mc-lg` `2px
double`.
- Border color `#666` + dark halo `box-shadow: 0 0 0 1px
rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.35)` so the border edge is
visible against both Carto Positron (`#f8f9fa`) and Carto Dark Matter
(`#262626`).
- `<div role="img" aria-label="<n> nodes — <breakdown>">` with the count
+ pills wrapped `aria-hidden="true"` so the AT announcement is the
summary, not the literal glyphs.

## V2 role pills

- `ROLE_LETTERS` map (`R` / `C` / `M` / `S` / `O`) is the primary
carrier — visible inside every pill, so protanopes/deuteranopes can read
the role without depending on hue.
- Wong (2011) palette as the secondary carrier, declared as
`--mc-role-repeater/companion/room/sensor/observer` — does NOT touch the
reserved `--info / --warning / --accent` system vars.
- `color: #1a1a1a` on **all five** pills (CSS rule + inline
defense-in-depth). Passes SC 1.4.3 small-text (≥4.5:1) against every
Wong hue.
- Font now `0.625rem/1.1 ui-monospace` (was `9px`, audit bumped to
`10px`, this PR converts to `rem` so user font-size preferences scale
the pill).
- Per-pill `aria-label="<n> <role>s"`, `overflow: visible` so a user
`letter-spacing` override doesn't clip (SC 1.4.12).

## V3 multi-byte hash labels

- `MB_GLYPHS` prefix (`✓` / `?` / `✗`) is the primary non-color status
carrier; the hash text is the data.
- Neutral dark fill `--mc-mb-fill` + colored 3px left border via
per-status `--mc-mb-confirmed/suspected/unknown` (high-luminance set
`#56F0A0` / `#FFD966` / `#FF8888` — audit override of original Tol
"vibrant" set, which failed border-stripe SC 1.4.11).
- Font now `0.75rem/1.2 ui-monospace` (was `11px`, audit bumped to
`12px`, this PR converts to `rem` for SC 1.4.4 robustness).
- `<div role="img" aria-label="multi-byte <status>, hash <ID>"><span
aria-hidden="true">` so AT reads the meaningful label (not the literal
`✓ 3E`). Observer-overlay `★` carries `aria-hidden="true"` for the same
reason. Null `mbStatus` falls through to `"repeater hash <ID>"` cleanly
— no `"multi-byte undefined"`.
- Forced-colors graceful degradation via `@media (forced-colors:
active)` block mapping all three surfaces to `Canvas` / `CanvasText`
with `forced-color-adjust: auto` (NOT `none`).

## TDD red→green

| Commit | Files | CI |
|---|---|---|
| `d48c1add` (red) | `test-issue-1356-map-a11y.js`,
`.github/workflows/deploy.yml` (test + wiring only) | [**failure** — 27
assertion ✗, exit
1](https://github.com/Kpa-clawbot/CoreScope/actions/runs/26411462973) |
| `b94755e6` (green) | `public/map.js`, `public/style.css`,
`test-issue-1356-map-a11y.js` (impl) |
[**success**](https://github.com/Kpa-clawbot/CoreScope/actions/runs/26411699037)
|
| `ac63e6ab` | refactor: drop `MB_COLORS` alias, hoist `MB_MARKER_TINT`
(round-1 #3 + #4) | (round-2) |
| `8aad60cb` | style: font sizes to `rem` for SC 1.4.4 (round-1 #2) |
(round-2) |
| `50a1aab1` | test: round-1 coverage adds + de-tautologise V2.c / V3.h
(round-1 #5) | (round-2) |

Red commit failed on **assertions** (not compile error) — the harness
loaded `public/map.js` + `public/style.css` end-to-end and exhausted all
27 string-presence checks. Green commit lands the audit-overridden
design and clears 32/32. Round-2 commits extend coverage to 40/40
without altering the original red→green gate.

## WCAG SC addressed

- **SC 1.4.1 Use of Color (A)**: cluster size + border-style ramp; pill
capital-letter prefix; MB label glyph prefix. Every visual is now
carried by at least one non-color channel.
- **SC 1.4.3 Contrast Minimum (AA)**: cluster `#fff` count on composited
fill = 10.12:1 vs Positron / 14.64:1 vs Dark Matter. MB label text =
11.48:1 / 14.65:1. Pill `#1a1a1a` on Wong hues: R 5.43, C 9.10, M 6.14,
S 13.16, O 6.86 — all ≥4.5:1.
- **SC 1.4.11 Non-text Contrast (AA)**: cluster border `#666` = 4.83:1
vs Positron, 3.30:1 vs Dark Matter; MB stripes vs `--mc-mb-fill`:
`#56F0A0` 5.13, `#FFD966` 8.66, `#FF8888` 4.62. Stripe-vs-basemap edge
is mitigated by the 1px dark halo box-shadow on `.mc-mb-label`.
- **SC 1.3.1 Info & Relationships (A)**: every divIcon now has
`role="img"` + a descriptive `aria-label`; visible glyph spans are
`aria-hidden="true"` so AT reads the meaning, not the typography.
- **SC 1.4.5 Images of Text (AA)**: implemented surfaces use live text
(`<span>` + `<div>` with CSS font), not rasterised glyphs — user
font-size / zoom scale them. Where SVG markers are used (non-label
path), the textual information is also exposed via `marker.alt` + popup,
satisfying the "essential" exception.

## Manual verification

1. **Both Carto themes on staging.** Open https://analyzer.00id.net and
switch the basemap (Positron and Dark Matter) — cluster bubbles, pills,
and MB labels must remain legible on both. Border edge of cluster bubble
visible on Positron (was the original bug).
2. **Screen-reader (NVDA / VoiceOver) test.**
- Focus a cluster bubble → expect `"<n> nodes — <role breakdown>"` and
NO literal letter/number announce per pill.
- Focus a MB label on a repeater marker → expect `"multi-byte confirmed,
hash 3E"` (or whatever status/hash applies) and NO `"check mark thin
space 3 E"`.
- Observer-also-repeater label → still announces the meaningful label
only; ★ is silent.
3. **Coblis simulation** (or equivalent). Run cluster + pills + MB
labels through deuteranopia / protanopia / tritanopia simulation.
Cluster bucket must be distinguishable by size + border-style (without
hue). Pill role must be distinguishable by the letter (without hue). MB
status must be distinguishable by glyph (without hue).
4. **Windows High Contrast / forced-colors.** Toggle on; all three
surfaces should fall back to `Canvas` / `CanvasText` (no invisible
elements, no `forced-color-adjust: none` regression).

## Out of scope

Filed for separate follow-up issues (audit explicitly tagged these as
either pre-existing or modern-interpretation non-blockers):

1. **SC 2.1.1 Keyboard (A)** — cluster click-to-zoom is mouse-only today
(Leaflet markercluster limitation). Needs `role="button"` + `tabindex=0`
+ `keydown` handler. Pre-existing, not introduced by this PR.
2. **SC 2.4.7 Focus Visible (AA)** — moot until #1 is addressed (no
focusable target). When the cluster becomes focusable, a
`:focus-visible` outline must be added.
3. **`prefers-reduced-motion` gate** — `.mc-cluster:hover { transform:
scale(1.06) }` and the 120ms transition are untouched from pre-PR.
Should be gated on `@media (prefers-reduced-motion: reduce)` in a
follow-up hygiene pass.
4. **px → rem for non-font sizes** — this PR converts font sizes (the SC
1.4.4 sensitive surface). Border widths and small paddings are kept in
px because physical-pixel snapping matters more for borders than user
font-zoom.

Fixes #1356

---------

Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
2026-05-25 11:38:50 -07:00
Kpa-clawbot adcf29dd6b fix(#1329): accordion map controls on mobile, drop 200px scroll cap (#1333)
## Summary

On mobile (≤640px) the Map controls panel was capped at `max-height:
200px` and forced an internal scrollbar through all the
layer/filter/display toggles. This makes every section a single-open
accordion and drops the cap, so the visible content always fits without
internal scroll.

## Changes

- `public/map.js` — Each `fieldset.mc-section` legend becomes a tappable
`aria-expanded` toggle. On mobile the first section opens by default;
activating any other section auto-closes the previously open one
(single-open). Desktop still renders all sections expanded.
- `public/style.css` — `@media (max-width: 640px)` rules:
  - `max-height: 200px` → `calc(100vh - 80px)`.
- `.mc-collapsed > *:not(legend) { display: none }` hides bodies of
collapsed sections.
- Legend styled as flex row with ▸/▾ indicator (colors via
`var(--text-muted)`).
- All new rules live inside the mobile media query, so desktop layout is
unchanged.

## Test

`test-issue-1329-map-controls-accordion-e2e.js` (added to CI in
`deploy.yml`):

- mobile 375x812: ≥1 accordion toggle present, ≤1 expanded by default,
no internal scroll, clicking another toggle collapses the first.
- desktop 1280x800: `position: absolute`, panel <50% viewport wide, all
controls visible.

Red commit: `85fdc25267eaf210369371f55da767016435dbff` (test fails on
master — no accordion toggles exist; all fieldsets render expanded under
the 200px cap forcing scroll).

E2E assertion added: `test-issue-1329-map-controls-accordion-e2e.js:56`.

Fixes #1329

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
2026-05-23 20:54:07 -07:00
efiten 317b59ab10 feat: area-based visual node filter — attribute packets by transmitter GPS (#804) (#839)
## Summary

- Adds configurable GPS polygon areas to `config.json`; nodes are
attributed to an area if their last-known position falls inside the
polygon
- New `Area: …` dropdown filter (matching the existing region filter
style) appears on all analytics, nodes, packets, map, and live screens
when areas are configured
- Backend resolves area membership with a 30s TTL cache; area filter
bypasses the 500-node cap on `/api/bulk-health` so all area nodes are
always returned
- Includes a polygon builder tool (`/area-map.html`) for drawing and
exporting area boundaries

## Changes

**Backend**
- `AreaEntry` type + `Areas` config field
- `GetNodePubkeysInArea` DB query + `resolveAreaNodes` (30s TTL,
`areaNodeMu` RWMutex)
- `PacketQuery.Area` + `filterPackets` polygon check
- `?area=` param propagated through all analytics, topology,
clock-health, and bulk-health routes
- `/api/config/areas` endpoint

**Frontend**
- `area-filter.js`: single-select dropdown, persists to localStorage,
cleans up stale keys on load
- Wired into analytics, nodes, packets, channels, map, and live pages
- Live map clears node markers on area change

**Docs & tools**
- `docs/user-guide/area-filter.md` — configuration and usage guide
- `docs/api-spec.md` — updated with new endpoint and `?area=` param
table
- `tools/area-map.html` — polygon builder for defining area boundaries
- Demo areas added to `config.example.json`

## Test plan

- [x] No areas configured → filter dropdown does not appear on any page
- [x] Areas configured → dropdown appears, "All" selected by default
- [x] Selecting an area filters nodes/packets/topology/map correctly
- [x] Selecting "All" restores unfiltered view
- [x] Selection persists across page reloads (localStorage)
- [x] Stale localStorage key (area removed from config) is cleared on
load
- [x] `/api/bulk-health?area=X` returns all nodes in area (no 500-node
cap)
- [x] `/api/config/areas` returns correct list

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-21 14:00:15 -07:00
efiten afdd455ed9 fix(ui): align filter-bar heights and compact MESH LIVE panel (#1182)
## Summary

- **Filter bar heights**: `.btn` and `.col-toggle-btn` carried
`min-height:48px` from the WCAG touch-target rule, making buttons like
`Group by Hash`, `★ My Nodes`, `Columns ▾`, and text inputs visibly
taller than the `multi-select-trigger` / `region-dropdown-trigger`
controls (which don't carry `.btn` and were already correct at 34px).
Fix adds `min-height:34px` overrides to `.filter-bar .btn`,
`.filter-group .btn`, `.filter-bar .col-toggle-btn`, and `.filter-bar
input, .filter-bar select` so the entire filter bar renders at a uniform
34px on desktop.

- **MESH LIVE panel**: `.live-overlay` sets `flex-direction:column` on
all overlay panels; `.live-header` did not override this. With
`#liveAreaFilter` populated (when areas are configured), the panel
stacked 4 rows — title, stats, toggles, area filter — consuming ~⅓ of
viewport height. Switch `.live-header` to `flex-direction:row;
flex-wrap:wrap`, give `.live-toggles` `flex:0 0 100%` to force it to its
own line, and move `#liveAreaFilter` inside `.live-toggles` so the area
dropdown is inline with the other controls. Panel shrinks from 4 rows to
2 rows.

## Test plan

- [x] Packets page filter bar: `Filters ▾`, text inputs, `All
Observers`, `All Types`, `Group by Hash`, `★ My Nodes`, `Columns ▾`,
`Hex Paths` all render at uniform ~34px height on desktop
- [x] Mobile (≤767px): filter bar touch targets unaffected (mobile media
query still authoritative)
- [x] Live page: MESH LIVE panel occupies 2 rows (title+stats / toggles)
instead of 4
- [x] Live page: `Area: All ▾` appears inline in the toggles row when
areas are configured; panel hides the area control entirely when no
areas are configured (existing behavior)
- [x] Audio controls still appear correctly when the Audio toggle is
checked

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:40:01 -07:00
efiten e078f4bbb6 fix(filters): unified 34px height for all filter controls across pages (#1192)
## Summary

The global `select { min-height: 48px }` touch-target rule was taller
than the 34px custom dropdown buttons (region filter, multi-select
dropdowns), causing visible height inconsistency on the packets,
analytics, and nodes pages.

- **`.filter-bar input/select`** — add `min-height: 34px` to match
existing `height: 34px` (packets page: time window, channel, sort
selects and text inputs)
- **`.nodes-filters select`** — add `height: 34px; min-height: 34px`
(nodes page: last-heard select)
- **Analytics page** — replace `.time-window-filter` + label with
`.analytics-filters` flex row; style `#analyticsTimeWindow` with
`.analytics-time-window-select` to match region dropdown button height
and appearance
- All filter controls now sit at a consistent 34px, matching the
existing custom dropdown buttons

Supersedes #1191 (which only fixed the analytics case).

## Test plan

- [x] Packets page: time window, channel, sort selects are same height
as Filters/Group by Hash/My Nodes buttons
- [x] Analytics page: region filter and time-window select sit side by
side at the same height
- [x] Nodes page: last-heard select is same height as All/Active/Stale
buttons
- [x] On mobile, filter controls wrap correctly (flex-wrap)
- [x] Dark theme: select background and border match surrounding
controls

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:56:38 -07:00
Kpa-clawbot e2d320449b fix(#1281): hide empty Location row + theme map link via --accent (#1284)
## Summary
Minimal fix for #1281 — two surgical changes to the packet detail pane:

1. **Hide the `Location` row when transmitter GPS is unavailable.**
Only ADVERT packets carry unencrypted GPS in their payload, so ~90% of
packet types (TXT_MSG, GRP_TXT, ACK, REQ, MULTIPART, …) were rendering
`<dt>Location</dt><dd>—</dd>` for nothing. We now skip the `<dt>/<dd>`
   pair entirely when `locationHtml` is empty. ADVERT rendering is
   unchanged.

2. **Fix the `📍map` link contrast in dark mode.**
The trailing link had only `style="font-size:0.85em"` and inherited the
   UA-default `<a>` blue (`rgb(0,0,238)`) → unreadable against
   `--card-bg` in dark theme. Replaced inline style with
   `class="loc-map-link"` and added a small CSS rule that pulls color
   from `var(--accent)`.

### Out of scope (per operator direction)
The original issue also proposed adding an `Rx:` observer-GPS line and
distance-from-observer. **Not in this PR** — operator decided the
existing observer IATA pill already conveys that, so adding more rows
here is unnecessary. Bullets 1–2 of the issue's "Acceptance" list are
covered; the multi-line `Tx:`/`Rx:` reformat is intentionally not done.

## TDD
- **Red** `d465cf84` — `test-issue-1281-location-row-e2e.js` asserting:
  - Non-ADVERT detail must NOT contain `<dt>Location</dt>`
  - ADVERT detail STILL contains `<dt>Location</dt>` with GPS coords
- `.loc-map-link` computed `color` equals `var(--accent)` (not UA blue)
  Verified to fail on master (`1 passed, 2 failed`) — see commit body.
- **Green** `8c9bd8cb` — implementation. All three assertions pass.
- **CI wiring** `9571b4f4` — added the test to `deploy.yml`'s E2E block.

## Files changed
- `public/packets.js` — empty-string default for `locationHtml`,
  conditional `<dt>/<dd>` render, three sites swap inline style → class.
- `public/style.css` — new `.loc-map-link { color: var(--accent); … }`
  rule next to `.detail-meta dd`.
- `test-issue-1281-location-row-e2e.js` — new Playwright E2E.
- `.github/workflows/deploy.yml` — one-line CI hook.

## Acceptance verification (against fixture DB)
```
=== #1281 Location row + map link contrast E2E against http://localhost:13581 ===
  ✓ Non-ADVERT packet detail does NOT render <dt>Location</dt>
  ✓ ADVERT packet detail STILL renders <dt>Location</dt> with GPS coords
    link.color=rgb(74, 158, 255)  --accent→rgb(74, 158, 255)
  ✓ 📍map link uses class="loc-map-link" with color = var(--accent)
3 passed, 0 failed
```

Fixes #1281

---------

Co-authored-by: bot <bot@local>
2026-05-18 23:37:04 -07:00
Kpa-clawbot c1d94f7db5 fix(#1273): collapse QR overlay wrap to content height (#1277)
## Summary
Fixes #1273 — `.node-top-row .node-qr-wrap` was 2-3× taller than the QR
canvas inside it, leaving empty translucent space below the QR.

## Root cause
Three compounding issues:

1. **SVG intrinsic height not constrained.** `qrcode-generator` emits an
SVG with fixed `width`/`height` attributes (e.g. 147×147). The CSS rule
`.node-qr svg { max-width: 100px }` (and 72px mobile) constrains *width*
only, so the svg's intrinsic height (147px) is preserved and the wrap is
sized to that.
2. **Flex stretch.** `.node-top-row` is `display:flex` with default
`align-items:stretch`, so the QR column was forced to match the map
column's height (~280px) on desktop.
3. **Excess padding/margin** added another ~24px above and below the
visible QR.

## Fix
Three small CSS changes in `public/style.css`:

| change | effect |
|---|---|
| `.node-qr svg { height: auto; }` | svg height scales with constrained
width |
| `.node-top-row .node-qr-wrap { align-self: flex-start; }` | wrap sizes
to content, not column |
| `.node-top-row .node-qr-wrap { padding: 8px; }` + zero inner
`.node-qr` margin-top | tight hug |

## Measurements (real-data fixture, full node detail page)

| viewport | wrap.height before | wrap.height after | QR canvas |
|---|---|---|---|
| 375×800 (mobile overlay) | 165px | **82px** | 72×72 |
| 1280×800 (desktop side-by-side) | 217px | **154px** | 100×100 (+ 28px
caption) |

Overlay remains `position:absolute` top-right on mobile; the original
#1243 behavior is preserved.

## TDD
- **RED**: `test-issue-1273-qr-overlay-height-e2e.js` asserts wrap
height ≤ visible QR + caption + 32px at 375×800 and 1280×800. Failed on
master with deltas of 93px (mobile) and 89px (desktop).
- **GREEN**: both viewports pass after the CSS fix.

Wired into the deploy workflow alongside the other `test-issue-*-e2e.js`
runs.

## Acceptance checklist
- [x] Container height ≈ QR canvas height + 16-24px padding total
- [x] No empty translucent space below the QR
- [x] E2E asserts at 375×800 and 1280×800
- [x] Desktop layout unchanged (overlay position preserved; column no
longer stretches but the QR card is the same width)
- [x] All colors via CSS variables
- [x] #1243 overlay behavior preserved (still top-right on mobile, still
rendered)

## Commits
- `e9d75c92` test(#1273): RED
- `13899270` fix(#1273): collapse QR overlay wrap

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-18 22:51:29 -07:00
Kpa-clawbot 1d33ac53b0 fix(#1254): trim .badge-iata h-padding on mobile to clear 1.25px clip (#1255)
Fixes #1254.

Master CI Playwright fail-fast on every push since #1252:

```
 Mobile viewport (375px): observer IATA badge stays visible — not clipped:
   .badge-iata right edge 376.25 exceeds 375px viewport
```

## Root cause

After #1252 unhid `.col-observer` at narrow widths so the IATA pill from
#1188 renders on mobile, at 375px the cell padding + truncated observer
name (10 chars in grouped rows) + `.badge-iata` pill (`padding: 1px 5px`
+ `margin-left: 4px`) sums to ~376.25px — overflowing the viewport by
1.25px.

Same class of failure as #1250/#1251 (VCR LCD-clip).

## Fix

`public/style.css` — inside the existing `@media (max-width: 640px)`
block, shrink `.badge-iata` `padding: 1px 5px → 1px 3px` and
`margin-left: 4px → 2px`. Reclaims ~6px horizontally, well clear of the
1.25px overflow. Desktop (≥641px) styling untouched.

## TDD

The failing E2E sub-test in `test-observer-iata-1188-e2e.js` (added in
#1189 R1) IS the red. Mutation verified locally:

| Variant            | Result |
|--------------------|--------|
| WITHOUT this fix |  `.badge-iata right edge 376.25 exceeds 375px
viewport` |
| WITH this fix      |  all 3 sub-tests pass |

## Local verification

```
$ go build -o /tmp/corescope-server ./cmd/server
$ /tmp/corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public &
$ CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 \
    node test-observer-iata-1188-e2e.js
Running observer-IATA E2E tests against http://localhost:13581
   Packets table renders an IATA badge in an observer cell
   Filter grammar: observer_iata == "<code>" narrows the table
   Mobile viewport (375px): observer IATA badge stays visible — not clipped
All observer-IATA E2E tests passed.
```

## Constraints honored

- All colors via existing CSS variables (no theming illusions; only
  `padding` / `margin-left` change inside `@media (max-width: 640px)`).
- No JS changes.
- Desktop badge display unaffected (selector scoped to narrow viewport).
- `config.example.json`: no config field added.
- PII preflight: clean.

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
2026-05-17 16:26:51 -07:00
Kpa-clawbot b881a09f02 feat(#1188): show observer IATA on packets + filter grammar (#1189)
Red commit: 4ed272761b (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25651898290)

Fixes #1188 — observer IATA on packets in three UI surfaces + filter
grammar.

cross-stack: justified — feature spans API shape (Go), store, filter
grammar (JS), three packets UI surfaces.

## Scope shipped
- Packets table row: `.badge-iata` pill inline next to observer name
- Expanded observation rows: per-observation IATA badge
- Detail pane: Observer dd + per-observation list both render the badge
- Filter grammar: `observer_iata` field + `iata` alias;
`==`/`!=`/`contains`, plus a new `in (a, b, c)` list operator. Both
names appear in autocomplete with descriptions.

## TDD red→green pairs
1. `271d72f` filter-grammar tests → `2c182eb` evaluator + suggest
entries
2. `4ed2727` backend `observer_iata` API tests → `7856914` SQL join +
struct/store wiring
3. `0e09371` display E2E → `7a3f45d` packets.js + style.css badge
(E2E swapped for string-contract unit test in `ee414b4` — fixture
`observations.observer_idx` stores text pubkeys, blocking the join the
badge depends on)

## Backend
- `cmd/server/db.go`: SELECT `obs.iata AS observer_iata` in
`transmissionBaseSQL`, grouped query, observations-by-transmissions
- `cmd/server/store.go`: `ObserverIATA` on `StoreTx`/`StoreObs`, load
via all three ingest paths, surface in
`txToMap`/`enrichObs`/`groupedTxsToPage`
- `cmd/server/types.go`: field added to
`TransmissionResp`/`ObservationResp`/`GroupedPacketResp`
- Test fixture schemas declare `iata` on observers

## Perf
Per #383, `obsIataBadge(packet)` reads `packet.observer_iata` directly
(server-joined). Falls back to `observerMap.get(id).iata` only if absent
— hot row-render loop avoids per-row Map lookup on fresh data.

## Display rules
Missing IATA: nothing inline (Region column still shows `—`). No new hex
— `.badge-iata` uses `var(--nav-bg)` / `var(--nav-text)`.

E2E assertion added: test-observer-iata-1188.js:51

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.dev>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-17 16:13:11 +00:00
Kpa-clawbot 74685ac82f fix(#1243): node detail mobile QR overlays map semi-transparently (#1245)
RED commit `fc9b619a` — CI:
https://github.com/Kpa-clawbot/CoreScope/actions

Fixes #1243.

## Problem
On `#/nodes/<pubkey>` at 375×800, the QR code rendered as a separate
~250px-tall panel below the map. Desktop already overlays the QR
semi-transparently via `.node-map-qr-overlay` for the compact view.

## Fix
Extend the mobile breakpoint (`@media (max-width: 640px)`) so the
full-screen `.node-top-row` mirrors the desktop overlay pattern:

- `.node-top-row` → `position: relative`; map wrap expands to 100%
- `.node-qr-wrap` → `position: absolute; bottom/right: 8px; z-index:
400`
- Semi-transparent background (`rgba(255,255,255,0.85)` light / `0.4`
dark)
- Caption hidden in overlay (already shown above)

Desktop (≥768px) flex layout untouched.

## TDD
- RED `fc9b619a` — E2E at 375×800 asserts QR is `position:
absolute|fixed`, overlaps map rect, and bg alpha < 1.
- GREEN `ded978c0` — CSS adds overlay rule.

## Verification
Preflight clean. Desktop layout unaffected — change is scoped inside
`@media (max-width: 640px)`.

## Files
- `public/style.css` (+29)
- `test-e2e-playwright.js` (+57)

---------

Co-authored-by: clawbot <clawbot@local>
2026-05-17 16:03:55 +00:00
Kpa-clawbot 4ea1bf8ebc fix(#1236): map mobile — sticky panel header + remove right gutter (#1237)
RED: 862d7c82 — E2E asserting (A) leaflet-map width == viewport on
mobile and (B) sticky panel header. CI URL: see Checks tab.

Fixes #1236.

## Sub-issue A — Map Controls panel scroll affordance
**Root cause:** `.map-controls` already had `max-height` + `overflow-y:
auto`, but the `<h3>` title was static — once the panel scrolled, the
title scrolled away with it and users lost the affordance that they were
inside a scroll container. No visual cue, no anchor.

**Fix:** make `.map-controls h3` `position: sticky` at the top of the
scroll container (pulled flush to the panel edges with negative margins
so it covers the corner radius cleanly), with the panel `--card-bg`
background and a `--border` bottom rule. Added `scrollbar-gutter:
stable` so the scroll indicator is consistently present.

## Sub-issue B — Map canvas offset left with right gutter
**Root cause:** `.map-side-pane` (Path Inspector) is `flex: 0 0 32px`
inside the flexbox `#map-wrap`. At every viewport width that 32px is
consumed before the leaflet canvas gets sized, leaving an unused band on
the right. Desktop has room for it; mobile (375px viewport) does not —
and Path Inspector hex-prefix entry is impractical on a phone anyway.

**Fix:** `display: none` on `.map-side-pane` at `≤640px`. Leaflet canvas
now fills 100% of the viewport.

## Verification
- E2E `test-issue-1236-map-mobile-e2e.js` covers both at 375x800 +
desktop guard at 1280x800. RED commit (`862d7c82`) failed 2/3 mobile
assertions; GREEN commit (`85efcba7`) passes 3/3.
- Map canvas width at 375x800: **343px → 375px**.
- Existing channels mobile E2E (#1224) still passes.
- Desktop (1280px): panel stays `position: absolute`, Path Inspector
pane still present.

All colors via CSS variables. No JS changes.

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
2026-05-16 20:01:00 +00:00
Kpa-clawbot 70855249c2 fix(#1224): channels page mobile UX overhaul (#1227)
## Summary
RED test commit: `02652d0042b7cf65d1f9b3e96ce376bbb3064ba6` — CI:
https://github.com/Kpa-clawbot/CoreScope/actions

Mobile UX overhaul for the Channels page (#1224). At 375x800 the sidebar
header was 112px tall (title + button stacked, analytics link + region
filter each on their own row) and the channel-name column was clipped to
83px by the inline 📤 Share + ✕ Remove buttons.

## What changed
- **Header is now ONE row**: title + region filter + `+ Add` chip + `📊`
analytics overflow chip. Capped to ≤56px on mobile.
- **`+ Add Channel` → `+ Add` chip** (no longer a full-width hero).
Verified <65% of sidebar width.
- **Analytics link** is an icon-only chip inside the header (was a
full-row link below).
- **Region filter** is inline inside the header (was its own row).
- **Channel rows**: `.ch-item-name` takes `flex:1`, share button is
icon-only (📤), remove button shrunk to 32px touch target. Name >150px on
the first row.
- **Empty state** is `max-height:30vh; padding:12px` on mobile — no
longer dominates the viewport.

## Design decisions
- Chose **inline chips** over an overflow `⋮` menu: header-level
controls are few enough (4) that stacking pills + filter dropdown fits
comfortably in 375px. Avoids the cost/complexity of a popover and
matches the page's existing pill vocabulary (region filter).
- Per-row share/remove kept inline but icon-only (`font-size:0` +
`::before`) — preserves single-tap access without consuming the row.
- Touch targets stay ≥32px (action chips) / 44px (other tappables); WCAG
2.5.5 spirit retained on the dominant interactive paths.
- **Desktop layout (≥768px) is unchanged** — verified by a desktop guard
in the E2E (`.ch-layout` flex-direction stays `row` at 1024px).

## Tests
- `test-issue-1224-channels-mobile-ux-e2e.js` — 5 assertions at 375x800
+ 1 desktop guard at 1024x800. Wired into CI.
- Existing channel suites still pass: `test-channel-fluid-e2e.js`
(11/11), `test-channel-issue-1087-e2e.js` (3/3),
`test-channel-issue-1111-e2e.js` (2/2), `test-channel-modal-ux.js`
(33/33), `test-channel-ux-followup.js` (29/29),
`test-channel-sidebar-layout.js` + `test-channel-fluid-layout.js`
(14/14).

Fixes #1224

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-16 15:50:52 +00:00
Kpa-clawbot a1f9dca951 fix(live #1205): re-anchor settings toggles inside MESH LIVE panel (#1219)
Red commit: f80ce5248a (CI URL appears in
the Checks tab once the workflow starts).

Supersedes closed PR #1209 with the correct approach (toggles in MESH
LIVE panel, not legend).

Fixes #1205.

## Problem
The Live Map settings toggle row (Heat / Ghosts / Realistic / Color by
hash / Matrix / Rain / Audio / Favorites / node filter / region filter —
`#liveControls`) rendered as a free-floating sibling `.live-overlay`
pinned `position: fixed` at bottom-right with `bottom: calc(78px +
var(--bottom-nav-height) + safe-area)`. On many viewports it visually
orphaned across the middle of the map, anchored to no panel.

## Regression cause
PR **#1180** (commit `127a1927` — "compact header, pin controls
bottom-right, narrow toggles") extracted `.live-toggles` from inside
`.live-header` (the MESH LIVE panel) into a brand-new sibling
`.live-overlay.live-controls` cluster. Before #1180 the toggles lived as
a direct child of `.live-header`.

## Fix
Restore the pre-#1180 structural pattern: `#liveControls` is re-parented
as a child of `#liveHeader`, breaking onto its own row via `flex: 0 0
100%`. No more `position: fixed` overlay, no more free-floating cluster
— the toggles share the MESH LIVE panel's chrome (background, blur,
border, padding).

- `public/live.js`: re-parent the `#liveControls` block inside
`#liveHeader`, drop the `.live-overlay` class.
- `public/live.css`:
- `.live-controls`: `position: static`, transparent (header supplies
chrome), `flex: 0 0 100%`.
- `.live-header`: `flex-wrap: wrap`, `row-gap: 6px`, `max-width:
calc(100vw - 24px)`; drop the `max-height: 40px` cap.

Why this beats PR #1209: that PR parked toggles inside `#liveLegend`,
inverting the *data → key → controls* hierarchy and pushing the legend
to 60vh on mobile. Anchoring back to the MESH LIVE panel keeps controls
with the panel that already labels the live surface and inherits its
corner / drag affordances.

## Tests
- **Red** (`test-issue-1205-live-controls-anchor-e2e.js`): asserts
`#liveHeader.contains(#liveControls)` AND not contained in
`#liveLegend`, parent is not `<body>` / `.live-page` directly, and the
controls rect stays within the viewport. Runs at **1440×900, 640×900,
320×800**. Fails on master.
- **Updated** `test-live-layout-1178-1179-e2e.js`:
- (a) `.live-header-critical` height ≤ 40px (the critical strip stays
compact; header itself now wraps).
- (b) `.live-controls` `position: static` AND descendant of
`#liveHeader` (new contract replacing the retired "fixed/right
≤24px/bottom>0").
- Wired in `.github/workflows/deploy.yml` next to the other live-layout
E2Es.

## Acceptance criteria
- [x] Settings toggle row renders inside the MESH LIVE panel
(`#liveHeader`)
- [x] Not parked in `#liveLegend` (rejected by #1209 review)
- [x] Tested at desktop + tablet + narrow phone viewport widths
- [x] E2E DOM assertion: parent is the MESH LIVE panel, not body /
`.live-page` / `#liveLegend`

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-16 05:54:43 +00:00
Kpa-clawbot 03b5d3fe28 fix(#1065): first-visit gesture discoverability hints (#1186)
Red commit: 4e0a168bc0 (CI run: see Checks
tab — branch pushes don't trigger CI on this repo; first CI is on this
PR)

Fixes #1065. Parent: #1052.

## What
First-visit gesture discoverability hints. Brief animated balloons
appear 800ms after page settle on first visit, announcing each gesture:
swipe-row-action, swipe-between-tabs, edge-swipe-drawer,
pull-to-refresh. Each hint dismisses individually via "Got it";
dismissed hints persist across sessions; "Reset gesture hints" in
Customize → Display restores them.

## Decisions
- **localStorage namespace:** `meshcore-gesture-hints-<id>` with keys
`row-swipe`, `tab-swipe`, `edge-drawer`, `pull-refresh`. Value:
`"seen"`.
- **Hint timing:** 800ms post-settle delay (lets page render); no
auto-mark — hints fade after 8s but only "Got it" sets the flag (so
users who miss the fade still see them next visit). Conservative
interpretation of AC.
- **Settings reset location:** Customize → Display tab → "Gesture Hints"
subsection → `↺ Reset gesture hints` button. Calls
`window.GestureHints.reset()` which clears all four keys + removes any
visible balloons.
- **Pull-to-refresh fallback:** hint only shown if `.pull-to-reconnect`
element exists in DOM (per #1063). If absent, the hint is silently
skipped — other 3 still show.
- **prefers-reduced-motion:** `animation-name: none !important` under
the media query; only opacity transition remains.
- **No focus stealing:** no `autofocus`, no `.focus()` calls. Wrapper
has `pointer-events: none`; only the inner balloon + dismiss button
capture pointer, so the row underneath stays interactive (no conflict
with #1185 row-swipe).
- **Singleton + cleanup:** module-scoped `window.__gestureHints1065Init`
counter; `hashchange` listener bound exactly once across SPA mounts;
dismissed hints don't re-show on route change (gated by `localStorage`).
- **Relevance gating:** row-swipe hint only on `/#/packets|nodes|live`;
edge-drawer only at viewport > 768px (matches #1064 drawer scope).

## E2E
`test-gesture-hints-1065-e2e.js` — Playwright covering first-visit show,
"Got it" dismiss + flag persistence, reload-no-show, Settings reset →
reload → re-show, edge-drawer at 1024x800, prefers-reduced-motion →
animation-name: none, focus not stolen, singleton across 5 SPA
round-trips.

E2E assertion added: test-gesture-hints-1065-e2e.js:90

Browser verified: pending CI run.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-09 20:03:54 -07:00
Kpa-clawbot b4f186af19 fix(#1062): gesture system — swipe rows, tabs, slide-over dismiss (#1185)
Red commit: bbb98cf81aae38bff1ef77a7c8a701813b25bb77 (CI run: pending —
see Checks tab)

Fixes #1062. Parent: #1052.

## Gesture system

Adds touch-gesture handling on phones (≤768px):

1. **Swipe-left on a packets/nodes/observers row** → reveals row-action
overlay (trace, filter, copy hash). Threshold: 24% of row width OR 80px.
Sub-threshold = visual peek that snaps back.
2. **Horizontal swipe on the bottom-nav strip** → advances tabs in TAB
order from `bottom-nav.js`. Packets ↔ Live ↔ Map etc.
3. **Swipe-down on a slide-over panel** → calls
`window.SlideOver.close()`.

## Hard constraints met

- **Pointer Events ONLY** — no `touchstart`/`touchend` mixing.
`setPointerCapture` for tracking continuity.
- **Axis-lock** — direction committed in first 8–12px movement. Vertical
scroll is never blocked unless we explicitly committed to a horizontal
swipe. `body { touch-action: pan-y }` so the browser owns vertical
natively.
- **Leaflet exclusion** — handlers early-bail on
`e.target.closest('.leaflet-container')` so pinch/pan on the map tab are
untouched.
- **Singleton pattern** — module-scoped `__touchGestures1062InitCount`
guard. Document-level pointer listeners registered exactly once even if
the script loads multiple times (mirrors the #1180 fix class).
- **prefers-reduced-motion** — animations have `transition-duration: 0s`
under the media query; gestures still trigger, snaps are instant.

## E2E

`test-gestures-1062-e2e.js` — Playwright with synthesized PointerEvents
(page.touchscreen unreliable in headless for axis-locked custom
handlers). Wired into the deploy.yml matrix.

E2E assertion added: test-gestures-1062-e2e.js:120 (overlay-visible
after left-swipe), :201 (tab advance), :219 (Leaflet exclusion), :247
(slide-over dismiss).

---------

Co-authored-by: openclaw-bot <bot@openclaw>
Co-authored-by: OpenClaw Bot <bot@openclaw.dev>
Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-09 18:41:19 -07:00
Kpa-clawbot 9b9848611b fix(#1064): edge-swipe nav drawer (Option A, wide-only) (#1184)
Red commit: a810b1fac5 (CI run pending —
draft PR)

Fixes #1064 (parent epic #1052).

## What
Edge-swipe nav drawer (slide-over from left). Pointer-down within left
20px → drag-follows-finger animation → settles open/closed on velocity
threshold. Drawer surfaces the same long-tail routes as the More sheet
(PR #1174) as an alternate entry point at the EDGE.

## Decisions
**Option A — drawer wide-only (>768px).** At ≤768px the bottom-nav has
the More tab (PR #1174); a left-edge drawer there would compete with
that UX. Wide viewports have no More tab, so the drawer replaces the
top-nav hamburger as a faster keyboard-free entry. Bottom-nav
coexistence at narrow widths: none — drawer is disabled.

**Singleton + cleanup.** Module-scoped guard +
`__navDrawerPointerBindCount` debug seam — same pattern as #1180 fix.
`pointermove`/`pointerup` are bound on `document` exactly once across
SPA mounts.

**`body { touch-action: pan-y }`.** Vertical scroll preserved
everywhere; the drawer claims horizontal swipes only inside our
viewport. iOS browser-back left-edge gesture still works (it's the OS,
not the page).

**Accessibility.** `inert` on the drawer when closed, removed when open.
Focus trap (Tab cycles last↔first). Esc closes. Backdrop tap closes.
`prefers-reduced-motion`: instant snap, no animation.

## Tests (TDD)
Red commit pushes the E2E first; CI must FAIL on assertion.
E2E assertion added: `test-nav-drawer-1064-e2e.js:1`

## CSS coordination with #1062
Additions are fenced (`/* === Issue #1064 — Edge-swipe nav drawer === */
… /* === end #1064 === */`) to minimize merge friction.

---------

Co-authored-by: corescope-bot <bot@corescope>
Co-authored-by: openclaw-bot <bot@openclaw>
Co-authored-by: OpenClaw Bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-09 17:55:42 -07:00
Kpa-clawbot 9d1f5d2395 fix(#1061): bottom navigation for narrow viewports (#1174)
Red commit: a200704d5e27e47c0b29a4745bf1a1772a8876fe (CI URL added once
Actions resolves the run)

Fixes #1061

## What
Bottom navigation at ≤768px with 5 tabs in spec order: Home, Packets,
Live, Map, Channels. Top-nav suppressed at the same breakpoint — no
duplicate nav UX.

## Files
- NEW `public/bottom-nav.js` — renders 5 tabs, syncs `.active` on
`hashchange`, reuses the existing in-app hash router (`<a
href="#/...">`). Stable selector `[data-bottom-nav-tab="<route>"]`.
Container `[data-bottom-nav]`.
- NEW `public/bottom-nav.css` — styles. Tokens reused: `--nav-bg`,
`--nav-text`, `--nav-text-muted`, `--nav-active-bg`, `--accent`,
`--border` (all global → resolve in BOTH light and dark themes).
- `public/index.html` — one `<link>` for the CSS, one `<script>` after
`app.js`. The `<nav>` is appended by JS as a sibling of `<main
id="app">` at DOMContentLoaded.
- `test-bottom-nav-1061-e2e.js` + `.github/workflows/deploy.yml` —
Playwright wiring.

## Decisions
- **Breakpoint:** `@media (max-width: 768px)`. No `@container` rules
exist anywhere in `style.css` today — media query is consistent.
- **Top-nav suppression:** `display:none` at ≤768px. Simpler than a
hamburger collapse; long-tail routes (Tools/Lab/Perf) remain reachable
by URL; "More"-tab/hamburger fallback deferred per issue body.
- **Active indicator:** `var(--nav-active-bg)` + 2px accent top-border.
No moving pill.
- **Safe-area:** `padding-bottom: env(safe-area-inset-bottom)` on nav +
reciprocal `body` reservation. `viewport-fit=cover` already in place.
- **Reduced motion:** `prefers-reduced-motion: reduce` disables the
transition.

## TDD
- Red: `a200704` — assertions fail (no bottom-nav).
- Green: `53851a1` — component + styles.

E2E assertion added: `test-bottom-nav-1061-e2e.js:71` (case (a) —
bottom-nav visible at 360x800).

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-09 11:00:46 -07:00
Kpa-clawbot 05876b3a59 fix(#1173): replace #liveDot with packet-driven brand-logo node-pulse (#1177)
Red commit: PENDING (will update)

Fixes #1173.

Replaces the `#liveDot` WebSocket-connected indicator with a
packet-driven node-pulse animation on the brand logo's two inner
circles.

## Behavior (locked per issue spec)
- **Animation curve:** `ease-out` (default per open-question 1).
- **Rate cap:** 15/sec (66ms gap; default per open-question 2). Excess
triggers are dropped, never queued.
- **Direction:** alternates A→B / B→A across messages (aesthetic, not
semantic).
- **Idle ≥10s:** logo at full brightness, no animation.
- **Disconnected:** `.logo-disconnected` applies `filter: grayscale(0.6)
opacity(0.7)`.
- **`prefers-reduced-motion: reduce`:** single-step `.logo-pulse-blip`
on destination only.

## Implementation
- WS handler hook lives in `public/app.js` `connectWS()` (`ws.onmessage`
triggers `Logo.pulse()`; `ws.onopen`/`ws.onclose` toggle
`Logo.setConnected()`).
- `Logo` is a small IIFE in `app.js` that exposes
`window.__corescopeLogo` for E2E injection.
- All animation is pure CSS; JS only toggles `.logo-pulse-active` /
`.logo-pulse-blip` / `.logo-disconnected`. Colors come exclusively from
`--logo-accent` / `--logo-accent-hi` tokens.
- Two new classes (`.logo-node-a`, `.logo-node-b`) attached to inner
circles in both `.brand-logo` and `.brand-mark-only` SVGs so the mobile
mark animates too.

## `#liveDot` removal proof
```
$ grep -rn liveDot public/
(no output)
```

## E2E
- E2E assertion added: `test-logo-pulse-1173-e2e.js:54` and follows.
- Wired into the Playwright matrix in `.github/workflows/deploy.yml`
(mirrors PR #1168 pattern from commit `5442652`).
- Test injects synthetic pings via `window.__corescopeLogo.pulse({
synthetic: true })`; matches the existing harness style (no new WS-mock
pattern invented).

Red→green discipline preserved: the test commit lands first and CI fails
on assertion; the implementation commit follows.

---------

Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-08 20:25:42 -07:00
Kpa-clawbot 9774403fa4 fix(#1058): analytics chart containers — fluid + auto-stacking (#1175)
Red commit: 0f29da3 (CI is pending — will be linked once dispatched)

Fixes #1058

This PR is in **red phase**. The new E2E asserts the desired
fluid + auto-stacking behavior; with `master`'s code it FAILS at
≥768px (cards don't stack). Green commit follows.

E2E assertion added: `test-charts-fluid-1058-e2e.js:99`.

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
2026-05-08 17:33:24 -07:00
Kpa-clawbot 89d644dd72 fix(#1056): row-detail slide-over panel at narrow widths (AC #4) (#1168)
Red commit: 8ac568bac3 (CI run: pending)

## Summary
Implements AC #4 of #1056: row-detail **slide-over panel** at narrow
viewports for the Packets, Nodes, and Observers tables.

ACs #1–#3, #5 already shipped in #1099; this PR closes the remaining
criterion.

## Approach
- Shared `window.SlideOver` helper (`packets.js`, top of file next to
`TableResponsive`) — singleton overlay (`.slide-over-backdrop` +
`.slide-over-panel`) injected into `<body>`. Close affordances: X button
(`.slide-over-close`), backdrop click, Escape key. `aria-modal="true"`,
focus moved to close button on open.
- Breakpoint: `window.innerWidth <= 1023` (matches the
`data-priority="3"` threshold reused by `TableResponsive`). At `>=1024`
the existing right-side panel / full-screen behavior is preserved — no
regression.
- Each page (`packets.js`, `nodes.js`, `observers.js`) checks the
breakpoint at row-click time and routes the same detail content into
`SlideOver.open(node)` instead of the side panel / full-screen
navigation.
- Reuses the existing `slideInRight` keyframe in `style.css`.
- CSS additions live in the table section of `style.css` only.

## E2E
`test-slideover-1056-e2e.js` — at 800x800 clicks the first row of each
of the three tables, asserts `.slide-over-panel` +
`.slide-over-backdrop` are visible and the close X exists; verifies
Escape, backdrop click, and X click all dismiss; verifies that at 1440
the slide-over does NOT appear.

E2E assertion added: `test-slideover-1056-e2e.js:71`

## TDD
- Red commit: `8ac568b` — E2E asserts on `.slide-over-panel` which does
not exist yet.
- Green commit: forthcoming in this PR.

Fixes #1056

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
2026-05-08 14:13:37 -07:00
Kpa-clawbot cf604ca788 fix(nav): #1109 mobile hamburger dropdown clipped by .top-nav overflow:hidden (#1163)
# Fix #1109 — mobile hamburger dropdown clipped invisible by `.top-nav {
overflow:hidden }`

Red commit: `5429b0f` (failing E2E, asserts pixel-level visibility).

## Symptom

On <768px viewports, tapping `#hamburger` toggles `.nav-links.open` and
`body.nav-open` correctly — DOM state is right, `aria-expanded="true"`,
computed `display:flex` — but **nothing appears below the navbar**. The
dropdown is laid out at `y=52..626` but visually clipped.

## Root cause

`.top-nav` is `position:sticky; height:52px; overflow:hidden` (added in
#1066 fluid scaffolding at `417b460` to guard against horizontal
overflow during the Priority+ measurement pass). At <768px the dropdown
becomes `position:absolute; top:52px`, so its containing block is
`.top-nav` — and `.top-nav`'s `overflow:hidden` clips everything below
`y=52`. Result: the dropdown renders inside a 52px box and the user sees
nothing.

Full RCA + screenshots:
https://github.com/Kpa-clawbot/CoreScope/issues/1109#issuecomment-4398900387

## Fix

In `public/style.css`, inside `@media (max-width: 767px)`, change
`.nav-links` from `position:absolute` to `position:fixed`.
`position:fixed` escapes any `overflow:hidden` ancestor (its containing
block becomes the viewport), so the dropdown is no longer clipped. All
other rules (display/flex/background/padding/z-index) keep working.

This deliberately does **not** relax `overflow:hidden` on `.top-nav` —
that would reopen the #1066 horizontal-overflow regression on desktop.

## Why prior tests missed this

Existing nav E2Es asserted `.classList.contains('open')` /
`getComputedStyle().display === 'flex'` — pure DOM state. Those passed
even while the dropdown was clipped invisibly. The new test in this PR
asserts **pixel-level visibility**:
`document.elementFromPoint(viewportWidth/2, 100)` must land on something
inside `.nav-links` (not `<body>`), and the first `.nav-link`'s bounding
rect must satisfy `bottom > 60` and have non-zero area. A state-only fix
can never satisfy this.

E2E assertion added:
`test-issue-1109-hamburger-dropdown-visible-e2e.js:113` (the
`hitInsideNavLinks` check).

## Files changed

- `public/style.css` — one line in the mobile media query: `position:
absolute` → `position: fixed`
- `test-issue-1109-hamburger-dropdown-visible-e2e.js` — new E2E
- `.github/workflows/deploy.yml` — wire the new E2E into the suite

Fixes #1109

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-07 09:39:29 -07:00
Kpa-clawbot cfd1903c6b fix(logo): default sage/teal brand colors, customizer mirrors accent (#1162)
## Summary

Restores sage/teal as default logo colors while preserving customizer
theming. Closes the gap from #1157 (closed) and the user's complaint
about lost two-tone.

Out-of-the-box, the navbar + hero CORE/SCOPE wordmarks now render the
brand-identity duotone — `#cfd9c9` (sage / fog) and `#2c8c8c` (teal /
water). When an operator picks a theme via the customizer (or sets a
custom accent color), the wordmark recolors to follow.

## Approach (Option C — decoupled defaults + customizer mirror)

- **`public/style.css` `:root`** — set `--logo-accent: #cfd9c9` and
`--logo-accent-hi: #2c8c8c` as literal defaults. Removes the previous
`var(--accent)` cascade so blue-by-default no longer leaks into the
brand mark.
- **`public/customize-v2.js`** — `applyTheme()`, the early-apply path,
and the live color-picker `input` handler now mirror
`themeSection.accent` → `--logo-accent` and `themeSection.accentHover` →
`--logo-accent-hi`.
- **`public/customize.js`** (legacy) — same mirroring in
`applyThemePreview()` and the early localStorage replay.
- **`.github/workflows/deploy.yml`** — adds the new e2e to the Chromium
batch.

This preserves `--accent` as the canonical app-wide accent token (no
other UI changes) while giving the logo its own brand-defaulted tokens
that the customizer still drives.

## Tests

Red → green commit pair on the branch.

- **NEW: `test-logo-default-sage-teal-e2e.js`** — gates both halves of
the contract:
1. Clean localStorage → navbar + hero CORE = `rgb(207, 217, 201)`, SCOPE
= `rgb(44, 140, 140)`.
2. Seeded `cs-theme-overrides` with red accent → navbar + hero recolor
to red.
- **UPDATED: `test-logo-theme-e2e.js`** — replaces the old "must NOT be
sage" sentinel (sage was a regression marker; it's now the brand
default) with a theme-reactivity probe that overrides `--logo-accent` /
`--logo-accent-hi` directly and asserts the wordmark fill changes.
Duotone, mobile-fit, and clip checks are unchanged.

## Verification

- Default load: sage CORE + teal SCOPE in navbar AND hero ✔ (asserted by
step 1 of the new e2e).
- Customizer override: wordmark follows `accent` / `accentHover` ✔
(asserted by step 2 of the new e2e + the theme-reactivity probe in
`test-logo-theme-e2e.js`).
- Preflight: all hard gates green (PII, branch scope, red commit,
CSS-var defined, CSS self-fallback, LIKE-on-JSON, sync migration); all
warnings green.

## Browser verified

E2E assertion added: `test-logo-default-sage-teal-e2e.js:73` (default
sage), `test-logo-default-sage-teal-e2e.js:124` (customizer override).
CI runs both via `deploy.yml:243`.

Browser verified: covered by Chromium e2e against
`http://localhost:13581` in CI; staging URL TBD on merge.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-07 08:29:02 -07:00
Kpa-clawbot 051c251e7f fix(#1146): WCAG AA contrast for Paths Through This Node links in dark mode (#1159)
Red commit: a4ec258fb82f72b8d5da64492dfe9a5ff4241886 (CI run linked from
`gh pr checks` once it starts)

## Problem
"Paths Through This Node" entries in node detail (side panel
#pathsContent and full-screen #fullPathsContent) render as `<div>`
blocks, not tables. The existing rule

```css
.node-detail-section .data-table td a,
.node-full-card .data-table td a { color: var(--accent); }
```

(public/style.css:1231) only covers `<td a>`, so path-hop links inherit
UA-default `rgb(0,0,238)`. On dark theme that's ~1.8–3.0:1 against
`--card-bg: #1a1a2e` — well under the 4.5:1 WCAG AA body-text floor.

## Fix
Add an explicit rule scoped to `#pathsContent` / `#fullPathsContent`
that uses `var(--accent)` (matching the data-table pattern) plus a
`:hover` to `var(--accent-hover)`. Tracks active theme + customizer
overrides — no hard-coded colours.

After: contrast measured at **6.19:1** in dark mode (link
`rgb(74,158,255)` on `rgb(26,26,46)`).

## TDD
- **Red commit** (`a4ec258`): adds
`test-issue-1146-path-link-contrast-e2e.js` + wires it into the e2e-test
job. Loads a node detail page, mocks `/paths`, forces `data-theme=dark`,
computes WCAG luminance/contrast on the path-hop `<a>`, asserts ≥ 4.5:1.
Reverting only the CSS commit restores the failure.
- **Green commit** (`5ad20fe`): the CSS fix.

E2E assertion added: `test-issue-1146-path-link-contrast-e2e.js:120`
Browser verified: local fixture run on `http://localhost:13591` (build
of `cmd/server` with this branch's `public/`) — 3 passed, 0 failed.

## Files changed
- `public/style.css` (+14 lines, scoped CSS rule + comment)
- `test-issue-1146-path-link-contrast-e2e.js` (new, +132 lines)
- `.github/workflows/deploy.yml` (+1 line, register the new E2E)

## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ all gates pass, no warnings.

Fixes #1146

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-07 06:16:30 -07:00
Kpa-clawbot dfacfd0f6e fix(logo): widen navbar SVG viewBox so CORE/SCOPE wordmark fits (#1141 followup) (#1156)
Fixes #1141 follow-up — the visible-on-staging SCOPE→SCOP clip that the
prior PRs (#1137, #1141) intended to address but didn't.

## What was actually broken (ground truth from staging)

Staging at `http://20.109.157.39:80/` renders the inline navbar SVG
correctly — duotone CORE/SCOPE fills inherit page CSS vars, mobile
mark-only swap fires at ≤400px, customizer logo override path works.
Those parts of #1137 + #1141 landed cleanly.

What did **NOT** land: the SVG `viewBox` was never widened to fit the
rendered Aldrich wordmark. At every desktop viewport the SCOPE `<text
text-anchor="start" x="773.8">` produces a bbox extending to user-space
x≈1112, but the navbar `viewBox="170 10 860 280"` ends at x=1030.
Result: SCOPE renders as **SCOP** on every desktop load. CORE also
slightly overflows the left edge (bbox.x=153.7 < viewBox.x=170).

The original brief premise (mushroom emoji still in `index.html` +
`<img>`-loaded SVG monotone fallback on staging) does not match current
state — `public/index.html:45` already has the inline SVG, staging
renders it, and computed fills are duotone (`rgb(74,158,255)` vs
`rgb(109,179,255)`). The visible bug is geometric clipping, not CSS-var
inheritance or a mushroom revert.

## Fix (one-liner SVG geometry change)

- `public/index.html` — navbar `svg.brand-logo`: `viewBox="170 10 860
280"` → `viewBox="150 10 970 280"`; intrinsic `width="111"` →
`width="125"` (preserves ~36px nav row height).
- `public/style.css` — `.brand-logo { width }` 111px → 125px (desktop),
tablet `@media (max-width:900px)` pin 99px → 112px to keep the new
aspect ratio so wordmark still doesn't clip on tablets.
- `public/customize-v2.js` — `_setBrandLogoUrl` `<img>` swap dimensions
updated to match (when an operator overrides `branding.logoUrl`).

The `≤400px` mobile mark-only swap is unchanged — at narrow widths the
wordmark still hides entirely and the dedicated `.brand-mark-only` SVG
(no `<text>`) renders.

## TDD (red → green)

| commit | role |
|---|---|
| `16b7a60` | **RED** — `test-logo-theme-e2e.js` assertion #7: every
`CORE`/`SCOPE` `<text>` bbox must fit inside the SVG `viewBox`. Master
fails: `[{text:CORE, bboxX:153.7, bboxRight:426.2, vbX:170},
{text:SCOPE, bboxX:773.8, bboxRight:1111.5, vbRight:1030}]` |
| `0db473b` | **GREEN** — widen viewBox + width to fit |

Test exercises real `getBBox()` measurement on a headless Chromium DOM
with the Aldrich webfont loaded — not a unit-test fill string check. The
earlier #1141 tests asserted computed `fill` colors (which were correct)
but never measured rendered geometry; that's the gap.

## Visual proof

**Before** (master HEAD against staging, viewport 1280):

`/tmp/staging-logo-before-1280.png` — SCOPE clearly clipped to "SCOP".

**After** (this branch against local server, viewport 1280):

`/tmp/local-after-1280-screen.png` — full CORE / SCOPE rendered.

**Mobile (after, 375px)**: `/tmp/local-after-mobile.png` — mark-only SVG
(no wordmark, no clip).

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— all hard gates clean (PII, branch-scope, red-commit-genuine,
css-vars-defined, css-self-fallback, like-on-json, sync-migration), all
warnings clean (img-svg-ratio, themed-img-svg, fixture-coverage).

E2E assertion added: `test-logo-theme-e2e.js:286-310`
Browser verified: `/tmp/local-after-1280-screen.png` (local server) +
`/tmp/staging-logo-before-1280.png` (staging baseline).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-06 23:39:10 -07:00
Kpa-clawbot 0d8bc7536c fix(logo): restore duotone fog/teal split + mobile mark-only swap (#1141)
Two related logo fixes bundled together (small scope each).

Cc @user-display-not-by-name.

## 1. Restore duotone (fog/teal) split — the original ask

The M2 (light-theme readability) fix-cycle on #1137 collapsed both
halves of the inline CoreScope wordmark to `var(--logo-text)` so they
would invert correctly on light themes. That restored readability but
erased the original side-split palette.

This change re-uses the existing `--logo-accent` / `--logo-accent-hi`
vars (already driving the left/right node arcs and dots) for the
wordmark too:

- `CORE` → `fill="var(--logo-accent)"` — matches left arcs + left node
dot
- `SCOPE` → `fill="var(--logo-accent-hi)"` — matches right arcs + right
node dot
- chirp polyline + `MESH ANALYZER` tagline → unchanged,
`var(--logo-muted)`

No hardcoded hex; theme customizer overrides via `--accent` /
`--accent-hover` keep working on both themes.

## 2. Fix mobile clipping (SCOPE → "SCOF" at ≤390px)

The full inline wordmark SVG has ~111px intrinsic content; the
`.brand-logo` mobile pin from #1137 (99px width) was squeezing it and
visibly clipping SCOPE.

**Approach:** swap the full wordmark SVG for a dedicated mark-only
inline SVG at ≤400px (option #1 from the design call). Keeps the duotone
arcs, dots, and chirp visible — drops the wordmark cleanly.

- `public/index.html`: CORE/SCOPE wrapped in `<g
class="brand-wordmark">` (clean grouping); new sibling `<svg
class="brand-mark-only">` with tight viewBox `425 15 250 230` covering
both nodes + dots only. Same `--logo-accent` / `--logo-accent-hi` vars →
duotone preserved on mobile.
- `public/style.css`: `.brand-mark-only` defaults `display:none`; new
`@media (max-width:400px)` rule hides `.brand-logo` and shows
`.brand-mark-only`.

## TDD

Three commits, red→green→red→green:

| commit | role |
|---|---|
| `d53d328` | RED — duotone assertions (#4, #5) added; master fails
(CORE === SCOPE) |
| `3e53031` | GREEN — split CORE/SCOPE fills |
| `e6b078f` | RED — mobile mark-only swap assertion (#6) at 360x640;
master fails (no `.brand-mark-only`) |
| `1a3b5db` | GREEN — add the mark-only SVG + media-query swap |

## Files changed

- `test-logo-theme-e2e.js` — assertions expanded from 3/3 to 6/6
- `public/index.html` — duotone fills + brand-wordmark grouping +
brand-mark-only sibling SVG
- `public/home.js` — duotone fills (hero)
- `public/style.css` — `.brand-mark-only` defaults + `@media
(max-width:400px)` swap rule

## Verification

CI Playwright run on commit `3e53031` (after the duotone fix, before the
mobile fix) confirmed assertions 1–5 pass:
- `navbar duotone preserved (dark: CORE=rgb(74,158,255)
SCOPE=rgb(109,179,255); light: CORE=rgb(74,158,255)
SCOPE=rgb(109,179,255))`
- `hero duotone preserved (dark: CORE=rgb(74,158,255)
SCOPE=rgb(109,179,255); light: CORE=rgb(74,158,255)
SCOPE=rgb(109,179,255))`

Final CI run on `1a3b5db` will additionally exercise the 6th (mobile
mark-only swap at 360×640).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-07 05:05:24 +00:00
Kpa-clawbot eae1b915ca feat: load Aldrich webfont for #1137 logo SVG (#1138)
Adds Aldrich webfont so the merged #1137 logo renders in the intended
typeface.

## Problem
The inline SVG logo merged in #1137 declares `font-family="Aldrich,
monospace"` in `public/index.html` and `public/home.js`, but the page
never loaded the Aldrich font face. Browsers silently fell back to
monospace.

## Fix
Self-hosted webfont:
- `public/fonts/aldrich-regular.woff2` — Regular 400, ~16KB, downloaded
from Google Fonts (latin subset). Self-hosted to avoid third-party CDN
dependency, privacy concern, and FOUT delay.
- `@font-face` declaration added at the top of `public/style.css` with
`font-display: swap`.

Aldrich only ships in 400; the SVG `font-weight="700"` on the wordmark
synthesizes bold (matches the design intent of #1137).

## TDD
- Red commit: E2E test asserting `document.fonts.check('1em Aldrich')`
is true and the navbar SVG `<text>` `font-family` contains "Aldrich".
Without the font face declaration, both assertions fail on an assertion
(not a build error).
- Green commit: adds the woff2 + `@font-face` rule, both assertions
pass.

## Files
- `public/fonts/aldrich-regular.woff2` (new, 16460 bytes)
- `public/style.css` — `@font-face` rule
- `test-e2e-playwright.js` — new test

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-06 21:24:57 -07:00
Kpa-clawbot 494d3022f9 Partial fix for #1128: close audit gaps (z-scale, css-var lint, multi-viewport E2E, Bug 1+5 polish) (#1133)
## Partial fix for #1128 — closes the gaps PR #1131 left behind

PR #1131 was a partial fix for the packets-page layout chaos
(merged 2026-05-06 ~01:55 UTC, then the issue was reopened by the
maintainer). #1131 shipped Bug 4 (`--surface` definition), the
`.path-popover` flip + lower z-index, the debounced re-measure for
Bug 1, the `.filter-bar` row-gap + `.multi-select-trigger`
truncation for Bug 3, the new z-index TOKENS, and a single-viewport
E2E with five individual-component assertions.

This PR closes everything else the issue body and the
`specs/packets-layout-audit.md` audit asked for.

### What changed (per gap)

**Gap A — apply the z-index scale (audit Section 2)**
#1131 added `--z-dropdown` / `--z-popover` / `--z-modal` /
`--z-tooltip` but explicitly left existing literal values in place.
This PR renumbers the 7 dropdowns/popovers the audit named:

| Selector | Before | After |
|---|---:|---:|
| `.col-toggle-menu` | 50 | `var(--z-dropdown)` (100) |
| `.multi-select-menu` | 90 | `var(--z-dropdown)` |
| `.region-dropdown-menu` | 90 | `var(--z-dropdown)` |
| `.node-filter-dropdown` | 100 | `var(--z-dropdown)` |
| `.fux-saved-menu` | `var(--z-tooltip)` (9200) | `var(--z-dropdown)` |
| `.fux-ac-dropdown` | `var(--z-tooltip)` | `var(--z-dropdown)` |
| `.hop-conflict-popover` | `var(--z-tooltip)` | `var(--z-popover)`
(300) |

`.fux-ctx-menu` deliberately retains the tooltip band — context
menus must float above all toolbar UI. `.region-filter-options-menu`
no longer exists in the source (was renamed
`.region-dropdown-menu`).

The `style.css` doc-block at the top is rewritten to record the
applied scale and to point operators at the new lint.

**Gap B — CSS-var lint (audit Section 5 #1, "single highest-value
addition")**
Adds `scripts/check-css-vars.js` (~70 lines). Walks
`public/*.css`, extracts every `var(--name)` reference WITHOUT a
fallback, asserts the name is defined in some `public/*.css`.
References WITH a fallback are tolerated. Wired into CI in the
`go-test` job before the JS unit tests.

The red commit (`608d81f`) shipped this lint exiting 1 against the
master tree — three undefined vars that bypassed earlier review:

```
public/style.css:2628  var(--text-primary)
public/style.css:2675  var(--bg-hover)
public/style.css:2924  var(--primary)
```

The green commit (`1369d1e`) defines those three as aliases in the
:root block (`--text-primary` → `--text`, `--bg-hover` →
`--hover-bg`, `--primary` → `--accent`). Light + dark themes
inherit through the existing tokens.

**Gap C — multi-viewport E2E (issue acceptance criterion)**
Adds `test-issue-1128-multi-viewport-e2e.js` — sister of the
existing single-viewport test. At each of three viewports
(1280×900, 1080×800, 768×1024):

  - takes a screenshot to `e2e-screenshots/issue-1128-<viewport>.png`
  - asserts no two `.filter-group` siblings vertically overlap
  - on desktop+laptop, opens the Saved menu and the Types
    multi-select and asserts the dropdown does not vertically
    overlap any `.filter-group` below it

Plus three viewport-agnostic assertions:

  - dropdown selectors compute z-index in `[100,199]`
    (`.col-toggle-menu`, `.multi-select-menu`,
    `.region-filter-options-menu`, `.fux-saved-menu`,
    `.fux-ac-dropdown`)
  - `.path-hops .hop / .hop-named / .arrow` compute
    `line-height ≤ 18px`
  - `.col-path` computes `height ≤ 28px`

Wired into the e2e-test job after the existing #1128 test.

**Gap D — Bug 5 polish (toolbar reorder)**
Audit Section 3 Bug 5: swaps `filter-group-dropdowns` and
`filter-group-toggles` in `public/packets.js` so time range +
Group by Hash + ★ My Nodes sit next to the search input. Pure
markup reorder. No CSS / no JS-handler changes.

**Gap E — Bug 1 belt-and-suspenders**
Audit Section 3 Bug 1 sub-bullets:

  - locks `.path-hops .hop / .hop-named / .arrow` to
    `line-height: 18px` so a chip with mixed font metrics cannot
    overflow the 22px host vertically and bleed into the row above
  - converts `.col-path { max-height: 28px }` → `height: 28px`
    because browsers widely ignore `max-height` on `<td>`s; the
    earlier rule was a no-op

### TDD discipline (red → green)

```
$ git log --oneline origin/master..HEAD
68b0426 fix(#1128): Bug 5 — toolbar group reorder (toggles before dropdowns)
6d16e6f fix(#1128): apply z-index scale to dropdowns + Bug 1 chip line-height lock
b9850c9 fix(check-css-vars): strip /* ... */ comments before scanning
1369d1e fix(#1128): define --text-primary, --bg-hover, --primary aliases (lint green)
0d4660f test(#1128): multi-viewport E2E + wire CSS-var lint into CI (red commit)
608d81f test(#1128): add scripts/check-css-vars.js — fails on 3 undefined vars (red commit)
```

Both red commits (`608d81f`, `0d4660f`) were verified to fail
locally before the green commits landed:

  - `608d81f` runs the lint and exits 1 on the three undefined vars
    listed above (proven against master).
  - `0d4660f` introduces the multi-viewport E2E and wires the lint
    into CI — the lint then fails the build on master, and the E2E
    z-scale assertion fails because pre-fix `.col-toggle-menu` is
    50, the multi-selects are 90, etc.

### Acceptance criteria status

From the original issue body:
  -  Bug 4 root cause fixed (#1131 + this PR's lint guard)
  -  Bug 1 chip-spill (debounced re-measure from #1131 +
       line-height lock + col-path height fix from this PR)
  -  Bug 2 +N popover positioning (#1131)
  -  Bug 3 toolbar overlap (#1131 + #1131 row-gap)
  -  Bug 5 group reorder (this PR)
  -  Z-index scale documented + applied (this PR)
  -  E2E screenshots at multiple viewports (this PR)
  -  Bounding-rect collision detection on visible interactive
       elements (this PR — `.filter-group` siblings + dropdown vs.
       toolbar)
  -  CSS-var lint in CI (this PR)

### Why this is "Partial fix for #1128", not "Fixes #1128"

Per `AGENTS.md` rule 34, automated closure is reserved for the
operator after they verify on staging. The acceptance criteria
above appear satisfied in code, but the user should confirm the
visual outcome on staging before closing.

### Files changed

- `scripts/check-css-vars.js` (new — ~70 lines)
- `test-issue-1128-multi-viewport-e2e.js` (new)
- `.github/workflows/deploy.yml` (lint step + e2e step wiring)
- `public/style.css` (z-renumber, doc-block, Bug 1 polish, alias defs)
- `public/packets.js` (Bug 5 reorder)

Refs #1128, follows #1131

---------

Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-07 03:41:12 +00:00
Kpa-clawbot 364c5766fc feat(logo): wire new CoreScope SVG logo into navbar + home hero (#1137)
## Adds new logo and home hero

Replaces the navbar mushroom emoji + "CoreScope" text spans with the new
CoreScope SVG mark, and adds a hero SVG (with the MESH ANALYZER tagline)
above the home page H1.

### What changed
- `public/img/corescope-logo.svg` — navbar mark, no tagline (locked
"aggressive low-amp chirp" variant: facing-arcs + low-amp chirp
connector between the two nodes).
- `public/img/corescope-hero.svg` — home hero version, includes the MESH
ANALYZER tagline.
- `public/index.html` — replaces `<span class="brand-icon">🍄</span><span
class="brand-text">CoreScope</span>` with `<img class="brand-logo"
src="img/corescope-logo.svg?__BUST__" …>`. `.nav-brand` link still
routes to `#/`. `.live-dot` retained.
- `public/style.css` — adds `.brand-logo { height: 36px }` (32px on
tablet ≤900px). Existing 52px nav height unchanged.
- `public/home.js` / `public/home.css` — adds `<img
class="home-hero-logo">` above the hero `<h1>`, sized `max-width:
min(720px, 90vw)` and centered.

### TDD
Red→green is visible in the branch:
- `3159b82` — `test(logo): add failing E2E …` (red commit). Adds
`test-logo-rebrand-e2e.js` and wires it into the `e2e-test` job in
`deploy.yml` with `CHROMIUM_REQUIRE=1`. On this commit `index.html`
still has the emoji + text spans, `home.js` has no hero img, and the SVG
asset files do not exist — the test asserts on each so CI fails on
assertion.
- `19434e1` — `feat(logo): wire new CoreScope SVG logo …` (green
commit). Implements the fix.

### E2E asserts
1. `.nav-brand img` exists with `src` ending `corescope-logo.svg`
2. legacy `.brand-icon` / `.brand-text` are gone
3. `.live-dot` is present, visible, and to the right of the logo (no
overlap)
4. `.home-hero img.home-hero-logo` exists with `src` ending
`corescope-hero.svg`, positioned BEFORE the `<h1>`
5. both `/img/corescope-{logo,hero}.svg` return 200 with svg
content-type

### Customizer compatibility
- `customize.js` still does `querySelector('.brand-text')` /
`.brand-icon` for live branding updates. Both now return `null`;
existing `if (el)` guards make those branches silent no-ops. **No JS
errors, but the customizer's `branding.siteName` and `branding.logoUrl`
fields no longer rewrite the navbar brand** — the brand is now a fixed
SVG asset.
- **Theme accent does NOT recolor the SVG.** SVGs loaded via `<img src>`
are isolated documents and cannot inherit document CSS variables; the
SVG falls back to its embedded brand colors. This is appropriate for a
brand mark; if recoloring per theme is desired later, swap to inline SVG
(separate PR).

### Browser validation
Local Chromium not available in this env; the E2E test soft-skips
locally and hard-fails in CI (`CHROMIUM_REQUIRE=1`). Server-side checks
done locally:
- `curl http://localhost:13581/` → confirmed `<img class="brand-logo"
src="img/corescope-logo.svg?<bust>" …>` rendered, no
`.brand-icon`/`.brand-text` spans.
- `curl -I /img/corescope-logo.svg` and `/img/corescope-hero.svg` → both
200.

### Performance
No hot-path changes. Two new static SVG assets (~7.6KB each), served
directly by the Go static handler. Cache-busted via `?__BUST__`
(auto-replaced server-side).

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
2026-05-06 19:17:46 -07:00
Kpa-clawbot b03ef4abd3 fix(packets): resolve --surface undefined + z-index scale + path chip re-measure + +N popover (#1128) (#1131)
## Summary

Resolves the **5 layout bugs** documented in
`specs/packets-layout-audit.md` from the issue investigation. All fixes
shipped in one PR per the audit's recommended fix order.

Fixes #1128

### Bug 4 (P0, 1 line) — `--surface` undefined
`var(--surface)` was referenced in **8** rules across `style.css`
(`.fux-saved-menu`, `.fux-popover`, `.path-popover`, `.fux-ac-dropdown`,
`.fux-ctx-menu`, `.path-overflow-pill:hover`,
`.fux-saved-trigger:hover`, `.fux-popover-header sticky`) but the
variable was **never defined** — every caller resolved to `transparent`
and row content bled through. Aliased `--surface: var(--surface-1);` in
the `:root`, `@media (prefers-color-scheme: dark)`, and
`[data-theme="dark"]` blocks.

### Z-index scale (foundational)
Added documented custom properties at the top of `style.css`:

```css
--z-base: 0;
--z-dropdown: 100;
--z-popover: 300;
--z-modal-backdrop: 9000;
--z-modal: 9100;
--z-tooltip: 9200;
```

New code uses these tokens. Existing working values left in place to
avoid behavioural risk.

### Bug 1 — path chip re-measure
`_finalizePathOverflow` runs **before** `hop-resolver` mutates chip text
from hex prefix → longer node name. Chips that fit on first measurement
overflow once names resolve, but the `+N` pill never gets appended.
Cleared the per-host `overflowChecked` guard and re-ran finalize on a
120 ms debounced timer, so post-resolution overflow is detected.

### Bug 2 — `+N` popover position + z-index
`.path-popover` was `z-index: 10500` (above the modal stack) and only
ever positioned **below** the pill — when near the bottom of the
viewport it hung over adjacent rows. Lowered to `var(--z-popover)`
(300), capped `max-height` from `60vh` → `240px`, and added flip-above
logic when there isn't room below.

### Bug 3 — filter-bar gap + multi-select truncation
`.filter-bar { row-gap: 6px }` was too tight for the 34px controls;
bumped to `12px`. `.multi-select-trigger` had no `max-width`, so a
selection like `"TRACE,MULTIPART,GRP_TXT"` ballooned the row and
overlapped toolbar buttons. Capped `max-width: 180px` with
`text-overflow: ellipsis` and surfaced the full selection in the
trigger's `title` attribute (so the value remains discoverable).

### Bug 5 — already addressed in #1124
Verified `.filter-group` structure prevents mid-cluster wrap; no further
change needed here.

## TDD

Branch shows the required **red → green** sequence:

| commit | result |
|---|---|
| `8ad6394` test(packets): red E2E for issue #1128 layout chaos | ✗ Bug
4 (alpha=0), ✗ Bug 2 (z=10500), ✗ Bug 3 (gap=6) |
| `eacadc1` fix(packets): resolve --surface undefined + z-index scale +
... | ✓ 5/5 |

Test file: `test-issue-1128-packets-layout-e2e.js` — asserts opaque
dropdown background, every overflowing `.path-hops` has a `+N` pill,
popover z-index ≤ 9000 + anchored to pill, filter-bar gap ≥ 10px,
trigger `max-width` bounded.

## E2E

Local run against the e2e fixture:

```
=== #1128 packets layout E2E ===
  ✓ navigate to /packets and wait for table + rows
  ✓ Bug 4: Saved-filter dropdown background is OPAQUE (alpha ≥ 0.99)
  ✓ Bug 1: every overflowing .path-hops has a .path-overflow-pill
  ✓ Bug 2: +N popover anchored to pill + z-index ≤ 9000
  ✓ Bug 3: .filter-bar row-gap ≥ 10px AND .multi-select-trigger has bounded max-width
=== Results: passed 5 failed 0 ===
```

CI hookup: please add `node test-issue-1128-packets-layout-e2e.js`
alongside the other `test-issue-XXXX-*-e2e.js` invocations in
`.github/workflows/deploy.yml` (line ~226).

## Files

- `public/style.css` — `--surface` definition × 3 blocks, z-index scale
tokens, `.path-popover`, `.filter-bar`, `.multi-select-trigger`
- `public/packets.js` — flip-above popover logic, debounced re-finalize,
trigger `title`
- `test-issue-1128-packets-layout-e2e.js` — new E2E (red → green)

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
2026-05-05 19:19:19 -07:00
Kpa-clawbot f86a44d6b5 fix(packets): filter UX disaster — modal help, single header, bounded path rows (#1122) (#1124)
## Summary
Fixes #1122 — Packets page filter UX repairs.

## Bugs addressed
1. **Filter syntax help no longer floats over the packet table.** It now
opens inside a real `.modal-overlay` backdrop and is centered via the
existing `.modal` flex pattern (same pattern as BYOP).
2. **Duplicate "Filter syntax" header removed.** The inner `<h3>` was
redundant with the popover header `<strong>`. Help body now contains
exactly one occurrence.
3. **Path column chips no longer spill into adjacent rows.**
`.path-hops` now uses `flex-wrap: nowrap` + `max-height: 22px` +
`overflow: hidden`; individual `.hop-named` chips cap at `max-width:
120px` with ellipsis. `td.col-path` itself caps at `max-height: 28px` so
a long hop chain can never push the row past 28px regardless of hop
count.
4. **Toolbar grouping documented.** Added a fenced section comment in
`style.css` enumerating the four logical clusters (quick filters /
toggles / time window / sort & view), bumped `.filter-bar` gap from
6→8px and added `row-gap: 6px` so wrapped controls stay readable at
narrow widths.

## Test
TDD red→green. New Playwright E2E
`test-issue-1122-packets-filter-ux-e2e.js` asserts:
- Help panel rect does not overlap any visible `#pktBody` row, and a
`.modal-overlay` backdrop is present.
- Help panel contains exactly 1 `Filter syntax` occurrence (not 2).
- Every rendered `.col-path` cell stays under 60px height.

Wired into `.github/workflows/deploy.yml` Playwright fail-fast step. Red
commit: `bd58634` (test only). Green commit: `c580254` (impl).

## Files
- `public/filter-ux.js` — `_showHelp` wraps the popover in
`.modal-overlay`; `_buildHelpHtml` drops the duplicate `<h3>Filter
syntax</h3>`.
- `public/style.css` — `.modal-overlay > .fux-popover` reset,
`.path-hops` clipping, `td.col-path` height cap, `.filter-bar` section
comment.
- `test-issue-1122-packets-filter-ux-e2e.js` — new Playwright E2E.
- `.github/workflows/deploy.yml` — runs the new E2E.

---------

Co-authored-by: clawbot <clawbot@example.com>
Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-05 18:12:33 -07:00
Kpa-clawbot c84ec409c7 fix(nav): #1102 — JS-driven Priority+ measures actual fit (#1104)
## Summary

Fixes #1102 — regression from PR #1097 polish where Priority+ collapsed
too aggressively at wide widths and the "More" menu didn't reflect what
was actually hidden.

## Root cause

Two bugs, one root: the post-#1097 CSS rule

```css
.nav-links a:not([data-priority="high"]) { display: none; }
```

unconditionally hid 6 of 11 links at every width ≥768px — including
2560px where everything fits comfortably. The "More" menu populator
(`querySelectorAll('.nav-links a:not([data-priority="high"])')`) ran
exactly once on load against that same selector, so it always held the
same 6 links and never reflected the actual viewport fit.

## Fix

Replace the static CSS hide with a JS measurement pass
(`applyNavPriority` in `public/app.js`):

1. Start with **all** links visible inline.
2. Compute `needed = brand + gutters + visible-links + More + nav-right
+ safety` and compare to `window.innerWidth`.
3. If it doesn't fit, mark the rightmost (lowest-priority) link
`.is-overflow` and re-measure. Repeat. High-priority links are queued
last so they're kept visible as long as possible.
4. Rebuild the "More ▾" menu from whatever currently has `.is-overflow`.
When nothing overflows, hide the More wrap entirely (`.is-hidden`).
5. Re-run on resize (rAF-debounced), on `hashchange` (active-link
padding shifts), and after fonts load.

Why JS, not CSS: the breakpoint where each link drops depends on label
width, gutters, active-link padding, and the nav-stats badge — none of
which are addressable from a media query.

## TDD trail

- Red commit `8507756`: `test-nav-priority-1102-e2e.js` — fails 2/4
(2560 and 1920 only show 5/11).
- Green commit `3e84736`: implementation — passes 4/4.

## E2E

`test-nav-priority-1102-e2e.js` asserts:
- 2560px → all 11 visible, More hidden
- 1920px → ≥9 visible
- 1080px → 5+ visible AND More menu contains every hidden link
- 800px → 5+ visible AND More menu non-empty

Local run on the e2e fixture: **4/4 pass**. Existing
`test-nav-fluid-1055-e2e.js` also stays green: **20/20 pass** (no
overlap, no overflow at 768/1024/1280/1440/1920 across 4 routes).

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 11:18:07 -07:00
Kpa-clawbot 52bb07d6c1 feat(#1056): fluid tables + +N hidden pill (packets/nodes/observers) (#1099)
## Summary

Implements priority-based responsive column hiding for the three primary
data tables (Packets, Nodes, Observers) per the parent task #1050
acceptance criteria, with a clickable **+N hidden** pill in the table
header to reveal collapsed columns.

## Approach

- New `TableResponsive` helper (defined once at the top of `packets.js`,
exposed on `window`) classifies `<th data-priority="N">` cells:
  - `1` = always visible
  - `2` = hide when viewport ≤ 1280
  - `3` = hide ≤ 1080
  - `4` = hide ≤ 900
  - `5` = hide ≤ 768
- Higher priority numbers drop first. The matching `<td>` cells in
`tbody` are tagged via `.col-hidden` (colspan-aware mapping).
- A `.col-hidden-pill` `<button>` is appended to the last visible
`<th>`. Clicking it sets a per-table reveal flag and clears all hidden
classes. Re-runs on `window.resize` (debounced) and a `ResizeObserver`
on the wrapping element.
- Each of `packets.js` / `nodes.js` / `observers.js` wraps its primary
table in `.table-fluid-wrap` and calls `TableResponsive.register` after
initial render.
- `style.css` removes legacy `min-width: 720px / 480px` floors on the
primary tables (which forced horizontal scroll) and lets columns flex
via `table-layout: auto` with `.col-time` switched to `clamp(72px, 8vw,
108px)`.

Per-column priorities chosen so identifier columns stay visible
(Time/Hash/Type/Name/Status) while numeric/secondary columns collapse
first.

## Files changed (matches Hard rules — only these)

- `public/packets.js` (`#pktTable` + `TableResponsive` helper)
- `public/nodes.js` (`#nodesTable`)
- `public/observers.js` (`#obsTable`)
- `public/style.css` (table sections only)
- `test-table-fluid-e2e.js` (new E2E)

## E2E

`BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js` — covers
all three tables at 768/1080/1440 viewports, asserting:

- No horizontal table overflow within `.table-fluid-wrap`
- Visible `+N hidden` pill at narrow widths with the count `N` matching
the number of `th.col-hidden` cells
- Clicking the pill clears all `.col-hidden` classifiers (reveals every
column)

## Manual verification in openclaw browser (local fixture server)

| Page      | Viewport | Hidden | Pill         |
|-----------|---------:|-------:|--------------|
| observers |      768 |      8 | `+8 hidden`  |
| packets   |      768 |      7 | `+7 hidden`  |
| packets   |     1080 |      4 | `+4 hidden`  |
| nodes     |      768 |      3 | `+3 hidden`  |
| nodes     |     1440 |      0 | (no pill)    |

Pill click verified to reveal all columns.

## TDD

- Red commit: `5ad7573` — failing E2E (no `.col-hidden-pill` exists yet)
- Green commit: `7780090` — implementation; test passes manually against
fixture server.

Fixes #1056

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-05 08:45:43 -07:00
Kpa-clawbot 85b8c8115a feat(channels): fluid sidebar + container-query stacking (#1057) (#1095)
## Summary

Makes the channels page sidebar + message area fluid as part of the
parent #1050 fluid-layout effort. Replaces the hardcoded
`.ch-sidebar { width: 280px; min-width: 280px }` with
`width: clamp(220px, 22vw, 320px); min-width: 220px`. Adds an
`@container` query (via `container-type: inline-size` on `.ch-layout`)
that stacks the sidebar above the message area when the channels
page itself is narrow (≤700px container width) — independent of
the global viewport, so it adapts even when an outer panel is
consuming width. Removes the legacy `@media (max-width: 900px)`
fixed 220px override; the clamp + container query handle that range.

`.ch-main` already used `flex: 1`, so it absorbs all remaining width
including ultrawides. The existing mobile (≤640px) overlay rules and
the JS resize handle in `channels.js` are untouched and still work
(user drag still wins via inline width).

Fixes #1057.

## Scope

- `public/style.css` — channels section only
- (no `public/channels.js` changes needed)

## Tests

TDD: red commit (failing tests) → green commit (implementation).

- `test-channel-fluid-layout.js` (new): static CSS assertions
  - `.ch-sidebar` uses `clamp()` for width (not fixed px)
  - `.ch-sidebar` keeps a sane `min-width` (200–280px)
  - `.ch-main` keeps `flex: 1`
  - `.ch-layout` declares `container-type` (container query root)
  - `@container` rule scopes channels stacking
- legacy `@media (max-width: 900px) .ch-sidebar { width: 220px }` is
gone
- `test-channel-fluid-e2e.js` (new): Playwright E2E at
  768 / 1080 / 1440 / 1920 (wide) and 480 (narrow). Asserts:
  - no horizontal scroll on the body
  - sidebar AND message area both visible side-by-side at ≥768px
  - sidebar consumes ≤45% of viewport, main ≥40%
  - at 480px the layout stacks (or overlays) — no overflow

Wired into `test-all.sh` and the unit + e2e steps of
`.github/workflows/deploy.yml`.

## Verification

- Static unit test: 6/6 pass on the green commit, 4/6 fail on the
  red commit (only the two trivially-true assertions pass).
- Local Go server boot: `corescope-server` serves the updated
  `style.css` containing `container-type: inline-size`,
`clamp(220px, 22vw, 320px)`, and `@container chlayout (max-width:
700px)`.
- Local Chromium on the dev sandbox is musl-incompatible
  (Playwright fallback build crashes with `Error relocating ...:
  posix_fallocate64: symbol not found`), so the E2E was not run
  locally. CI will run it on Ubuntu runners.

---------

Co-authored-by: clawbot <clawbot@example.com>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 08:31:37 -07:00
Kpa-clawbot d1e6c733dc fix(nav): apply Priority+ at all widths (#1055) (#1097)
## Summary

Make the top-nav use the **Priority+ pattern at all widths** (not just
768–1279px), so the nav-right cluster never gets pushed off-screen or
visually overlapped by the link strip.

Fixes #1055.

## What changed

**`public/style.css`** — nav section only (clearly fenced):
- Removed the upper bound on the Priority+ media query (`max-width:
1279px`). The rule now applies at any viewport `>= 768px`. Above that
breakpoint, only `data-priority="high"` links render inline; the rest
collapse into the existing `More ▾` overflow menu.
- Swapped nav-only hardcoded spacing/type to the fluid `clamp()` tokens
shipped in #1054:
  - `.top-nav` padding → `var(--gutter)`
  - `.nav-left` gap → `var(--space-lg)`
  - `.nav-brand` gap → `var(--space-sm)`, font-size → `var(--fs-md)`
  - `.nav-links` gap → `var(--space-xs)`
- `.nav-link` padding → `clamp(8px, 0.6vw + 4px, 14px)`, font-size →
`var(--fs-sm)`
  - `.nav-right` gap → `var(--space-sm)`
- Mobile (<768px) hamburger layout, the More-menu markup, and the JS
that builds the menu in `public/app.js` are unchanged — they already
supported this pattern.

`public/index.html` did not need changes — the `data-priority="high"`
markup, `nav-more-wrap`, `navMoreBtn`, and `navMoreMenu` are already in
place from earlier work.

## Why the bug existed

The previous Priority+ rule was scoped `@media (min-width: 768px) and
(max-width: 1279px)`. From 1280px–~1599px the full 11-link strip
rendered but didn't fit alongside `.nav-stats` + `.nav-right`. The
parent `overflow: hidden` masked the symptom, but the rightmost links
physically rendered underneath `.nav-right` and were unreachable.

## E2E assertion added

New `test-nav-fluid-1055-e2e.js` — Playwright multi-viewport test
(768/1024/1280/1440/1920) that asserts:

1. `.nav-right.right` ≤ `document.documentElement.clientWidth` (no
horizontal overflow)
2. Last visible `.nav-link.right` ≤ `.nav-right.left` (no overlap
underneath the right cluster)
3. `.top-nav.scrollWidth` ≤ `.top-nav.clientWidth` (no scrolled-off
content)

Wired into the `e2e-test` job in `.github/workflows/deploy.yml`.

**TDD evidence:**
- Red commit `466221a`: test passes 3/5 (1024/768/1920) — fails at 1280
(253px overlap) and 1440 (93px overlap).
- Green commit `1aa939a`: test passes 5/5.

## Acceptance criteria (from #1055)

- [x] Priority+ at ALL widths (not just mobile).
- [x] No nav link overflow at 1080px (or any tested width).
- [x] Overflow menu accessible via keyboard + touch (existing
`navMoreBtn` aria-haspopup wiring; verified by existing app.js
handlers).
- [x] Active route still highlighted when in overflow (existing logic in
`app.js` adds `.active` to the cloned link in `navMoreMenu`).
- [x] Tested at 768/1024/1280/1440/1920 — visible link count adapts (5
priority links + More menu at all desktop widths; full 11 inline only on
hamburger mobile when expanded).

---------

Co-authored-by: bot <bot@corescope>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 08:19:51 -07:00
Kpa-clawbot b52a938b27 fix(#1059): map controls + modals — fluid + safe max-height (#1096)
## Summary

Fixes #1059 — Task 6 of #1050. Makes map controls + modals fluid and
safely capped so they work across 768px–2560px viewports.

## Changes

`public/style.css` only — modal section + map-controls section (per task
scope).

### Map controls (`.map-controls`)
- `width: clamp(160px, 18vw, 240px)` — fluid, scales with viewport.
- `max-width: calc(100vw - 24px)` — never overflows narrow viewports.
- Eliminates horizontal scroll on the map page at
768/1024/1440/1920/2560.

### Modal box (`.modal`)
- `max-height: 80vh → 90vh` (spec §3).
- `width: min(90vw, 500px)` — fluid, drops to 90vw below 555px.
- `position: relative` so sticky descendants anchor to the modal box.
- `.modal-overlay` gets `padding: clamp(8px, 2vw, 24px)` for edge
breathing room.

### BYOP modal sticky close
- `.byop-header { position: sticky; top: 0 }` with `var(--card-bg)`
backdrop and bottom border — the title bar + ✕ stay reachable while the
body scrolls.
- `.byop-x` restyled with border, hit area, hover state.

### Untouched (intentional)
- `public/map.js` did not need changes — the `.map-controls` element is
the only narrow-viewport offender; the markup stays identical.
- Channel modals (`.ch-modal*`, `.ch-share-modal*`) already have their
own width/max-width tokens from #1034/#1087 and are out of scope for
this task.

## TDD

- **Red commit** `b69e992`: `test-map-modal-fluid-e2e.js` asserts (a) no
horizontal scroll on `/#/map` at 1024/1440/1920/2560, (b)
`.map-controls` right edge inside viewport at 768px wide, (c) BYOP modal
at 1024×768 has `height ≤ 90vh`, `overflow-y: auto|scroll`, and close
button is `position: sticky` and reachable. All assertions fail against
the previous CSS (fixed-width 220px controls overflow at narrow widths;
modal max-height was 80vh, not 90vh; close button was `position:
static`).
- **Green commit** `3e6df9d`: CSS changes above; all assertions pass.

## E2E

- Wired into `.github/workflows/deploy.yml` after the channel-1087 E2E:
  ```
  BASE_URL=http://localhost:13581 node test-map-modal-fluid-e2e.js
  ```

## Acceptance criteria

- [x] Map controls do not overlap markers at narrow viewports (fluid
clamp width + max-width).
- [x] Map fills extra space on ultrawide (panel caps at 240px, leaflet
flex:1 takes the rest — already true; controls no longer steal grow
room).
- [x] Modals: `max-height: 90vh`, internal scroll, sticky close button,
max-width via `min()`.
- [x] No modal can exceed viewport height at any tested width.
- [x] Verified via E2E at 768/1024/1440/1920/2560.

## Out of scope (left for sibling tasks under #1050)

- Tab bars / nav (Task 1050-1, blocker).
- Filter bars and table chrome (other 1050-N tasks).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-05 08:10:11 -07:00
Kpa-clawbot 6d17cac40e fix(#1058): fluid + container-queried analytics chart grid (#1098)
## Summary
Makes the analytics chart grid fluid and auto-stacking based on its
**own** width rather than the viewport's. Implements task 5 of #1050.

## What changed
- `public/style.css` — `.analytics-charts` section only:
- Replaced `grid-template-columns: 1fr 1fr` with `repeat(auto-fit,
minmax(min(100%, 380px), 1fr))` so columns wrap when intrinsic space is
too narrow.
- Added `container-type: inline-size` so the grid is a query container
and descendants/future tweaks can size against its own width rather than
the viewport. The `auto-fit minmax` already handles the stack-on-narrow
case, so the previously-included `@container (max-width: 800px)` rule
was redundant and has been dropped to keep one source of truth.
- `min-width: 0` on cards and `max-width: 100%; height: auto` on
`<svg>`/`<canvas>` (descendant selector, robust to wrapper elements
between the card and the chart media) to prevent intrinsic-content
overflow.
- Switched hardcoded `12px` / `16px` spacing to the #1054 tokens
`--space-sm` / `--space-md`.
- Removed the redundant `@media (max-width: 768px) { .analytics-charts {
grid-template-columns: 1fr; } }` rule (the fluid grid supersedes it).

No `analytics.js` / `node-analytics.js` markup changes were required —
the existing classes are reused.

## TDD
- **Red commit (47f56e9)** — `test-analytics-fluid-charts.js`: failing
E2E that loads `public/style.css` against a sized harness and asserts no
overflow + correct stacking. On master: assertion failures on
container-type opt-in + wide-viewport / narrow-container stacking.
- **Green commit (d300dfa)** — CSS fix; all assertions pass.

## E2E (mandatory frontend coverage)
`node test-analytics-fluid-charts.js` — Playwright + Chromium against a
`file://` harness, 8/8 assertions:
- `.analytics-charts` opts in to container queries (`container-type:
inline-size`)
- viewport 1440 / wrapper 1300px → side-by-side (≥2 cols), no overflow
- viewport 1080 / wrapper 1040px → no horizontal overflow
- viewport 768 / wrapper 760px → cards stack to 1 column, no overflow
- viewport 1440 / wrapper 600px → cards stack via fluid grid (the
original bug)
- viewport 1920 / wrapper 1880px → side-by-side (≥2 cols), no overflow
(AC4)
- viewport 2560 / wrapper 2520px → side-by-side (≥2 cols), no overflow
(AC4)
- AC3: open at 1440px wide (side-by-side), shrink wrapper to 760px /
viewport to 768px, assert layout reflows to 1 column (charts redraw on
resize, not stuck at initial value)

`node test-fluid-scaffolding.js` — still green (15/15), confirms #1054
tokens are unaffected.

Partial fix for #1058

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 08:04:25 -07:00
Kpa-clawbot ac0cf5ac7d fix(channels): #1087 QR library + share modal + PSK persistence (#1090)
Red commit: 5def4d073c61058fc9f327a3c60ece27e21cbc69 (CI run pending —
see Checks tab)

Fixes #1087

## What's broken (4 bugs)
1. **"QR library not loaded"** — `channel-qr.js` checked `root.QRCode`
(capital), but the vendored library exports lowercase `qrcode` (Kazuhiko
Arase API). Generate & Show QR always fell into the "library not loaded"
branch.
2. **QR encodes `name=psk:hex`** — the Share button (and parts of the
Generate path) passed the internal `psk:<hex8>` lookup key to
`ChannelQR.generate`, ignoring the user's display label stored in
`LABELS_KEY`.
3. **PSK channel doesn't persist on refresh** — the persistence path was
scattered, and the read-back wasn't verified. Added channels disappeared
on refresh and "reappeared" only when a later add ran the persist hook.
4. **Share button reuses the Add Channel modal** — wrong intent reuse
(Add = INPUT, Share = OUTPUT). Replaced with a dedicated `#chShareModal`
(separate DOM id, separate title, share-only affordances, privacy
warning).

## TDD
Red commit (this) lands ONLY the failing tests:
- `test-channel-issue-1087.js` — source-string contract assertions for
all 4 bugs
- `test-channel-issue-1087-e2e.js` — Playwright E2E covering generate →
QR render, QR display name, persistence across refresh, Share opens
dedicated modal

Green commit (follow-up) lands the production fixes.

## E2E assertion added
E2E assertion added: test-channel-issue-1087-e2e.js:55

## CI wiring
- `test-channel-issue-1087.js` added to `.github/workflows/deploy.yml`
(go-test JS unit step) + `test-all.sh`
- `test-channel-issue-1087-e2e.js` added to
`.github/workflows/deploy.yml` (e2e-test step)

---------

Co-authored-by: bot <bot@corescope>
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 03:24:52 -07:00
Kpa-clawbot f7d8a7cb8f feat(packets): filter UX — in-UI docs + autocomplete + right-click + saved filters (#966) (#1083)
## Summary

Implements the full filter-input UX upgrade from #966 — Wireshark-style
help, autocomplete, right-click-to-filter, and saved filters.

Closes #966.

## Surfaces

### A. Help popover (ⓘ button next to filter input)
Auto-generated from `PacketFilter.FIELDS` / `OPERATORS` so it stays in
sync with the parser. Includes:
- Syntax overview (boolean ops, parens, case-insensitivity,
URL-shareable filters)
- Full field reference (27 entries: top-level + `payload.*`)
- Full operator reference with one example per op
- 10 ready-to-paste examples
- Tips (right-click, autocomplete, save)

### B. Autocomplete dropdown
- Type partial field name → field suggestions (top-level + dynamic
`payload.*` keys discovered from visible packets)
- Type `field` → operator suggestions
- Type `type ==` → list of canonical type values (`ADVERT`, `GRP_TXT`,
…)
- Type `route ==` → list of route values (`FLOOD`, `DIRECT`,
`TRANSPORT_FLOOD`, …)
- Keyboard nav: ↑/↓, Tab/Enter to accept, Esc to dismiss

### C. Right-click → filter by this value
Right-click any of these cells in the packet table:
- `hash`, `size`, `type`, `observer`

Context menu offers `==`, `!=`, `contains`. Click → clause appended to
filter input (with `&&` if expression already present).

### D. Saved filters
- ★ Saved ▾ dropdown next to the input
- 7 starter defaults (Adverts only, Channel traffic, Direct messages,
Strong signal SNR > 5, Multi-hop, Repeater adverts, Recent < 5m)
- "+ Save current expression" prompts for a name and persists to
`localStorage` under `corescope_saved_filters_v1`
- User filters can be deleted (✕); defaults cannot
- User filters with the same name as a default override it

## Implementation

**`public/packet-filter.js`** — exposes `FIELDS`, `OPERATORS`,
`TYPE_VALUES`, `ROUTE_VALUES`, and a new `suggest(input, cursor, opts)`
function that returns ranked autocomplete suggestions with
replace-range. Pure function — no DOM, fully unit-tested.

**NEW `public/filter-ux.js`** — `window.FilterUX` IIFE owning the help
popover, autocomplete dropdown, context menu, and saved-filters store.
`init()` is idempotent, called once after the filter input renders.

**`public/packets.js`** — calls `FilterUX.init()` after the filter input
IIFE; row builders gain `data-filter-field` / `data-filter-value` attrs
on hash/size/type/observer cells. `filter-group` wrapper now `position:
relative` so dropdowns anchor correctly.

**`public/style.css`** — scoped `.fux-*` styles using existing CSS
variables (no new theme tokens).

## Tests

- `test-packet-filter-ux.js` (19 unit tests, wired into `test-all.sh`):
  - Metadata exposure (FIELDS / OPERATORS / TYPE_VALUES / ROUTE_VALUES)
- `suggest()` for empty input, prefix match, after `==`, dynamic
`payload.*` keys
- `SavedFilters.list/save/delete` — defaults, persistence, override,
dedup
- `buildCellFilterClause()` and `appendClauseToExpr()` quoting +
appending
- `test-filter-ux-e2e.js` (Playwright, wired into `deploy.yml`):
  - Navigate /packets → metadata exposed
  - Help popover opens with field reference, operators, examples
  - Autocomplete shows on focus, filters by prefix, accepts on Enter
  - Saved-filter dropdown lists defaults, click populates input
  - Right-click on TYPE cell → context menu → click appends clause
  - Save current expression persists to localStorage

TDD red commit (`bddf1c1`) — assertion failures only, no import errors.
Green commit (`0d3f381`) — all 19 unit tests pass.

## Browser validation

Spawned local server on :39966 against the e2e fixture DB and exercised
every UX surface via the openclaw browser tool. Confirmed:
- `window.PacketFilter.FIELDS.length === 27`, `suggest()` available
- `FilterUX.SavedFilters.list().length === 7` (defaults seeded)
- Help popover renders with `payload.name`, `contains`, `ADVERT` text
content
- Right-click on a `data-filter-field="type"` /
`data-filter-value="Response"` cell → context menu showed three options
→ clicking == populated the input with `type == "Response"` (and the
existing alias resolver matched it to `payload_type === 1`)
- Autocomplete on `pay` returned `payload_bytes`, `payload_hex`,
`payload.name`, `payload.lat`, `payload.lon`, `payload.text`

## Out of scope (deferred per the issue)

- Server-synced saved filters (cross-device)
- Visual filter builder
- Custom field expressions

## Acceptance criteria

- [x] Help icon (ⓘ) next to filter input opens documentation popover
- [x] Field reference table + operator reference + 6+ examples in
popover
- [x] Autocomplete dropdown on field names (top-level + `payload.*`)
- [x] Autocomplete dropdown on values for `type` / `route` operators
- [x] Right-click on packet cell → "Filter ==" / "Filter !=" / "Filter
contains"
- [x] Right-click context menu hides when clicking elsewhere / Esc
- [x] Saved-filters dropdown with at least 5 default examples (7
shipped)
- [x] User-saved filters persist in localStorage
- [x] Real-time match count next to filter input (already shipped
pre-PR; preserved)
- [ ] Improved error messages with token + position — partial: existing
parse errors already cite position; not a regression
- [x] No regression in existing filter behavior
(`test-packet-filter.js`: 69/69 pass)

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:50:12 -07:00
Kpa-clawbot aa3d26f314 fix(nav): stop nav bar from jumping when Live is selected (#1046) (#1078)
## Summary

The `🔴 Live` nav link could wrap onto two lines at certain viewport
widths once it became the `.active` link, which grew `.nav-link`'s
height and made the whole `.top-nav` "hop" the instant Live was selected
(issue #1046).

Adding `white-space: nowrap` to the base `.nav-link` rule keeps every
nav label on a single line at every breakpoint (default desktop + the
768–1279px and <768px responsive overrides), eliminating the jump.

## Changes

- `public/style.css` — `white-space: nowrap` on `.nav-link`.
- `test-e2e-playwright.js` — new assertion at viewport 1115px (the width
in the issue screenshots) that:
  - computed `white-space` prevents wrapping
  - the Live link renders on a single line in both states
  - `.top-nav` height does not change when `.active` is toggled

## TDD

- Red commit `ba906a5` — test added, fails because base `.nav-link` has
no `white-space` rule (default `normal`).
- Green commit `51906cb` — single-line CSS fix makes the test pass.

Fixes #1046

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-05 01:36:08 -07:00
Kpa-clawbot 417b460fa0 feat(css): fluid scaffolding — clamp() spacing/type/container tokens (#1054) (#1066)
## Summary

Lands the **fluid CSS foundation** for the responsive scaffolding effort
(parent #1050). Pure additive change to the top of `public/style.css` —
no component CSS touched.

## What changed

### New tokens in `:root`
- **Spacing scale** — `--space-xs … --space-2xl` via `clamp()`. 1440px
targets match the prior hardcoded `4 / 8 / 16 / 24 / 32 / 48` px values
to within ~1px.
- **Type scale** — `--fs-sm … --fs-2xl` via `clamp(min, vw-based, max)`.
Floors keep text readable at 768px; caps prevent runaway growth at
2560px+.
- **Radii** — `--radius-sm/md/lg` via `clamp()`.
- **Container layout** — `--gutter` (`clamp()`) and `--content-max`
(`min(100% - 2*gutter, 1600px)`) for fluid horizontal layout without
media queries.

### Base consumption
- `html, body` now sets `font-size: var(--fs-md)`.

### Parallel-work safety
- Added `FLUID SCAFFOLDING` section header at the top.
- Added `COMPONENT STYLES` section header marking where the rest of the
file (nav, tables, charts, map, packets, analytics …) begins. Sibling
tasks 1050-3..6 / 1052-* edit inside that region and won't conflict with
this PR.

## TDD

- **Red:** `2d6f90a` — `test-fluid-scaffolding.js` asserts the new
tokens exist with `clamp()`/`min()`, that `html, body` consumes
`--fs-md`, and that the section marker is present. Fails on assertions
(15 failed, 0 passed).
- **Green:** `7b4d59b` — implementation in `public/style.css`. All 15
assertions pass.

## Acceptance criteria

- [x] Fluid spacing scale `--space-xs..--space-2xl` via `clamp()`
- [x] Fluid type scale `--fs-sm..--fs-2xl` via `clamp()`
- [x] Replace base body font-size with the new token
- [x] Container layout vars `--content-max`, `--gutter` via
`min()`/`clamp()`
- [x] No component CSS edits (only `:root`, `html`, `body`)
- [x] No visual regression at 1440px (token targets numerically match
prior px values)

## Notes for reviewers

- Pre-existing `test-frontend-helpers.js` failure on master is unrelated
(`nodesContainer.setAttribute is not a function`) and not introduced
here.
- `--content-max` uses `min(100% - 2*gutter, 1600px)` — the `100% - …`
arm wins on small viewports and guarantees a gutter always remains.

Fixes #1054

---------

Co-authored-by: clawbot <bot@corescope.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:14:39 -07:00
Kpa-clawbot eaf14a61f5 fix(css): 48px touch targets, :active states, hover→tap (#1060) (#1067)
## Summary

Fixes #1060 — free-win CSS pass for touch usability.

- All major interactive controls (`.btn`, `.btn-icon`, `.nav-btn`,
`.nav-link`, `.ch-icon-btn`, `.ch-remove-btn`, `.ch-share-btn`,
`.ch-gear-btn`, `.panel-close-btn`, `.mc-jump-btn`, `button.ch-item`)
now declare `min-height: 48px` / `min-width: 48px`. Hit-area grows;
visual padding/icon size unchanged on desktop because the rules use
`inline-flex` centering.
- Added visible `:active` feedback (background shift + `transform:
scale(0.92–0.97)` + opacity) on every button class — touch devices have
no hover, so `:active` is the only press signal.
- Hover-only `.sort-help` tooltip rule is now wrapped in `@media (hover:
hover)`; added a CSS-only `:focus` / `:focus-within` tap-to-reveal path
with a visible focus ring so the same content is reachable on touch (and
via keyboard).
- All changes scoped to the `=== Touch Targets ===` section. No other
CSS section modified, no JS touched, no markup edits.

## Acceptance criteria

- [x] All interactive controls reach 48×48 CSS-px touch target (verified
by `test-touch-targets.js`).
- [x] Every button has a visible `:active` state (no hover-only
feedback).
- [x] Hover tooltip rule is gated behind `@media (hover: hover)`, with
`:focus-within` tap-to-reveal fallback.
- [x] Desktop visuals preserved (padding-based, not visual-size-based).

## TDD

- Red commit `327473b` — `test-touch-targets.js` asserts every required
selector/property; it compiles and fails on assertion against pre-change
CSS.
- Green commit `e319a8f` — Touch Targets section rewrite; test passes.

```
$ node test-touch-targets.js
test-touch-targets.js: OK
```

Fixes #1060

---------

Co-authored-by: bot <bot@corescope>
2026-05-05 01:11:08 -07:00
Kpa-clawbot f9cd43f06f fix(analytics): integrate channels list with PSK decrypt UX + add link from Channels page (#1042)
## What

Integrates the Analytics → Channels section with the PSK decrypt UX (PRs
#1021–#1040). Replaces nonsense `chNNN` placeholders with useful display
names and groups the table the same way the Channels sidebar does.

## Before
- Encrypted channels showed raw `ch185`, `ch64`, `ch?` placeholders.
- Locally-decrypted PSK channels (with stored keys + labels) were not
surfaced — every encrypted row looked identical and useless.
- Single flat list, sorted by last activity by default.

## After
- **My Channels** 🔑 — any analytics row whose hash byte matches a stored
PSK key (via `ChannelDecrypt.getStoredKeys()` + `computeChannelHash`).
Display name uses the user's label if set, otherwise the key name.
- **Network** 📻 — known cleartext channels (server-provided names) and
rainbow-table-decoded encrypted channels.
- **Encrypted** 🔒 — unknown encrypted, rendered as `🔒 Encrypted (0xNN)`
instead of `chNNN`.
- Within each group: messages descending (most active first).
- New `📊 Channel Analytics →` link in the Channels page sidebar header →
`#/analytics`.

## How
- Pure `decorateAnalyticsChannels(channels, hashByteToKeyName, labels)`
— testable in isolation, sets `displayName` + `group` per row.
- `buildHashKeyMap()` — async helper that resolves stored PSK keys to
their channel hash bytes via `computeChannelHash`. Used at render time;
first paint uses an empty map (best-effort) and re-renders once keys
resolve. Graceful fallback when `ChannelDecrypt` is missing or there are
no stored keys.
- `channelTbodyHtml` gains an `opts.grouped` flag — opt-in so the
existing flat sort still works for any other caller.
- The analytics API endpoint is **unchanged** — this is purely frontend
rendering.

## Tests
`test-analytics-channels-integration.js` — 19 assertions covering
decoration, grouping, sort order, and the channels-page link. Added to
`test-all.sh`.

Red commit: `5081b12` (12 assertion failures + stub).  
Green commit: `6be16d9` (all 19 pass).

---------

Co-authored-by: bot <bot@corescope.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 00:05:09 -07:00
Kpa-clawbot 67da696a42 fix(channels): hide raw psk:* in header, label share button, red delete button (#1041)
## Channel UX round 2 (follow-up to #1040)

Three UX issues reported after #1040 landed:

### 1. Header shows raw `psk:372a9c93` for PSK channels
The selected-channel title rendered `ch.name` directly, which for
user-added PSK channels is the synthetic `psk:<hex8>` string. Users see
opaque key fragments where they expected the friendly name they typed.

**Fix:** new `channelDisplayName(ch)` helper. Returns `ch.userLabel`
when set, falls back to `"Private Channel"` for any `psk:*` name, then
to the original name, then to `Channel <hash>`. Used in both
`selectChannel` (header) and `renderChannelRow` (sidebar).

### 2. Share button `⤴` is unrecognizable
Up-arrow glyph carried no meaning — users didn't know it opened the
QR/key reshare modal.

**Fix:** swap `⤴` for `📤 Share` text label. Same hook, same handler.

### 3. ✕ delete button is a subtle span, not a destructive button
Looked like decorative text, not a real action.

**Fix:** `.ch-remove-btn` gets `background: var(--statusRed, #b54a4a)`,
`color: white`, `border-radius: 4px`, `padding: 4px 8px`, `font-weight:
bold`. Now reads as a destructive action.

### TDD
- Red commit `2d05bbf`: 9 failing assertions (helper missing, ⤴ still
present, CSS rules absent), test compiles + runs to assertion failure.
- Green commit `938f3fc`: all 12 assertions pass. Existing
`test-channel-ux-followup.js` still 28/28.

### Files
- `public/channels.js` — `channelDisplayName` helper, header + row
rendering, share button label
- `public/style.css` — `.ch-remove-btn` destructive styling
- `test-channel-ux-round2.js` — new test (helper behavior + source/CSS
assertions)

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-04 20:56:01 -07:00
Kpa-clawbot c00b585ee5 fix(channels): UX follow-ups to #1037 (touch target, '0 messages', share, locality, #meshcore) (#1040)
## Summary

Seven UX follow-ups to the channel modal/sidebar redesign in #1037.

## Fixes

1. **✕ touch target** — was 13px font + 0×4 padding, far below WCAG
2.5.5 / Apple HIG 44×44px. Bumped `.ch-remove-btn` to a 44×44 hit area
without disturbing desktop layout.
2. **"0 messages" preview** — user-added (PSK) channel rows showed `0
messages` even when dozens were decrypted. `messageCount` only tracks
server-known activity, not PSK decrypts. Drop the misleading fallback:
when no last message is known and the count is zero/absent, render
nothing.
3. **Privacy footer wording** — old copy "Clear browser data to remove
stored keys" was misleading after #1037 added per-channel ✕. Reworded to
point users at the ✕ button.
4. **Reshare affordance** — each user-added row now exposes a `⤴` Share
button that re-opens the QR + key for that channel via
`ChannelQR.generate` (with a plain-hex + `meshcore://channel/add?...`
URL fallback when the QR vendor lib isn't loaded). Reuses the Add
Channel modal; cleared on close.
5. **Drop "(your key)" suffix** from the row preview. The 🔑 badge
already conveys ownership; the suffix was noise. The key hex itself is
now only revealed on explicit Share, not in the sidebar.
6. **Make browser-local nature obvious** — the prior framing made
local-only sound like a feature when it's actually a constraint users
need to plan around. Adds:
- Prominent `.ch-modal-callout` in the Add Channel modal: *"Channels are
saved to **THIS browser only**. They won't appear on other devices or
browsers, and clearing browser data will remove them."*
   - `🖥️ (this browser)` marker in the **My Channels** section header
- Remove-confirm prompt now explicitly says *"permanently remove the key
from this browser"*
7. **#meshcore, not #LongFast** — `#LongFast` is Meshtastic's default
channel name. The meshcore network's analogous default is `#meshcore`.
Updated placeholder + case-sensitivity example in the modal.

## TDD

- Red commit `878d872` — failing assertions for fixes 1–6.
- Green commit `444cf81` — implementation.
- Red commit `6cab596` — failing assertions for fix 7.
- Green commit `9adc1a3` — `#meshcore` swap.

`test-channel-ux-followup.js` (18 assertions) passes. Existing
`test-channel-modal-ux.js` (33) and `test-channel-sidebar-layout.js` (8)
remain green.

## Files
- `public/channels.js` — row template, share handler, modal
callout/footer, sidebar header, confirm copy, placeholder swap
- `public/style.css` — `.ch-remove-btn` / `.ch-share-btn` 44×44,
`.ch-modal-callout`, `.ch-section-locality`
- `test-channel-ux-followup.js` — new test file

---------

Co-authored-by: clawbot <clawbot@local>
2026-05-04 19:57:53 -07:00