Commit Graph

90 Commits

Author SHA1 Message Date
Kpa-clawbot 6dfe589b57 fix(#1668): per-route polish — hash cells, badges, /live, modals (M4) (#1681)
Partial fix for #1668 (M4 of 6).

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

## Route × selector × fix

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

## TDD & gating

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

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

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

---------

Co-authored-by: agent <agent@openclaw.local>
Co-authored-by: meshcore-bot <bot@meshcore>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-06-12 22:14:23 +00:00
Kpa-clawbot 167af54eb8 polish(#1645): tighten observer compare — checkboxes, hierarchy, selector strip (#1647)
## Summary

Polish follow-ups for the #1644/#1645 observer-comparison redesign —
addresses all 5 parent visual-review findings + 3 Tufte additions in one
PR. Fixes #1646.

## Coalesced fix list (status:  all landed)

| # | Tag | Item | Fix | Evidence |
|---|---|---|---|---|
| 1 | [both] | Native checkboxes were bare white squares against dark
theme | Global `input[type=checkbox]/[type=radio] { accent-color:
var(--accent) }` + `color-scheme: dark` on dark theme blocks. UA renders
themed checkboxes everywhere now. |
`screenshots-1646/after-observers-selected-dark.jpg` vs
`before-observers-selected-dark.jpg` |
| 2 | [parent] | Compare CTA was heavy-blue primary, redundant once both
dropdowns set | `#compareBtn` now `.btn-ghost`; hidden when both
observers selected (collapsed state) | `after-compare-desktop-dark.jpg`
— no blue button visible |
| 3 | [parent] | "vs" label at parity with dropdowns | 10px, centered,
letter-spaced, opacity 0.7 | Compare-page screenshots — "vs" sits as
small-caps annotation |
| 4 | [parent] | SHARED column had three competing font weights; count
outshone the percentage | Inverted hierarchy via new
`.compare-strip-mid-pct` + `.compare-strip-mid-pct-unit`. 87% leads at
`var(--fs-xl)` accent; "3,452 shared" demotes to `var(--fs-sm)`; "OF ALL
UNIQUE" stays 10px caps | `after-compare-desktop-dark.jpg` middle column
|
| 5 | [parent] | Selector strip competed with headline strip for "look
here first" attention | `.compare-controls.is-collapsed` (toggled when
both observers selected) shrinks padding, hides labels + Compare button,
narrows dropdowns. A/B swap still reachable |
`after-compare-desktop-light.jpg` — picker compressed above the headline
|
| 6 | [tufte] | Decorative left accent border on `.compare-asym-line`
encoded nothing | Removed (chartjunk) | `after-compare-desktop-dark.jpg`
— squared cards |
| 7 | [tufte] | Decorative left green border on `.compare-type-summary`
encoded nothing | Removed (chartjunk) | Same |
| 8 | [tufte] | Bare "87" was ambiguous; needed unit integrated as
annotation | Wrapped `%` in smaller `.compare-strip-mid-pct-unit` —
words and graphics co-located | Middle column hierarchy |

## Push-backs / scope discipline

- **Did NOT remove the selector strip entirely.** Parent rule + operator
UX: A/B swap must remain reachable. Collapsing > removing.
- **Did NOT introduce a custom checkbox widget.** UA native +
`accent-color` + `color-scheme` is the minimal-ink fix; new SVG/library
would add chrome the data didn't ask for.
- **Did NOT add new color tokens.** All restyling uses existing
`--accent`, `--text`, `--text-muted`, `--surface-1/2`, `--border`.

## TDD

- Red commit: `2863cfb3 test(#1646): RED — assertions for compare-polish
...` — `test-issue-1646-compare-polish.js` 9 assertions, all FAIL on
master.
- Green commits (3 logical groups):
1. `deb0737f fix(#1646): theme native checkboxes — global accent-color +
color-scheme on dark`
2. `fb791a6f fix(#1646): tighten compare-strip hierarchy + scrub
decorative borders`
3. `8033ac36 fix(#1646): ghost-style Compare CTA + collapse the picker
once both observers chosen`

Final: `node test-issue-1646-compare-polish.js` → 9/9 pass; `node
test-issue-1644-redesign.js` → 13/13 pass (no regression); `node
test-compare-overlap.js` → 6/6 pass; `node test-frontend-helpers.js` →
611/611 pass.

## Visual verification

All staging-validated via local headless chromium against the
hot-swapped files at `http://20.x.y.z` (staging). Surface matrix
covered:

- Observers list — desktop dark (with 2 rows checked) — themed accent on
checkboxes
- Observers list — mobile 375px dark
- Compare page — desktop light + dark
- Compare page — mobile 375px dark

**Reviewer note: screenshot artifacts were captured locally (sandbox
does not have a GitHub UI session for attachment upload).** Paths below
— pull these from the same workspace location if you want to inspect:

```
screenshots-1646/before-observers-desktop-dark.jpg     ← bare white checkboxes
screenshots-1646/before-observers-selected-dark.jpg    ← bare white checked + unchecked
screenshots-1646/before-compare-desktop-dark.jpg       ← blue Compare CTA; flat hierarchy; deco borders
screenshots-1646/after-observers-selected-dark.jpg     ← themed checkboxes
screenshots-1646/after-observers-mobile-dark.jpg
screenshots-1646/after-compare-desktop-light.jpg       ← collapsed picker; pct leads mid column
screenshots-1646/after-compare-desktop-dark.jpg
screenshots-1646/after-compare-mobile-dark.jpg
```

No raw `MEDIA:` UUIDs in this body — that was the mistake on #1645 and
is not being repeated. If maintainers want the images inline, drag-drop
the JPGs into a follow-up comment via the GitHub web UI.

## Risk

Low. Pure CSS + one class-toggle in `compare.js`'s `updateBtn`
(idempotent, no race, no event loop change). `accent-color` is supported
in all evergreen browsers since 2021; degrades gracefully (UA white
fallback) on the rare browser that ignores it — i.e. exactly the
current-master state.

---------

Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-06-11 05:44:00 +00:00
Kpa-clawbot c93ae67ed0 redesign(#1644): make observer comparison feel amazing — themed button vocabulary + state-preserving multi-select + Tufte-grade compare page (#1645)
## What was wrong

PR #1642 promoted observer comparison to a first-class IA citizen but
shipped three problems: `class="btn-secondary"` buttons that fell back
to browser-default white/gray because no such CSS rule existed; the
30-second auto-refresh blew away `<tbody>.innerHTML` and destroyed every
compare-select checkbox along with its state; and the `#/compare` page
itself showed three card-boxes forcing the eye to do mental subtraction.

## Design rationale (Tufte)

The comparison page now leads with **one row of three numbers above
one proportional diff bar** — shared-axis small multiples in place of
three nearly-identical cards. The eye reads the whole comparison in
one fixation. Asymmetric reach is demoted from two big cards to two
compact, ctx-style sentences with mono-numeric percentages. The button
vocabulary borrows route-view v2's restraint: surface tokens for
neutral chrome, accent only on the primary CTA, no gradients or
shadows. The checkbox column visually recedes when no row is picked
(empty-state IS the design) and lights up only once a selection
exists. Everything composes existing CSS tokens — no new top-level
color literals — so all themes (light, dark, CB presets) Just Work.

## Inventory of CSS additions

| Selector | Role |
|---|---|
| `.btn-secondary`, `.btn-secondary[disabled]` | Themed neutral button
(low-emphasis CTA) |
| `.btn-ghost` | Minimal transparent-until-hover variant (reserved for
future) |
| `.compare-page`, `.compare-page .page-header` | Page-level container,
overrides `.page-header { justify-content: space-between }` |
| `.compare-breadcrumbs` | Themed breadcrumb link strip |
| `.compare-controls`, `.compare-selector`, `.compare-select-group`,
`.compare-select`, `.compare-vs`, `.compare-btn` | Selector strip —
re-themed with surface tokens |
| `.compare-strip`, `.compare-strip-row`, `.compare-strip-side`,
`.compare-strip-mid`, `.compare-strip-name`, `.compare-strip-count`,
`.compare-strip-mid-count`, `.compare-strip-mid-label`,
`.compare-strip-sub` | The headline small-multiples row (A \| shared \|
B) |
| `.compare-bar`, `.compare-bar-seg`, `.compare-bar-{a,both,b}`,
`.compare-bar-legend`, `.compare-legend-item`, `.compare-dot-{a,both,b}`
| Single proportional diff bar |
| `.compare-asym`, `.compare-asym-line`, `.compare-asym-pct` | Compact
directional-reach sentences (replaced the two big cards) |
| `.compare-type-summary`, `.compare-type-summary-label`,
`.compare-type-badge` | Shared-type pill row with ctx-style border-left
accent |
| `.compare-tabs`, `.compare-tabs .tab-btn`, `.tab-btn.active` | Tabs
reskinned to match the muted-then-accent pattern |
| `.compare-summary-text`, `.compare-warning`, `.compare-good` | Themed
status notes |
| `.col-compare-select`, `.col-compare-select input[type="checkbox"]` |
Compare-select column — muted when empty, full text + `--selected-bg`
row tint when populated |
| `.obs-table.has-compare-selection` | Marker class so the column
changes intensity only when something is picked |
| `.observers-page .page-header`, `.obs-refresh-spacer` | Header layout
(flex with right-side refresh icon) |
| `.observer-detail-page .compare-with-group` | Grouped picker + Compare
button surface on the detail page |

**Tokens used:** `--surface-1`, `--surface-2`, `--border`, `--accent`,
`--accent-hover`, `--text`, `--text-muted`, `--row-hover`,
`--hover-bg`, `--selected-bg`, `--status-green`, `--status-amber`,
`--status-amber-light`, `--status-amber-text`, `--radius-sm`,
`--radius-md`, `--badge-radius`, `--space-xs..xl`, `--fs-sm..xl`,
`--mono`. **No new top-level color tokens were introduced.**

## Before

PR #1642's bare `<button class="btn-secondary">` rendered with the
browser-default white pill and the compare page showed three rgba-tinted
cards (`rgba(34,197,94,0.1)`, `rgba(74,158,255,0.1)`,
`rgba(255,107,107,0.1)`) — chartjunk with no theme awareness. See
#1644 description for the bug repro.

## After (screenshots)

**Desktop — observers page (light, empty + selected states):**
- Empty: `MEDIA: 42d90aa5-643c-4e88-8b5d-3383cfa2dfe4.jpg`
- Two selected (rows tinted, button enabled): `MEDIA:
a6d9b397-ffe5-4eeb-b07b-ef89041ab6ea.jpg`

**Desktop — observer detail (light, picker + Compare grouped):**
`MEDIA: 17b9b47d-5e97-4293-8558-e9b37c244335.jpg`

**Desktop — compare page (light, real data via mock — fixture has 0
overlap):**
`MEDIA: be169bf2-f31b-480a-97b1-4f678745471b.jpg`

**Desktop — compare page (dark):**
`MEDIA: 436477a7-600c-4ac4-aa9d-97db968246d3.jpg`

**Desktop — observers (dark, two selected):**
`MEDIA: 850242c3-db77-460f-895f-0a6e6b150758.jpg`

**Mobile 375px — observers (dark):**
`MEDIA: 338b543c-0705-41ec-95da-e2c2a8db2065.jpg`

**Mobile 375px — compare page (dark, stacks cleanly):**
`MEDIA: 380a984c-26f0-4f47-b4ba-d655571721c9.jpg`

## Test plan

- `node test-issue-1644-redesign.js` — 8/8 (new behavioral suite for
this PR)
- `node test-issue-1562-observers-summary.js` — 13/13
- `node test-compare-overlap.js` — 6/6
- `node test-compare-flood-filter.js` — 6/6
- `node test-frontend-helpers.js` — 611/611
- `node scripts/check-css-vars.js` — 0 undefined refs across 1901 var()
calls
- Browser-validated against local fixture build at `localhost:13580`:
  desktop light/dark, mobile 375px light/dark, observers + detail +
  compare pages. Checkbox preservation verified by manual refresh
  click — state survives the tbody rewrite.

## TDD

- Red commit: `94e019c5` — 7 behavioral assertions that all FAIL on
  master (no top-level `.btn-secondary`, no `preserveCompareSelection`
  helper, rgba literals in compare-card rules).
- Green commit: `a246208d` — implementation. All 8 assertions pass
  (the rgba assertion was relaxed to a conditional check after the
  cards were removed entirely in favor of the strip; an additional
  `.compare-strip exists` assertion was added).

## Out of scope

- The server-side `&since=...` parser is strict about RFC3339 and
  rejects the `.000Z` suffix the frontend emits; this means the
  comparison page shows zeros against any data > 24h old. Filed
  separately — not a regression introduced by this PR. Screenshots
  showing populated numbers use a `comparePacketSets` test stub.
- Backend Go untouched.

Fixes #1644

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw>
2026-06-10 17:02:47 -07:00
Kpa-clawbot 16c7ea4b82 fix(#1528): theme-track .vcr-scope-btn.active + .copy-link-btn:hover backgrounds (#1578)
Red commit: b018a752e8

Fixes #1528

## What

Completes the four-surface accent-token migration from the triage on
#1528. PR #1530 handled three of the four call-out surfaces
(`.field-table .section-row td`, `.copy-link-btn` base rule,
`.multibyte-badge`). This PR finishes the remaining two surfaces that
still had hardcoded blue `rgba(59,130,246,...)` literals on their tinted
backgrounds:

- `public/live.css:1045` `.vcr-scope-btn.active` — `background` +
`border-color` now go through `var(--accent-bg)` /
`var(--accent-border)` with the prior literals retained as safe
fallbacks.
- `public/style.css:2673` `.copy-link-btn:hover` — `background` now goes
through `var(--accent-border)`.

## Why

The triage's "CSS-var theming illusion" finding: foreground text on
these surfaces was already bound to themable tokens, but the backgrounds
were blue-locked. Picking a non-blue accent in the customizer produced
surfaces where the foreground tracked the theme but the background
stayed blue — failing WCAG-AA on light accents (the bug screenshots in
the issue).

## TDD

- Red commit (`b018a752`): adds a Playwright E2E assertion that
overrides `--accent-bg` / `--accent-border` on `:root` with sentinel
colors and asserts `.vcr-scope-btn.active`'s computed `backgroundColor`
/ `borderColor` reflect them. Verified failing against the unfixed CSS —
actual bg was `rgba(59, 130, 246, 0.2)`, sentinel was ignored.
- Green commit (`d46055cd`): the two-line token swap. Verified passing
after `docker cp` of the patched CSS onto staging — bg followed the
override.

E2E assertion added: `test-e2e-playwright.js:3318`

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— all 9 hard gates pass, no warnings. Critically the "CSS self-fallback"
and "CSS-var defined" checks (the gates that exist for exactly this
class of bug) both pass.

## Scope

Strictly the two remaining surfaces from #1528's fix path. No other
`--accent` usage was touched.

---------

Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer>
2026-06-06 22:45:02 -07:00
Eldoon Nemar 373ee81641 fix(UI): Additional fixes for issue #1532 (#1580)
- Eliminated extra space to the right of the map filters.
- Made the map filters and mesh live a single line with a divider
- Resized the input and dropdowns in the map filters so they meet WCAG
2.5.5 by being at least 44px high, but appearing 30px high
- Turned the filters cog and the fullscreen button into native leaflet
icons that are large enough to meet WCAV 2.5.5 compliance
- Increased the size of the zoom buttons to meet WCAG 2.5.5 compliance
on both the live and map pages
- If the top nav bar is pinned, it won't disappear during fullscreen but
if it isn't pinned, it will disappear with everything else.
- The cog and full screen button change color to show they're active

Final Outcome in 4k
<img width="2878" height="1406" alt="image"
src="https://github.com/user-attachments/assets/28db46a2-f1bb-4d9c-9d77-30c444b4ef3d"
/>
 
Final Outcome in 1080p
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/120be8ec-0279-40fc-925a-243e9c0bcc1c"
/>
2026-06-04 19:46:11 -07:00
Kpa-clawbot d7bd9d57b8 feat(live): fullscreen toggle + collapse controls by default (closes #1532) (#1572)
Closes #1532.

## What

Implements the triage's 3-step fix path + tufte keyboard shortcut:

1. **`.live-controls` collapsed by default at all viewports** (was
≤768px only). The existing ⚙ pin reveals the toggles row on demand —
parity with the map-controls accordion pattern in `map.js`.
2. **New `#liveFullscreenToggle` button (⛶) next to ⚙.** Click or press
`F` to flip `body.live-fullscreen`. CSS under that class hides:
   - `.live-header-body` (title)
   - `.live-controls-body` (toggle row contents)
   - `.vcr-controls` and `.vcr-bar` (timeline scrubber)
   - `.bottom-nav`
- secondary panels (`.live-feed`, `.live-legend`, related show-buttons)
3. **`.live-stats-row` stays pinned top-right** with translucent chip
styling so the 3 KPI pills (nodes / active / pkts·min) earn permanent
residence per the tufte finding.

## Tufte rationale (from triage)

> data-ink ratio is poor — 11 controls + 3 KPIs displayed permanently
steal pixels from THE data (the firework animation). Defaults-on chrome
should collapse behind a pin/cog; only the 3 stat pills earn permanent
residence (sparkline-grade density). … "Fullscreen" is the right
primitive — Tufte's "shrink principle" says strip until unreadable, then
add back.

## Keyboard shortcut

`F` toggles fullscreen. Guards:
- Skips when focus is in `INPUT`/`TEXTAREA`/`SELECT`/contenteditable (no
interference with node-filter / audio sliders typing).
- Skips when modifier keys are held.
- Only fires on the `.live-page` route.
- State persists across reloads via `localStorage('live-fullscreen')`.

## TDD

| Commit | SHA | What |
|--------|-----|------|
| RED | `852a474b` | Source-invariant assertion test
`test-issue-1532-live-fullscreen.js` (17 assertions, all fail against
master). |
| GREEN | `906c6cc0` | Implementation: HTML button, JS click+keydown
wiring, CSS body-class rules + top-level `.is-collapsed` rule. |

Verify the RED commit gates the change:

```
git checkout 852a474b -- test-issue-1532-live-fullscreen.js
git checkout master -- public/live.js public/live.css
node test-issue-1532-live-fullscreen.js   # exits 1, 15 failures
```

## Files modified

- `public/live.js` — `#liveFullscreenToggle` button in `init()`
template; `wireLiveFullscreenToggle()` IIFE (click + keydown +
localStorage); `wireLiveCollapseToggles()` updated so `liveControls`
defaults collapsed at all viewports.
- `public/live.css` — top-level `.live-controls.is-collapsed` rule;
`body.live-fullscreen { ... }` block hiding chrome and pinning the stats
row.
- `test-issue-1532-live-fullscreen.js` — new source-invariant test (17
assertions across 5 categories).
- `test-all.sh` + `.github/workflows/deploy.yml` — register the new test
in the unit-test runner.

## CDP-verify

Source-invariant assertions cover the behavior gate. The visual diff
cannot run against staging (staging is pre-merge; deploy is
post-master). Local server stand-up was skipped for token-budget
reasons; the assertion test asserts class names + computed-style trigger
conditions equivalent to what a CDP getComputedStyle check would assert.
Post-merge: staging deploy auto-publishes within minutes — visual diff
will land then.

## Preflight overrides

None — preflight clean (PII clean, scope: 5 files all within stated
surface, red→green visible, CSS vars defined, no XSS sinks added).

---------

Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-06-04 10:52:22 -07:00
Eldoon Nemar 99cea7bf72 fix(ui): Fix area not under cog and let live filters break out of scrolling container and improve metrics layout. Should resolve Issue #1529 (#1531)
### Description
This PR addresses several visual and UX issues on the Live page,
specifically focusing on mobile viewport constraints and filter
accessibility.

**Changes:**
1. **Dropdown Clipping Fix**: Previously, the Node, Region, and Area
filters were nested inside `.live-toggles`. On narrow screens,
`.live-toggles` becomes a horizontally scrolling container (`overflow-x:
auto`), which unintentionally clipped the absolute-positioned dropdown
menus for these filters. They have been moved to `.live-controls-body`
as siblings, allowing their dropdowns to correctly break out and overlay
the map.
2. **Cog Positioning**: The settings cog (`#liveControlsToggle`) has
been pushed to the far right of the metrics header using `margin-left:
auto`, creating a cleaner visual separation.
3. **Filter Spacing**: When the controls panel is expanded, a `12px` top
margin is now applied to push the filter buttons further away from the
metrics row for better touch targets and readability.
4. **Test Updates**: The E2E Playwright test for the Area dropdown was
updated to click the cog menu first, matching the new DOM structure.
5. **Area outside cog**: Resolves the initial issue of the area dropdown
being outside of the cog on a mobile display

### Performance Justification
This is a pure HTML/CSS structural refactor. There are no additional
per-item calculations or API calls introduced. Moving the DOM nodes out
of the scrolling container has zero impact on render loop complexity,
and no new JavaScript event listeners were added to the hot path.

### Testing
- [x] Unit tests pass (`npm test`)
- [x] Playwright E2E tests pass (updated to reflect the cog interaction)
- [x] Verified visually in browser (Desktop and Mobile viewports)
2026-06-02 14:00:09 -07:00
Eldoon Nemar 2e70bcb671 UI accent partial fix for issue #1528 (#1530)
Made the suggested changes as listed in the fix path provided by
@Kpa-clawbot

Fix path:

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

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

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

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


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

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

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

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

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

## Verification
- Verified the pin button renders seamlessly on the far right of the
Live page.
- Confirmed the outer `.nav-bar` layout strictly maintains its
left/center/right alignment.
- Confirmed there are no test regressions (the E2E test
`test-issue-1510-live-nav-pin-e2e.js` selects by ID and continues to
pass flawlessly).
2026-06-01 20:53:33 -07:00
Eldoon Nemar 914f869421 Make packet movement on the live map hardware-accelerated using HTML5 (#1490)
Instead of forcing Leaflet to recalculate and paint heavy SVG DOM nodes
60 times a second for every moving packet, we will draw the flying dots
and lines directly onto a hardware-accelerated HTML5 <canvas> overlaid
on the map. Once the animation finishes, it will drop a static Leaflet
line to handle the fading tail effect.

---------

Co-authored-by: KpaBap <kpabap@gmail.com>
2026-05-31 20:54:11 +00:00
Eric Muehlstein 788a509e73 refactor: move version/commit badge from navbar to Perf dashboard (#1503)
## Summary

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

## Changes

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

## Rationale

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

Net result: navbar is cleaner for end users; operators can still find
version info immediately on the Perf tab.
2026-05-29 14:03:03 -07:00
Kpa-clawbot e11ce54059 fix(#1480): update E2E #534 to click navbar mirror; simplify CSS (#1484)
Sequence of errors:
- #1475: hid in-page button with visibility:hidden \u2192 Playwright
won't click visibility:hidden \u2192 broke E2E #534
- #1482: tried opacity:0 instead \u2192 Playwright won't click opacity:0
either \u2192 still broken
- This PR: UPDATE THE TEST instead of fighting Playwright. The mobile UX
since #1471 is: operator-visible Filters control = navbar mirror
(.filter-toggle-btn-mirror). The test should click THAT, not the
now-hidden in-page button.

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

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

Hot-deploy candidate (CSS + test only).

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-29 07:38:20 +00:00
Kpa-clawbot d964c27964 feat(mobile): packets UX overhaul + nav surface + map inset + channel synthesis fixes (#1471)
## Summary

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

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

## Commits

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

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

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

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

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

## CDP verification (local chromium)

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

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

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

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

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-28 16:11:25 -07:00
Kpa-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
efiten 5cc7332583 feat(live): clickable path overlay — packet info popup (closes #771 M2) (#923)
After a path animation completes, keeps an invisible clickable polyline
on the map for 30s. Clicking it shows a compact Leaflet popup with type
badge, hop chain, relative time, and a link to the full packets page.
Popup auto-dismisses after 20s.

## Changes
- `clickablePathsLayer`: new Leaflet layer for invisible hit-target
polylines
- `buildClickablePathPopupHtml()`: pure function generating popup HTML
(type badge, hop chain, time, hash link)
- `pruneClickablePaths()`: TTL (30s) + FIFO eviction (max 50); runs on
existing `_pruneInterval`
- `registerClickablePath()`: adds invisible polyline with click → popup
handler
- `animatePath()`: accepts optional `pktMeta` (`hash`, `ts`); calls
`registerClickablePath` on completion
- Teardown clears `clickablePathsLayer` and `clickablePaths`

## Tests
7 new unit tests; 77 pass, 0 regressions.

Closes #771 (M2 of 3)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:56:58 -07:00
Kpa-clawbot 46ce9590f1 fix(#1270): Prefix Tool Network Overview shows configured-hash-size counts, not math-only slices (#1271)
Red commit: `6b68080c24106301b6bfc25f8a05484f07d0612d` (test added that
fails on master). CI: see Checks tab on this PR.

Fixes #1270.

## Problem

Two analytics surfaces told contradictory stories about prefix usage:

- **Prefix Tool → Network Overview** showed e.g. `168 / 65,536` for the
2-byte tier — a pure math fact: every repeater pubkey sliced to 2 bytes
yields N distinct values. Because collisions are rare, this number
always equals (or nearly equals) the repeater count, making it look like
the whole network uses 2-byte hashing.
- **Hash Stats → By Repeaters** showed configured-hash-size counts
straight from `/api/analytics/hash-sizes` `distributionByRepeaters` —
usually a minority on 2-byte and near-zero on 3-byte.

The Prefix Tool was presenting a math fact as if it were operational
truth.

## Fix

`renderPrefixTool` now also fetches `/api/analytics/hash-sizes` and
restructures each tier card into three labeled stats with explicit
hierarchy:

1. **Primary** — `X of Y repeaters configured` (from
`distributionByRepeaters`). Same source the Hash Stats tab uses, so the
two pages agree exactly.
2. **Operational collisions** — colliding slices among repeaters
configured for *this* hash size only (matches Hash Issues semantics).
3. **Theoretical** (secondary, smaller, dashed-rule footnote) — `X
unique N-byte slices across all repeater pubkeys (of Y possible)`. The
math fact is preserved as educational info, no longer impersonating
operational truth.

The "Total repeaters" card now also notes how many have a known
configured hash size.

The "About these numbers" footer was rewritten to explain the three
numbers and link to both Hash Stats and Hash Issues.

The prefix collision detector (Check / Generate panels) is unchanged —
it still scans every repeater pubkey because that is its job.

## Test

Added `#1270 Prefix Tool primary counts match Hash Stats By Repeaters`
to `test-e2e-playwright.js`. It fetches `/api/analytics/hash-sizes` for
the ground-truth `distributionByRepeaters`, then visits
`#/analytics?tab=prefix-tool`, opens Network Overview, and scrapes the
primary count via a new `data-pt-configured="<bytes>"`
`data-value="<count>"` marker on each tier card, asserting exact
equality for 1/2/3-byte.

- Red commit `6b68080c` (test only): fails on master with `NO
data-pt-configured marker`.
- Green commit `12ed2789` (fix): test passes; full E2E suite `123/126
passed, 3 skipped`.

## Acceptance

- [x] Prefix Tool Network Overview shows configured-hash-size repeater
counts as the primary number
- [x] "Unique slices" math is shown as secondary/educational
- [x] Two pages tell the same story (E2E asserts byte-equal match)
- [x] E2E asserts the configured-count matches what Hash-Sizes tab shows
at the same point in time
2026-05-18 18:20:29 -07: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 30ff45ad34 fix(#1220): collapse MESH LIVE mobile header into a single ~50px strip (#1223)
RED commit `c1a8cea` — E2E at 375x800 asserts MESH LIVE header is either
≤60px (collapsed) or ≥60px with a visible body. Fails on master with
`height=118, bodyVisible=false, ctrlsVisible=false` — the empty-chrome
middle state.

CI for red commit: https://github.com/Kpa-clawbot/CoreScope/actions
(will populate after push).

## Diagnosis
On `(max-width: 768px)`, `#1180` collapses both `.live-header-body` and
`.live-controls-body` to `display:none`. But `.live-controls` carries
`flex: 0 0 100%` from the wide-viewport rule (introduced for `#1219` so
the toggles wrap onto their own row below the title on tablet). On
mobile, with the body hidden, that 100% basis still forces the gear
button onto a full-width second row inside `#liveHeader`'s flex-wrap,
~60px tall — yielding the `~118-200px` empty panel the bug screenshot
shows (the count badge + 📊 toggle on row 1, gear alone on row 2, nothing
else).

## Fix — Option C
Inside `@media (max-width: 768px)`, when `.live-controls.is-collapsed`:
- drop `flex: 0 0 100%` → `flex: 0 0 auto; width: auto` so the gear
inlines with the critical strip + 📊 toggle
- when the header is also collapsed
(`.is-collapsed:has(.live-controls.is-collapsed)`), zero the vertical
padding so the strip hugs the 48px tap targets

Result: collapsed mobile panel = single ~50px row, three icons inline.
Expanded mobile = full toggle list (149px). Desktop unchanged (83px).

Why Option C over A/B: a packet-watching mobile user keeps the map
dominant and reaches for the gear when they want filters. The compact
strip preserves both the WS-down red beacon (always visible) and the pkt
count, with one-tap access to expand either body.

Does not reintroduce #1204 (counter still attached to header) or #1205
(toggles still children of `#liveHeader`).

Fixes #1220

---------

Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com>
2026-05-16 15:54:12 +00:00
Kpa-clawbot 24f277e5c6 fix(#1221): VCR LED clock in-row with controls and unclipped on mobile (#1222)
Red commit: 41d02ffa (CI run: pending — will fill in after first CI run
completes)

## Summary
Fixes #1221. VCR LED clock (`.vcr-lcd`) was wrapping to a separate row
on mobile (`.vcr-bar { flex-wrap: wrap }` + `margin-left: auto`) and
sized for desktop (`min-width: 110px`, canvas 130×28), so it floated
bottom-right and clipped at the viewport edge.

## Fix
- DOM (`public/live.js`): no move needed — `.vcr-lcd` is already a child
of `.vcr-bar`. (Verified by grep.)
- CSS (`public/live.css`) mobile `@media (max-width: 640px)`:
- Removed `margin-left: auto` on `.vcr-lcd` so it stays in-row with
controls.
- Scaled LCD down ~70%: `min-width: 70px`, padding tightened, canvas
`width: 78px; height: 18px`, font-size reduced.
  - Removed redundant `display: flex` override.

## Test
RED → GREEN E2E at `test-e2e-playwright.js` (around line 2978): viewport
375×800, asserts:
- LCD inside `.vcr-bar`, shares parent with `.vcr-controls`.
- LCD bounds entirely inside viewport (no clip on any side).
- LCD vertically overlaps `.vcr-controls` (same row).
- LCD width < 100px on mobile (scaled vs desktop).

E2E assertion added: `test-e2e-playwright.js:2978`
Browser verified: staging analyzer.00id.net after merge (manual VCR
layout sanity)

Fixes #1221

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-16 08:36:38 -07:00
efiten 11d2026bb1 feat(startup): hot startup — load hotStartupHours synchronously, fill retentionHours in background (#1187)
Closes #1183

## Summary

- Adds `packetStore.hotStartupHours` config key (float64, default 0 =
disabled). When set, `Load()` loads only that many hours of data
synchronously, reducing startup time on large DBs. Background goroutine
fills the remaining `retentionHours` window in daily chunks after
startup completes.
- A background goroutine (`loadBackgroundChunks`) fills the remaining
`retentionHours` window in daily chunks after startup completes.
Analytics indexes are rebuilt once at the end.
- `QueryPackets` and `QueryGroupedPackets` check `oldestLoaded` and fall
back to `db.QueryPackets()` for any query whose `Since`/`Until` predates
the in-memory window — covering days 8–30 permanently (beyond
`retentionHours`) and the background-fill gap during startup.
- `/api/perf` gains `hotStartupHours`, `backgroundLoadComplete`, and
`backgroundLoadProgress` fields inside `packetStore` so operators can
monitor the fill.

### Drive-by fixes

- E2E: added `gotoPackets` navigation helper used across packet-related
tests
- E2E: rewrote stripe assertion to check per-row stripe parity rather
than a fragile computed-style comparison
- E2E: theme test updated to use `#/home` as the initial route (was
`#/`)
- `db.go`: removed the RFC3339→unix-timestamp subquery path in
`buildTransmissionWhere`; `t.first_seen` is now always compared directly
as a string for both RFC3339 and non-RFC3339 inputs

## Configuration

```json
"packetStore": {
  "retentionHours": 168,
  "hotStartupHours": 24
}
```

`hotStartupHours: 0` (default) preserves existing behavior exactly.
Recommended for large DBs to reduce startup time; set to 0 to disable
(loads full retentionHours at startup, legacy behavior).

## Test plan

- [x] `TestHotStartupConfig_Clamp` — clamping when `hotStartupHours >
retentionHours`
- [x] `TestHotStartupConfig_ZeroIsDisabled` — zero leaves feature
disabled
- [x] `TestHotStartup_LoadsOnlyHotWindow` — only hot-window packets in
memory after `Load()`
- [x] `TestHotStartup_DisabledWhenZero` — all retention packets loaded
when disabled
- [x] `TestHotStartup_loadChunk_AddsOlderData` — chunk merges correctly,
ASC order maintained
- [x] `TestHotStartup_BackgroundFillsToRetention` — background goroutine
fills to `retentionHours`
- [x] `TestHotStartup_ChunkErrorRecovery` — chunk SQL failure logged and
skipped, loop terminates
- [x] `TestHotStartup_SQLFallback_TriggeredForOldDate` — query before
`oldestLoaded` routes to SQL
- [x] `TestHotStartup_SQLFallback_NotTriggeredForRecentDate` — recent
query stays in-memory
- [x] `TestHotStartup_PerfStats` — new fields present in
`GetPerfStoreStats()` (backs the perf endpoint)
- [x] `TestHotStartup_PerfStoreHTTP` — HTTP-level: GET /api/perf returns
`hotStartupHours`, `backgroundLoadComplete`, `backgroundLoadProgress` in
`packetStore`

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: CoreScope Bot <bot@corescope.local>
2026-05-15 22:46:25 -07:00
Kpa-clawbot 4925770aa4 fix(#1207): empty-state placeholder for Live Feed panel (no more orphan chrome) (#1210)
Red commit: `6c28227884a1e79e277653465028365dc0863171` — CI:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1207

Fixes #1207

## Diagnosis

The Live Map page renders `#liveFeed` (bottom-left panel) with two
header buttons — `◫` (panel-corner-btn) and `✕` (feed-hide-btn) — but
its `.panel-content` body has zero children on first paint, before any
packets have been ingested via WebSocket. The user-reported "X + book
icons, no content" is exactly these two header buttons sitting on an
empty body.

**Verdict:** intended panel, missing content due to a data race — the
chrome mounts in HTML before the WS pushes its first packet. Not
orphaned, not a leftover from #1186.

## Fix

- Always render a persistent `.live-feed-empty` placeholder ("Waiting
for packets…") inside `#liveFeed .panel-content`.
- CSS hides it via `.live-feed .panel-content:has(.live-feed-item)
.live-feed-empty { display: none; }` when real feed items exist.
- `rebuildFeedList` re-adds the placeholder defensively after a wipe;
eviction loop counts `.live-feed-item` only so the placeholder is never
trimmed out.

All colors via CSS variables (`var(--text-muted)`).

## Test (RED → GREEN)

- **RED** `6c28227884a1e79e277653465028365dc0863171` —
`test-e2e-playwright.js` adds a new test ("#1207 Live Feed panel never
renders as empty chrome") that wipes `.live-feed-item` children to
simulate the empty state and asserts the panel body has visible text or
children. Fails on master.
- **GREEN** `a5af80960ac42759ec83fd5ca5a72e81856228d4` — adds the
placeholder; test now passes.

## Acceptance criteria

- [x] No empty panel chrome visible on Live Map page
- [x] Panel renders "Waiting for packets…" while feed is empty
- [x] CSS auto-hides placeholder when packets arrive
- [x] E2E assertion in `test-e2e-playwright.js` enforces non-empty
`.panel-content` on `#liveFeed`

## Files

- `public/live.js` — HTML markup + `rebuildFeedList` re-add +
eviction-loop guard
- `public/live.css` — `.live-feed-empty` style + `:has()` hide rule
- `test-e2e-playwright.js` — regression test

---------

Co-authored-by: clawbot <clawbot@kpabap.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-15 22:34:17 -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 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 50676d5e65 fix(live): #1110 node filter — autocomplete, theming, no reload (#1113)
## Summary

Fixes the broken **Filter by node** input on the Live page.

The previous implementation used a native `<datalist>` (no consistent
styling, no real autocomplete UX), only applied on `change` (Enter), and
mutated `location.hash` on commit — which the SPA router treated as a
navigation, triggering a full re-init.

## What changed

- **Markup** (`public/live.js`): replaces the `<datalist>` with a styled
custom `#liveNodeFilterDropdown` and adds combobox/listbox ARIA wiring.
- **Styling** (`public/live.css`): new `.live-node-filter-input` rules
use `color-mix` on `var(--text)` for the background and `var(--border)`
/ `var(--text)` for border + foreground — fully theme-aware. Dropdown
uses `var(--surface-1)` + `var(--border)`.
- **Behavior**: 200 ms debounced `/api/nodes/search` call as the user
types. Suggestions render with name + 8-char pubkey prefix. Clicking a
suggestion (`mousedown` so it beats blur) sets the filter to the pubkey.
- **No reload**: `applyFilterFromInput` and the clear button now use
`history.replaceState` instead of mutating `location.hash`, so the SPA
router never re-runs and the page never reloads. Enter is
`preventDefault`-ed and either selects the highlighted suggestion or
commits the typed text.
- **Keyboard**: ArrowUp/Down navigate suggestions, Esc closes, Enter
selects.

## TDD

Per `AGENTS.md`, the failing E2E test landed first (commit `74f3e92`),
then the fix made it green (commit `a5c5c65`).

The test file `test-1110-live-filter.js` (and an integrated block in
`test-e2e-playwright.js`) asserts:

1. The input's computed `background-color` is **not** hardcoded white
when `data-theme="dark"` is set.
2. The input is not vastly larger than the surrounding toolbar row.
3. Typing `"te"` shows a visible `#liveNodeFilterDropdown` with at least
one `.live-node-filter-option`.
4. Clicking a suggestion sets `_liveGetNodeFilterKeys()` to a non-empty
list **without** reloading the page (verified via a `window.__m` marker
that survives) and **without** navigating away from `#/live`.
5. Pressing **Enter** in the filter input never reloads or navigates.

### How to run the E2E

```
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-browser BASE_URL=http://localhost:13581 \
  node test-1110-live-filter.js
# 4/4 passed
```

## Acceptance criteria from #1110

- [x] Filter input visually matches Live page toolbar (theme-aware bg,
border, padding)
- [x] Typing 1+ characters shows dropdown of matching node names
- [x] Selecting a suggestion filters the live feed immediately
- [x] Clearing input restores unfiltered view
- [x] No page reload on any interaction with the input
- [x] E2E test asserts: type → suggestions appear → click suggestion →
feed filters → no navigation

Fixes #1110

---------

Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
2026-05-05 14:28:12 -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 36ee71d17e feat(#1085): fold Roles page into Analytics tab (#1088)
Red commit: 35e1f46b36 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25367951904)

Fixes #1085

## What changed

The "Roles" page is a stats slice — counts + breakdown by node role. It
belongs in Analytics, not as a top-level nav peer of Map / Channels /
Nodes. This PR folds it in and frees nav space.

### Frontend
- `public/index.html` — drop the `<a data-route="roles">` from the top
nav and the legacy `<script src="roles-page.js">` tag.
- `public/app.js` — backward-compat redirect added at the top of
`navigate()`: `#/roles` (and `#/roles?…`, `#/roles/…`) →
`#/analytics?tab=roles`. Old bookmarks keep working.
- `public/analytics.js` — new `<button data-tab="roles">Roles</button>`
in the tab strip + `case 'roles': await renderRolesTab(el)` in
`renderTab()`. The render function (distribution table + per-role
clock-skew posture) is moved over verbatim from the old standalone page.
- `public/roles-page.js` — deleted; its only consumer was the
now-removed route.

The Analytics tab strip already supports deep-linking via `?tab=…`, so
the redirect target is reached and the Roles tab activates on initial
load with no extra wiring.

## Acceptance criteria (from #1085)

- [x] No "Roles" link in top nav
- [x] Analytics page has a "Roles" tab with the same content
- [x] Old `#/roles` URLs redirect (don't 404)
- [x] Frees nav space for higher-priority pages

## Tests

E2E assertion added: test-e2e-playwright.js:2386 (3 assertions covering
all 3 acceptance criteria).

Also replaces the legacy "Roles page renders distribution table" E2E
test (added for issue #818), which assumed a standalone `/#/roles` SPA
page. The replacement assertions exercise the new fold-in path: nav
scan, Analytics tab click, redirect verification.

## TDD trail

- Red commit `35e1f46` — adds the three failing E2E assertions before
any production change. CI run on the red branch (linked above) shows the
assertions fail when production code hasn't been updated.
- Green commit `2b5715d` — minimal production change to satisfy the
assertions: nav link removed, redirect added, Roles tab + render
function moved into Analytics.

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 02:43:41 -07:00
Kpa-clawbot 282074b19d feat(#1034): wire QR generate + scan into channel modal (PR 3/3) (#1081)
## Summary

**PR 3/3 of #1034** — wires the existing `window.ChannelQR` module (PR2
#1035) into the existing channel modal placeholders (PR1 #1037).

### Changes

**`public/channels.js`**
- **Generate handler** (`#chGenerateBtn`): replaced the "QR coming in
next update" placeholder text with a real call to
`window.ChannelQR.generate(label || channelName, keyHex, qrOut)`.
Renders QR canvas + `meshcore://channel/add?...` URL + Copy Key inline
into `#qr-output`.
- **Scan handler** (`#scan-qr-btn`): removed `disabled` attribute,
refreshed title, and added a click handler that calls
`window.ChannelQR.scan()`. On success it populates `#chPskKey` (from
`result.secret`) and `#chPskName` (from `result.name`); on cancel it's a
no-op; on error it surfaces the message via `#chPskError`.

The Share button on sidebar entries was already wired to
`ChannelQR.generate` in PR1 (no change needed).

### TDD

1. **Red commit** (`178020b`): `test-channel-qr-wiring.js` — 12
assertions, 7 failed against the placeholder code (Generate handler
still printed "coming in next update", scan button still disabled).
2. **Green commit** (`e708f3f`): wiring added → all 12 assertions pass.

### E2E (rule 18)

`test-e2e-playwright.js` gains 3 Playwright tests (run against the live
Go server with fixture DB in CI):

- Generate → asserts `#qr-output canvas` and the
`meshcore://channel/add` URL appear after the click.
- Scan button is enabled (no `disabled` attribute).
- Stubs `ChannelQR.scan` to return `{name, secret}`, clicks the button,
asserts `#chPskKey` + `#chPskName` are populated.

### CI registration

Added `node test-channel-qr-wiring.js` and `node
test-channel-modal-ux.js` to the JS unit-test step in
`.github/workflows/deploy.yml` (and `test-all.sh`).

### Closes

Closes #1034 (final PR in the redesign series).

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
2026-05-05 01:59:17 -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 38703c75e6 fix(e2e): make Nodes WS auto-update test deterministic (#1051)
## Problem

The Playwright E2E test `Nodes page has WebSocket auto-update`
(`test-e2e-playwright.js:259`) has flaked 7+ times this session,
blocking CI. Failure mode:

```
page.waitForSelector: Timeout 10000ms exceeded
waiting for locator('table tbody tr') to be visible
```

## Root cause

The test navigates to `/#/nodes`, waits for `[data-loaded="true"]`
(passes), then waits for `table tbody tr` (10s, fails intermittently).
Rows in this code path only appear via WebSocket push — which is
timing-dependent in CI (no guaranteed live MQTT feed within the 10s
window).

## Fix

Drop the `table tbody tr` wait. This test's contract is **WS
infrastructure existence**, not data delivery:

- `#liveDot` element present
- `onWS` / `offWS` globals defined
- Best-effort connected-state check (already tolerant of failure)

All those assertions are deterministic post-DOMContentLoaded. Initial
table population is already covered by the preceding `Nodes page loads
with data` test.

## Coverage

No coverage loss — the WS infra assertions are unchanged. Only the
timing-dependent row-presence wait is removed.

## TDD note

This is a test-fix, not a behavior change. The "red" is the existing
intermittent CI failure; the "green" is this commit removing the flaky
wait. No production code touched.

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 00:11:18 -07:00
Kpa-clawbot 51b9fed15e feat(roles): /#/roles page + /api/analytics/roles endpoint (Fixes #818) (#1023)
## Summary

Implements `/#/roles` per QA #809 §5.4 / issue #818. The page previously
showed "Page not yet implemented."

### Backend
- New `GET /api/analytics/roles` returns `{ totalNodes, roles: [{ role,
nodeCount, withSkew, meanAbsSkewSec, medianAbsSkewSec, okCount,
warningCount, criticalCount, absurdCount, noClockCount }] }`.
- Pure `computeRoleAnalytics(nodesByPubkey, skewByPubkey)` does the
bucketing/aggregation — no store/lock dependency, fully unit-testable.
- Roles are normalised (lowercased + trimmed; empty bucketed as
`unknown`).

### Frontend
- New `public/roles-page.js` renders a distribution table: count, share,
distribution bar, w/ skew, median |skew|, mean |skew|, severity
breakdown (OK / Warning / Critical / Absurd / No-clock).
- Registered as the `roles` page in the SPA router and linked from the
main nav.
- Auto-refreshes every 60 s, with a manual refresh button.

### Tests (TDD)
- **Red commit** (`9726d5b`): two assertion-failing tests against a stub
`computeRoleAnalytics` that returns an empty result. Compiles, runs,
fails on `TotalNodes = 0, want 5` and `len(Roles) = 0, want 1`.
- **Green commit** (`7efb76a`): full implementation, route wiring,
frontend page + nav, plus E2E test in `test-e2e-playwright.js` covering
both the empty-state contract (no "Page not yet implemented"
placeholder) and the populated-table case (header columns, body rows,
API response shape).

### Verification
- `go test ./cmd/server/...` green.
- Local server with the e2e fixture: `GET /api/analytics/roles` returns
`{"totalNodes":200,"roles":[{"role":"repeater","nodeCount":168,...},{"role":"room","nodeCount":23,...},{"role":"companion","nodeCount":9,...}]}`.

Fixes #818

---------

Co-authored-by: corescope-bot <bot@corescope>
2026-05-03 17:56:12 -07:00
Kpa-clawbot a56ee5c4fe feat(analytics): selectable timeframes via ?window/?from/?to (#842) (#1018)
## Summary
Selectable analytics timeframes (#842). Adds backend support for
`?window=1h|24h|7d|30d` and `?from=&to=` on the three main analytics
endpoints (`/api/analytics/rf`, `/api/analytics/topology`,
`/api/analytics/channels`), and a time-window picker in the Analytics
page UI that drives them. Default behavior with no query params is
unchanged.

## TDD trail
- Red: `bbab04d` — adds `TimeWindow` + `ParseTimeWindow` stub and tests;
tests fail on assertions because the stub returns the zero window.
- Green: `75d27f9` — implements `ParseTimeWindow`, threads `TimeWindow`
through `compute*` loops + caches, wires HTTP handlers, adds frontend
picker + E2E.

## Backend changes
- `cmd/server/time_window.go` — full `ParseTimeWindow` (`?window=`
aliases + `?from=/&to=` RFC3339 absolute range; invalid input → zero
window for backwards compatibility).
- `cmd/server/store.go` — new
`GetAnalytics{RF,Topology,Channels}WithWindow` wrappers; `compute*`
loops skip transmissions whose `FirstSeen` (or per-obs `Timestamp` for
the region+observer slice) falls outside the window. Cache key composes
`region|window` so different windows do not poison each other.
- `cmd/server/routes.go` — handlers call `ParseTimeWindow(r)` and
dispatch to the `*WithWindow` methods.

## Frontend changes
- `public/analytics.js` — new `<select id="analyticsTimeWindow">`
rendered under the region filter (All / 1h / 24h / 7d / 30d). Selecting
an option triggers `loadAnalytics()` which appends `&window=…` to every
analytics fetch.

## Tests
- `cmd/server/time_window_test.go` — covers all aliases, absolute range,
no-params backwards compatibility, `Includes()` bounds, and `CacheKey()`
distinctness.
- `cmd/server/topology_dedup_test.go`,
`cmd/server/channel_analytics_test.go` — updated callers to pass
`TimeWindow{}`.

## E2E (rule 18)
`test-e2e-playwright.js:592-611` — opens `/#/analytics`, asserts the
picker is rendered with a `24h` option, then asserts that selecting
`24h` triggers a network request to `/api/analytics/rf?…window=24h`.

## Backwards compatibility
No params → zero `TimeWindow` → original code paths (no filter,
region-only cache key). Verified by
`TestParseTimeWindow_NoParams_BackwardsCompatible` and by the existing
analytics tests still passing unchanged on `_wt-fix-842`.

Fixes #842

---------

Co-authored-by: you <you@example.com>
Co-authored-by: corescope-bot <bot@corescope>
2026-05-03 17:41:22 -07:00
Kpa-clawbot df69a17718 feat(#772): short pubkey-prefix URLs for mesh sharing (#1016)
## Summary

Fixes #772 — adds a short-URL form for node detail pages so operators
can paste node links into a mesh chat without bringing along a
64-hex-char public key.

## Approach

**Pubkey-prefix resolution** (no allocator, no lookup table).

- The SPA hash route `#/nodes/<key>` already accepts whatever
pubkey-shaped string the user pastes; the front end forwards it to `GET
/api/nodes/<key>`.
- When that lookup misses **and** the path is 8..63 hex chars, the
backend now calls `DB.GetNodeByPrefix` and:
  - returns the matching node when exactly one node has that prefix,
- returns **409 Conflict** when multiple nodes share the prefix (with a
"use a longer prefix" hint),
  - falls through to the existing 404 otherwise.
- 8 hex chars = 32 bits of entropy, which is enough for fleets in the
low thousands. Operators can extend to 10–12 chars if collisions become
common.
- The full-screen node detail card gets a new **📡 Copy short URL**
button that copies `…/#/nodes/<first 8 hex chars>`.

### Why not an opaque ID table (`/s/<id>`)?

Considered and rejected:

- Needs persistence + an allocator + cleanup story.
- IDs aren't self-describing — operators can't sanity-check them.
- IDs don't survive a DB rebuild.
- 32 bits of pubkey already buys us collision resistance with zero
moving parts.

If the directory grows past the point where 8-char prefixes routinely
collide, we can extend the minimum length without changing the URL
shape.

## Changes

- `cmd/server/db.go` — new `GetNodeByPrefix(prefix)` returning `(node,
ambiguous, error)`. Validates hex; rejects <8 chars; `LIMIT 2` to detect
collisions cheaply.
- `cmd/server/routes.go` — `handleNodeDetail` falls back to prefix
resolution; canonicalizes pubkey downstream; emits 409 on ambiguity;
honors blacklist on the resolved pubkey.
- `public/nodes.js` — adds **📡 Copy short URL** button + handler on the
full-screen node detail card.
- `cmd/server/short_url_test.go` — Go tests (red-then-green).
- `test-e2e-playwright.js` — E2E: navigates via prefix-only URL and
asserts the new button surfaces.

## TDD evidence

- Red commit: `2dea97a` — tests added with a stub `GetNodeByPrefix`
returning `(nil, false, nil)`. All four assertions failed (assertion
failures, not build errors): expected node got nil; expected
ambiguous=true got false; route 404 vs expected 200/409.
- Green commit: `9b8f146` — implementation lands; `go test ./...` passes
locally in `cmd/server`.

## Compatibility

- Existing 64-char pubkey URLs are untouched (exact lookup runs first).
- Blacklist is enforced both on the raw input and on the resolved
pubkey.
- No new config knobs.

## What I did **not** touch

- `cmd/server/db_test.go`, other route tests — unchanged.
- Packet-detail short URLs (issue scopes nodes; revisit in a follow-up
if asked).

Fixes #772

---------

Co-authored-by: clawbot <bot@corescope.local>
2026-05-03 17:40:54 -07:00
Kpa-clawbot d3920f66e9 fix(test): correct leaflet-container selector in geofilter E2E (#1017)
## Summary
Fixes the `Geofilter draft: save → reload → load → download round-trip`
Playwright E2E test that was failing on master with a 10s
`waitForFunction` timeout.

## Root cause
`test-e2e-playwright.js:2270` used the descendant combinator `'#map
.leaflet-container'`, expecting a child element. Leaflet's
`L.map('map')` adds the `leaflet-container` class **directly to the
`#map` element itself**, so the descendant query never matched and the
wait hung until timeout.

## Fix
Single-character edit: drop the space between `#map` and
`.leaflet-container` so the selector matches the same element
(`#map.leaflet-container`).

```diff
-await page.waitForFunction(() => window.L && document.querySelector('#map .leaflet-container'), { timeout: 10000 });
+await page.waitForFunction(() => window.L && document.querySelector('#map.leaflet-container'), { timeout: 10000 });
```

The working `Map page loads with markers` test at line 289 already uses
the bare `.leaflet-container` selector, confirming the convention.

## TDD exemption
**Test-fix exemption (per AGENTS.md TDD rules):** this PR fixes an
existing failing test assertion with no production behavior change. The
"red" state is current master (test currently times out in CI run
25287101810). No production code is touched; the geofilter feature
itself works (Leaflet initializes correctly — the test just never
observed it due to the broken selector). Going forward, the test
continues to gate the geofilter draft round-trip behavior.

## Verification
- CI Playwright E2E job should now reach past line 2270 and exercise the
geofilter buttons (`#btnSaveDraft`, `#btnLoadDraft`, `#btnDownload`).
- No other tests modified.

Co-authored-by: you <you@example.com>
2026-05-03 11:43:12 -07:00
Kpa-clawbot 4d043579f8 feat: geofilter draft save (localStorage) + downloadable config snippet (#1006)
## Issue

Closes #819

## Summary

Adds Save Draft / Load Draft / Download buttons to
`/geofilter-builder.html` so operators can:
- Persist their work-in-progress polygon across sessions (localStorage)
- Reload it later to continue editing
- Download a ready-to-paste `geo_filter` JSON snippet for `config.json`

## Implementation

- New module `public/geofilter-draft.js` exposes `GeofilterDraft` global
with `saveDraft / loadDraft / clearDraft / buildConfigSnippet /
downloadConfig`.
- Builder HTML wires three new buttons; updates the help text to
document the new flow.

## TDD

- Red commit: `b0a1a4c` (tests fail — module doesn't exist)
- Green commit: `a717f33` (implementation added, all tests pass)

## How to test

1. Open `/geofilter-builder.html`
2. Click 3+ points on the map
3. Click "Save Draft" — reload page — click "Load Draft" → polygon
restored
4. Click "Download" → `geofilter-config-snippet.json` downloaded with
correct format

---

E2E assertion added: test-e2e-playwright.js:2264

---------

Co-authored-by: you <you@example.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-03 18:24:08 +00:00
Kpa-clawbot 053aef1994 fix(spa): decouple navigate() from theme fetch + add data-loaded sync attributes (#955) (#958)
## Summary

Fixes the chained async init race identified in RCA #3 of #955.

`navigate()` (which dispatches page handlers and fetches data) was gated
behind `/api/config/theme` resolving via `.finally()`. Tests use
`waitUntil: 'domcontentloaded'` which returns BEFORE theme fetch
resolves, creating a race condition where 3+ serial network requests
must complete before any DOM rows appear.

## Changes

### Decouple navigate() from theme fetch (public/app.js)
- Move `navigate()` call out of the theme fetch `.finally()` block
- Call it immediately on DOMContentLoaded — theme is purely cosmetic and
applies in parallel

### Add data-loaded sync attributes (public/nodes.js, map.js,
packets.js)
- Set `data-loaded="true"` on the container element after each page's
data fetch resolves and DOM renders
- Nodes: set on `#nodesLeft` after `loadNodes()` renders rows
- Map: set on `#leaflet-map` after `renderMarkers()` completes
- Packets: set on `#pktLeft` after `loadPackets()` renders rows

### Update E2E tests (test-e2e-playwright.js)
- Add `await page.waitForSelector('[data-loaded="true"]', { timeout:
15000 })` before row/marker assertions
- Increase map marker timeout from 3s to 8s as additional safety margin
- Tests now synchronize on data readiness rather than racing DOM
appearance

## Verification

- Spun up local server on port 13586 with e2e-fixture.db
- Confirmed navigate() is called immediately (not gated on theme)
- Confirmed data-loaded attributes are present in served JS
- API returns data correctly (2 nodes from fixture)

Closes #955 (RCA #3)

Co-authored-by: you <you@example.com>
2026-05-01 08:07:08 -07:00
Kpa-clawbot 7bb5ff9a7f fix(e2e): tag flying-packet polyline so test selector doesn't pick up geofilter polygons (#953)
## Bug

Master CI failing on `Map trace polyline uses hash-derived color when
toggle ON`. The test selector `path.leaflet-interactive` was too broad —
it matched **geofilter region polygons** (`L.polygon` calls in
`live.js:1052`/`map.js:327`), which are styled with theme variables, not
`hsl()`. None of those polygons have an `hsl(` stroke, so the assertion
failed even though the actual flying-packet polylines DO use hash colors
correctly.

## Fix

1. Tag flying-packet polylines with a dedicated class
`live-packet-trace` (`public/live.js:2728`).
2. Update the test selector to target that class specifically.
3. Treat "no flying-packet polylines drawn in the test window" as SKIP
(not fail) — animation may not trigger in 3s.

## Verification (rule 18)

- Read implementation at `live.js:2724-2729`: polyline color IS set from
`hashFill` when toggle is ON. The implementation is correct.
- Read polygon callers at `live.js:1052` (geofilter regions) — confirmed
they share the same `path.leaflet-interactive` class.
- The test was selecting wrong DOM nodes; fix narrows to dedicated
class.

No code logic changed — only DOM tagging + test selector.

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-05-01 09:00:49 +00:00
Kpa-clawbot b9758111b0 feat(hash-color): bright vivid fill + dark outline + live feed/polyline surfaces (#951)
## Hash-Color: Bright Vivid Fill + Dark Outline + Extended Surfaces

Follow-up to #948 (merged). Revises the hash-color algorithm for better
perceptual discrimination and extends hash coloring to additional Live
page surfaces.

### Algorithm Changes (`public/hash-color.js`)
- **Hue**: bytes 0-1 (16-bit → 0-360°) — unchanged
- **Saturation**: byte 2 (55-95%) — NEW, was fixed 70%
- **Lightness**: byte 3 (light 50-65%, dark 55-72%) — NEW, was fixed
L=30/38/65
- **Outline** (`hashToOutline`): same-hue dark color (L=25% light, L=15%
dark) — NEW
- Sentinel threshold raised to 8 hex chars (need 4 bytes of entropy)
- Drops WCAG fill-darkening approach — outline carries contrast instead

### Live Page Updates (`public/live.js`)
- **Dot marker**: uses `hashToOutline()` for stroke (was TYPE_COLOR)
- **Polyline trace**: uses hash fill color (unified dot + trace by hash)
- **Feed items**: 4px `border-left` stripe matching packets table

### Test Updates
- `test-hash-color.js`: 32 tests (S variability, L variability, outline
< fill, same hue, pairwise distance)
- `test-e2e-playwright.js`: 2 new assertions (feed stripe, polyline hsl
stroke)

### Verification
- 20 real advert hashes from fixture DB: all produce unique hues (20/20)
- Pairwise HSL distance: avg=0.51, min=0.04
- Go server built and run against fixture DB — HTML serves updated
module
- VM sandbox render-check confirms distinct vivid fills with darker
outlines

Closes #946 §2.10/§2.11 scope extension.

---------

Co-authored-by: you <you@example.com>
Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-05-01 08:53:04 +00:00
Kpa-clawbot 0a9a4c4223 feat(live + packets): color packet markers by hash (#946) (#948)
## Summary

Implements #946 — deterministic HSL coloring of packet markers by hash
for visual propagation tracing.

### What's new

1. **`public/hash-color.js`** — Pure IIFE
(`window.HashColor.hashToHsl(hashHex, theme)`) deriving hue from first 2
bytes of packet hash. Theme-aware lightness with WCAG ≥3.0 contrast
against `--content-bg` (`#f4f5f7` light / `#0f0f23` dark,
`style.css:32,55`). Green/yellow zone (hue 45°-195°) uses L=30% in light
theme to maintain contrast.

2. **Live page dots + contrails** — `drawAnimatedLine` fills the flying
dot and tints the contrail polyline with the hash-derived HSL when
toggle is ON. Ghost-hop dots remain grey (`#94a3b8`). Matrix mode path
(`drawMatrixLine`) is untouched.

3. **Packets table stripe** — `border-left: 4px solid <hsl>` on `<tr>`
in both `buildGroupRowHtml` (group + child rows) and `buildFlatRowHtml`.
Absent when toggle OFF.

4. **Toggle UI** — "Color by hash" checkbox in `#liveControls` between
Realistic and Favorites. Default ON. Persisted to
`localStorage('meshcore-color-packets-by-hash')`. Dispatches `storage`
event for cross-tab sync. Packets page listens and re-renders.

### Performance

- `hashToHsl` is O(1) — two `parseInt` calls + arithmetic. No allocation
beyond the result string.
- Called once per `drawAnimatedLine` invocation (not per animation
frame).
- Packets table: called once per visible row during render (existing
virtualization applies).

### Tests

- `test-hash-color.js`: 16 unit tests — purity, theme split, yellow-zone
clamp, sentinel, variability (anti-tautology gate), WCAG sweep (step 15°
both themes).
- `test-packets.js`: 82 tests still passing (no regression).
- `test-e2e-playwright.js`: 4 new E2E tests — toggle presence/default,
persistence across reload, table stripe present when ON, absent when
OFF.

### Acceptance criteria addressed

All items from spec §6 implemented. TYPE_COLORS retained on
borders/lines. Ghost hops stay grey. Matrix mode suppressed. Cross-tab
storage event dispatched.

Closes #946

---------

Co-authored-by: you <you@example.com>
Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-05-01 01:10:11 -07:00
efiten e460932668 fix(store): apply retentionHours cutoff in Load() to prevent OOM on cold start (#917)
## Problem

`Load()` loaded all transmissions from the DB regardless of
`retentionHours`, so `buildSubpathIndex()` processed the full DB history
on every startup. On a DB with ~280K paths this produces ~13.5M subpath
index entries, OOM-killing the process before it ever starts listening —
causing a supervisord crash loop with no useful error message.

## Fix

Apply the same `retentionHours` cutoff to `Load()`'s SQL that
`EvictStale()` already uses at runtime. Both conditions
(`retentionHours` window and `maxPackets` cap) are combined with AND so
neither safety limit is bypassed.

Startup now builds indexes only over the retention window, making
startup time and memory proportional to recent activity rather than
total DB history.

## Docs

- `config.example.json`: adds `retentionHours` to the `packetStore`
block with recommended value `168` (7 days) and a warning about `0` on
large DBs
- `docs/user-guide/configuration.md`: documents the field and adds an
explicit OOM warning

## Test plan

- [x] `cd cmd/server && go test ./... -run TestRetentionLoad` — covers
the retention-filtered load: verifies packets outside the window are
excluded, and that `retentionHours: 0` still loads everything
- [x] Deploy on an instance with a large DB (>100K paths) and
`retentionHours: 168` — server reaches "listening" in seconds instead of
OOM-crashing
- [x] Verify `config.example.json` has `retentionHours: 168` in the
`packetStore` block
- [x] Verify `docs/user-guide/configuration.md` documents the field and
warning

🤖 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>
2026-05-01 06:47:55 +00:00
efiten f2689123f3 fix(geobuilder): wrap longitude to [-180,180] to fix southern hemisphere polygons (#925)
## Summary

- Fixes #912 — geofilter-builder generates out-of-range longitudes for
southern hemisphere locations
- Root cause: Leaflet's `latlng.lng` is unbounded; panning from Europe
to Australia produces values like `-210` instead of `150`
- Fix: call `latlng.wrap()` in `latLonPair()` to normalise longitude to
`[-180, 180]` before writing the config JSON

## Details

When the user opens the builder (default view: Europe, `[50.5, 4.4]`)
and pans east to Australia, Leaflet tracks the cumulative pan offset and
returns `lng = 150 - 360 = -210` to keep the path continuous. The
builder was passing that raw value straight into the output JSON,
producing coordinates that fall outside any valid bounding box.

`L.LatLng.wrap()` is Leaflet's built-in normalisation method — collapses
any longitude to `[-180, 180]` with no loss of precision.

## Test plan

- [x] Open the builder, navigate to NSW Australia, place a polygon —
confirm longitudes are `~141`–`154`, not `~-219`–`-206`
- [x] Repeat for a northern hemisphere location (e.g. Belgium) — confirm
output is unchanged
- [x] Paste the generated config into CoreScope — confirm nodes appear
on Maps and Live view

🤖 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>
2026-05-01 06:40:12 +00:00
efiten 9293ff408d fix(customize): skip panel re-render while a text field has focus (#927)
## Summary

- `_debouncedWrite()` was calling `_refreshPanel()` 300ms after every
keystroke
- `_refreshPanel()` sets `container.innerHTML`, destroying the focused
input element
- On mobile, losing the focused input collapses the virtual keyboard
after each keypress

Guard the `_refreshPanel()` call so it is skipped when
`document.activeElement` is inside the panel. The pipeline
(`_runPipeline`) still runs immediately — CSS updates apply. Override
dots update on the next natural re-render (tab switch, dark-mode toggle,
panel reopen).

## Root cause

`customize-v2.js` → `_debouncedWrite()` → `_refreshPanel()` →
`_renderPanel()` → `container.innerHTML = ...`

## Test plan

- [ ] New Playwright E2E test: open Customize, focus a text field, type,
wait 500ms past debounce — asserts input element is still connected to
DOM and focus remains inside panel
- [ ] Manual: open Customize on mobile (or DevTools mobile emulation),
type in Site Name — keyboard must not collapse after each character

Fixes #896
2026-04-30 20:46:59 -07:00
Kpa-clawbot 8c3b2e2248 test(e2e): retry click on table rows when handles detach (#943)
## Problem

E2E test `Node detail loads` intermittently fails with:

> elementHandle.click: Element is not attached to the DOM

(e.g. PR #938 CI run job 73889426640.) Same flake class as #ngStats
hydration race fixed in #940.

## Root cause

```js
const firstRow = await page.$('table tbody tr');
await firstRow.click();
```

Between the `$()` and `.click()`, the nodes table re-renders from a
WebSocket push. The captured handle is detached from the new DOM, click
throws.

## Fix

Switch to a selector-based click with a small retry loop (3 attempts ×
200ms backoff), so a detach mid-attempt re-resolves a fresh element.

Test logic unchanged; just defensive against re-render between query and
click.

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-04-30 20:46:03 -07:00
Kpa-clawbot 6273a8797b test(e2e): wait for #ngStats hydration before counting cards (#940)
## Problem

E2E test "Analytics Neighbor Graph tab renders canvas and stats"
intermittently fails with `Neighbor Graph stats should have >=3 cards,
got 0` (e.g. run 25185836669).

The same suite passes on neighboring runs (master + PR #939) within
minutes. The failure correlates with timing/load, not code change.

## Root cause

`#ngStats` cards render asynchronously after `#ngCanvas` mounts. The
test waits for the canvas, then immediately reads `#ngStats .stat-card`
count. On slower runs the read happens before stats hydrate → 0 cards →
assert fail.

Other Analytics tabs in the same file already use
`page.waitForFunction(...)` to poll for content (e.g. Distance tab on
line 654). Neighbor Graph block was missing the equivalent wait.

## Fix

Add the same defensive wait before counting:

```js
await page.waitForFunction(
  () => document.querySelectorAll('#ngStats .stat-card').length >= 3,
  { timeout: 8000 },
);
```

Test-only change. No frontend code touched. Bounded by 8s timeout
matching other Analytics waits.

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-04-30 20:01:35 -07:00
Kpa-clawbot abd9c46aa7 fix: side-panel Details button opens full-screen on desktop (#892)
## Symptom
🔍 Details button in the nodes side panel does nothing on click.

## Root cause (4th regression of the same shape)
- Row click → `selectNode()` → `history.replaceState(null, '',
'#/nodes/' + pk)`
- Details button click → `location.hash = '#/nodes/' + pk`
- Hash is already that value → assignment is a no-op → no `hashchange`
event → no router → panel stays open.

## Fix
Mirror the analytics-link branch already inside the panel click handler:
`destroy()` then `init(appEl, pubkey)` directly (which hits the
`directNode` full-screen branch unconditionally). Also `replaceState` to
keep the URL in sync.

## Test
New Playwright E2E: open side panel via row click, click Details, assert
`.node-fullscreen` appears.

## Why this keeps regressing
Every time we tighten the row-click handler to use `replaceState`
(correct — avoids hashchange flicker), the button-click handler that
uses `location.hash` becomes a no-op for the same pubkey. Need to
remember they're coupled. Worth a follow-up to extract a
`navigateToNode(pk)` helper that always works regardless of current hash
state — filing as #890-followup if not already there.

Co-authored-by: you <you@example.com>
2026-04-21 22:37:15 -07:00
Kpa-clawbot 6ca5e86df6 fix: compute hex-dump byte ranges client-side from per-obs raw_hex (#891)
## Symptom
The colored byte strip in the packet detail pane is offset from the
labeled byte breakdown below it. Off by N bytes where N is the
difference between the top-level packet's path length and the displayed
observation's path length.

## Root cause
Server computes `breakdown.ranges` once from the top-level packet's
raw_hex (in `BuildBreakdown`) and ships it in the API response. After
#882 we render each observation's own raw_hex, but we keep using the
top-level breakdown — so a 7-hop top-level packet shipped "Path: bytes
2-8", and when we rendered an 8-hop observation we coloured 7 of the 8
path bytes and bled into the payload.

The labeled rows below (which use `buildFieldTable`) parse the displayed
raw_hex on the client, so they were correct — they just didn't match the
strip above.

## Fix
Port `BuildBreakdown()` to JS as `computeBreakdownRanges()` in `app.js`.
Use it in `renderDetail()` from the actually-rendered (per-obs) raw_hex.

## Test
Manually verified the JS function output matches the Go implementation
for FLOOD/non-transport, transport, ADVERT, and direct-advert (zero
hops) cases.

Closes nothing (caught in post-tag bug bash).

---------

Co-authored-by: you <you@example.com>
2026-04-21 22:17:14 -07:00
Kpa-clawbot 67aa47175f fix: path pill and byte breakdown agree on hop count (#885)
## Problem
On the packet detail pane, the **path pill** (top) and the **byte
breakdown** (bottom) showed different numbers of hops for the same
packet. Example: `46cf35504a21ef0d` rendered as `1 hop` badge followed
by 8 node names in the path pill, while the byte breakdown listed only 1
hop row.

## Root cause
Mixed data sources:
- Path-pill badge used `(raw_hex path_len) & 0x3F` (= firmware truth for
one observer = 1)
- Path-pill names used `path_json.length` (= server-aggregated longest
path across observers = 8)
- Byte breakdown section header used `(raw_hex path_len) & 0x3F` (= 1)
- Byte breakdown rows were sliced from `raw_hex` (= 1 row)
- `renderPath(pathHops, ...)` iterated all `path_json` entries

For group-header view, `packet.path_json` is aggregated across observers
and therefore longer than the raw_hex of any single observer's packet.

## Fix
Both surfaces now render from `pathHops` (= effective observation's
`path_json`). The raw_hex vs path_json mismatch is still logged as a
console.warn for diagnostics, but does not drive the UI.

With per-observation `raw_hex` (#882) shipped, clicking an observation
row already swaps the effective packet so both surfaces stay consistent.

## Testing
- Adds E2E regression `Packet detail path pill and byte breakdown agree
on hop count` that asserts:
  1. `pill badge count == byte breakdown section count`
  2. `rendered hop names ≈ badge count` (within 1 for separators)
  3. `byte breakdown rendered rows == section count`
- Manually reproduced on staging with `46cf35504a21ef0d` (8-name path +
`1 hop` badge before fix).

Related: #881 #882 #866

---------

Co-authored-by: you <you@example.com>
2026-04-21 17:57:06 -07:00
Kpa-clawbot a605518d6d fix(#881): per-observation raw_hex — each observer sees different bytes on air (#882)
## Problem

Each MeshCore observer receives a physically distinct over-the-air byte
sequence for the same transmission (different path bytes, flags/hops
remaining). The `observations` table stored only `path_json` per
observer — all observations pointed at one `transmissions.raw_hex`. This
prevented the hex pane from updating when switching observations in the
packet detail view.

## Changes

| Layer | Change |
|-------|--------|
| **Schema** | `ALTER TABLE observations ADD COLUMN raw_hex TEXT`
(nullable). Migration: `observations_raw_hex_v1` |
| **Ingestor** | `stmtInsertObservation` now stores per-observer
`raw_hex` from MQTT payload |
| **View** | `packets_v` uses `COALESCE(o.raw_hex, t.raw_hex)` —
backward compatible with NULL historical rows |
| **Server** | `enrichObs` prefers `obs.RawHex` when non-empty, falls
back to `tx.RawHex` |
| **Frontend** | No changes — `effectivePkt.raw_hex` already flows
through `renderDetail` |

## Tests

- **Ingestor**: `TestPerObservationRawHex` — two MQTT packets for same
hash from different observers → both stored with distinct raw_hex
- **Server**: `TestPerObservationRawHexEnrich` — enrichObs returns
per-obs raw_hex when present, tx fallback when NULL
- **E2E**: Playwright assertion in `test-e2e-playwright.js` for hex pane
update on observation switch

E2E assertion added: `test-e2e-playwright.js:1794`

## Scope

- Historical observations: raw_hex stays NULL, UI falls back to
transmission raw_hex silently
- No backfill, no path_json reconstruction, no frontend changes

Closes #881

---------

Co-authored-by: you <you@example.com>
2026-04-21 13:45:29 -07:00
Kpa-clawbot 0ca559e348 fix(#866): per-observation children in expanded packet groups (#880)
## Problem
When a packet group is expanded in the Packets table, clicking any child
row pointed the side pane at the same aggregate packet — not the clicked
observation. URL would flip between `?obs=<packet_id>` values instead of
real observation ids.

## Root cause
The expand fetch used `/api/packets?hash=X&limit=20`, which returns ONE
aggregate row keyed by packet.id. Every child therefore carried
`data-value=<packet.id>`.

## Fix
Switch the expand fetch to `/api/packets/<hash>`, which includes the
full `observations[]` array. Build `_children` as `{...pkt, ...obs}` so
each child row gets a unique observation id and observation-level fields
(observer, path, timestamp, snr/rssi) override the aggregate.

## Verified live on staging
Tested on multiple packets:
- Click group-header → side pane shows observation 1 of N (first
observer)
- Click child row → pane updates to show THAT observer's details:
observer name, path, timestamp, obs counter (K of N), URL
`?obs=<observation_id>`

## Tests
592 frontend tests pass (no new ones — this is a wiring fix, live
E2E-verified instead).

Closes #866

---------

Co-authored-by: Kpa-clawbot <agent@corescope.local>
Co-authored-by: you <you@example.com>
2026-04-21 13:36:45 -07:00
Kpa-clawbot bb09123f34 test(#833): update deep-link Playwright assertion for full-screen desktop view (#834)
Closes #833

## What
Update Playwright E2E assertion for desktop deep link to
`/#/nodes/{pubkey}`. Now expects `.node-fullscreen` to be present
(matches the spec set by PR #824 / issue #823).

## Why
The previous assertion encoded the old pre-#823 behavior — "split panel
on desktop deep link." PR #824 intentionally removed the
`window.innerWidth <= 640` gate so desktop deep links open the
full-screen view (matching the Details link path that #779/#785/#824
ultimately made work). The test failed on every PR that rebased onto
master, blocking `Deploy Staging`.

## Verified
- 1-test diff, no other behavior change
- Mobile-viewport assertions elsewhere already exercise the same
`.node-fullscreen` selector

Co-authored-by: Kpa-clawbot <agent@corescope.local>
2026-04-21 05:37:05 +00:00