Commit Graph
235 Commits
Author SHA1 Message Date
dborup 40dc297e2b Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	public/packets.js
2026-07-18 11:16:05 +02:00
Kpa-clawbotandclawbot a06ac8aceb fix(#1849): render — for TRACE Hop Bytes column (path bytes are SNR, not hop hashes) (#1850)
Fixes #1849.

## Problem
The packets table "HB" (Hop Bytes / col-hashsize) column always shows
`1` for TRACE packets. TRACE packets use header path bytes as per-hop
SNR readings, not truncated hop hashes — see
`internal/packetpath/route.go` `PathBytesAreHops(TRACE) = false`. The
high-2-bit "hash_size" derivation applied to a SNR byte is meaningless
(typically `1`).

## Fix
At the 3 render sites in `public/packets.js` (`buildGroupRowHtml` header
row, its child rows, `buildFlatRowHtml`), when `payload_type === 9`
(TRACE) render `—` in the `col-hashsize` cell with a `title` tooltip
explaining that TRACE path bytes are SNR readings and directing users to
the sidebar decoder for the actual hop count. Non-TRACE rows unchanged.

## TDD
- Red commit: `dd7a4e78` — `test-issue-1849-trace-hashbytes.js` asserts
col-hashsize cell equals `—` with a title tooltip for TRACE, numeric for
non-TRACE. CI must fail on this commit.
- Green commit: `115368d0` — 3-site fix in `public/packets.js`.

## Verification
```
$ node test-issue-1849-trace-hashbytes.js
✅ All 4 tests passed
```
Preexisting failures in `test-packets.js` (13) are unchanged by this PR
(verified via `git stash`) — unrelated to the surface touched here.

## Scope
- `public/packets.js`: 3 render sites, +21/-8.
- `test-issue-1849-trace-hashbytes.js`: new unit test.
- `test-all.sh`: wire new test.
No public API change.

---------

Co-authored-by: clawbot <bot@example.invalid>
2026-07-18 01:47:37 -07:00
dborup aaef080f94 feat: surface scope on the Packets tab's transport badge
The packet detail pane already showed scope (with an "unknown scope"
fallback for empty scope_name), but the packets table itself gave no
at-a-glance signal — the existing transportBadge() "T" marker only
encoded route type.

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

QueryGroupedPackets (SQLite + in-memory) didn't select scope_name at
all — added it, since the Packets tab defaults to the grouped view.
2026-07-17 15:47:54 +02:00
b74a64ccfa fix(ui): canonical payload label map across packets/live/packet-filter (#1799) (#1804)
## Summary

Replaces the three drifted per-surface payload-type label vocabularies
with a single canonical map keyed by firmware enum name.

Per the locked triage comment on #1799
([comment-4823975431](https://github.com/Kpa-clawbot/CoreScope/issues/1799#issuecomment-4823975431)):

> Create `public/payload-labels.js` exporting `{GRP_DATA: {short:'Group
Data', long:'Group data packet', enumId:6}, ...}`. Migrate `packets.js
typeMap`, `packet-filter.js FW_PAYLOAD_TYPES`, `live.js TYPE_COLORS
legend` to consume it. E2E that scrapes each surface and asserts label
equality.

## Changes

- **`public/payload-labels.js`** (new) — canonical map exposed as
`window.PayloadLabels` and `window.PayloadLabelsApi`. Keys are firmware
enum names; values carry `{short, long, enumId}` plus derived
`SHORT_BY_ID` / `FW_PAYLOAD_TYPES` / `TYPE_ALIASES` for legacy callers.
- **`public/packets.js`** — `TYPE_NAMES` + `typeMap` now read from
`PayloadLabelsApi.SHORT_BY_ID`. Literal kept only as a defensive
fallback for the case where the script tag fails to load.
- **`public/packet-filter.js`** — `FW_PAYLOAD_TYPES` + `TYPE_ALIASES`
now sourced from `PayloadLabelsApi`. Literal fallback retained so `node
test-packet-filter.js` still works headlessly.
- **`public/live.js`** — legend `<li>` rows are now generated from
`window.PayloadLabels` in stable order, killing the third-vocabulary
`Message — Group text` / `Direct — Direct message` drift the #1797
review surfaced.
- **`public/index.html`** — `<script src="payload-labels.js">` loaded
before `roles.js` / `packet-filter.js` / `packets.js`.
- **`test-issue-1799-label-vocab-e2e.js`** (new) — Playwright E2E.
Scrapes `#liveLegend` rows and the `/packets` type-filter checklist,
asserts each label matches `window.PayloadLabels[ENUM].short` for
`TXT_MSG`, `GRP_TXT`, `GRP_DATA`. Also verifies `window.PacketFilter`
still recognises the enum names.
- **`.github/workflows/deploy.yml`** — wired the new E2E into the
existing Playwright block.

## TDD trail

- Red commit `eb392d4` — adds the failing E2E only (asserts
`window.PayloadLabels` exists and labels match; both fail).
- Green commit `44e902a` — introduces the canonical map and migrates the
three surfaces.

## Verification

- `node test-packet-filter.js` — 92/92 pass with the new fallback
wiring.
- Preflight: `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh
origin/master` — clean.

Browser verified: E2E `test-issue-1799-label-vocab-e2e.js` exercises
`/live` legend + `/packets` type filter against a Playwright headless
Chromium; CI's Playwright block runs it on every push.

E2E assertion added: `test-issue-1799-label-vocab-e2e.js:139` —
`assert(fromLegend === canon, ...)` and `assert(fromPackets === canon,
...)` per enum.

Fixes #1799

---------

Co-authored-by: mc-bot <bot@corescope>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@kpa.com>
Co-authored-by: clawbot <bot@clawbot.local>
2026-06-30 05:48:47 -07:00
707d70c738 fix(packets): clamp .col-details to one line on mobile (#1770 S path) (#1805)
## Summary

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

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

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

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

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

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

## TDD

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

## Files touched

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

## Preflight

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

---------

Co-authored-by: clawbot <bot@clawbot.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-28 07:48:39 -07:00
Kpa-clawbotandclawbot b3189c613a fix(#1802): decode CONTROL DISCOVER_REQ/RESP subtype + body fields (#1806)
## Summary
Extend CONTROL packet decoding to surface DISCOVER_REQ / DISCOVER_RESP
subtype plus body fields in the packet detail view. Previously only the
byte0 zero-hop flag was decoded; the body was rendered as opaque hex.

## What changed

**Backend** — `cmd/ingestor/decoder.go` `decodeControl()`
- New `Payload` fields (all omitempty): `CtrlSubtype`, `CtrlFilter`,
`CtrlTag`, `CtrlSince`, `CtrlNodeType`, `CtrlSNR`, `CtrlPubKey`.
- Subtype derived from `byte0 & 0xF0`: `0x80` → `DISCOVER_REQ`, `0x90` →
`DISCOVER_RESP`, otherwise `UNKNOWN`.
- REQ body parsed when `len(buf) >= 6`: `filter:u8 | tag:u32 LE`, plus
optional `since:u32 LE` when 4 more bytes remain.
- RESP body parsed when `len(buf) >= 6`: `node_type` (low nibble of
byte0), `snr:i8`, `tag:u32 LE`, and `pubkey` hex — 32 bytes when full, 8
bytes when prefix-only.
- Every field gated on length; short/truncated bodies emit subtype only
and never panic.
- `CtrlZeroHop` retained for backwards compatibility (rename flagged for
follow-up per triage).

**Frontend** — `public/packets.js` `getDetailPreview()`
- New `decoded.type === 'CONTROL'` branch renders subtype + present body
fields (filter / tag / since / node_type / snr / pubkey). Each field
shown only when populated, so truncated CONTROL still gets a subtype
label.

## Wire format reference
- `firmware/src/Mesh.cpp:69` — `CTL_TYPE_NODE_DISCOVER_REQ=0x80`,
`CTL_TYPE_NODE_DISCOVER_RESP=0x90`.
- `firmware/examples/simple_repeater/MyMesh.cpp:773-820` — body parse /
build.

## Tests (red → green, per AGENTS.md STRICT TDD)
- `cmd/ingestor/issue1802_test.go` — 6 cases: REQ full body (with
since), REQ no-since, RESP 32B pubkey, RESP 8B prefix pubkey, RESP
truncated pubkey (no panic, no pubkey emitted), short body (subtype
only), unknown subtype. Red commit `43713d3a` → green commit `d4b28180`.
Pre-existing CONTROL tests (`TestDecodeControlZeroHop`,
`TestDecodeControlMultiHop`) still pass.
- `test-packets.js` — 3 cases on `getDetailPreview`: DISCOVER_REQ
(filter+tag rendered), DISCOVER_RESP (snr+pubkey rendered), UNKNOWN
subtype label. Red commit `be23e349` → green commit `845d6c48`.

## Preflight overrides
- `check-branch-clean` (cross-stack): justified — issue #1802 explicitly
spans backend decoder (`cmd/ingestor/decoder.go`) and frontend renderer
(`public/packets.js`) per triage comment. Tests in both layers.
Single-purpose PR.

## Scope discipline
Files touched: `cmd/ingestor/decoder.go`,
`cmd/ingestor/issue1802_test.go`, `public/packets.js`,
`test-packets.js`. No other files. No firmware changes. No
`cmd/server/decoder.go` changes. No `CtrlZeroHop` rename (deferred per
triage).

Fixes #1802

---------

Co-authored-by: clawbot <bot@meshcore.local>
2026-06-28 06:32:07 -07:00
Kpa-clawbotandopenclaw-bot 120ac052d3 fix(packets): add Multipart/Control/Raw Custom to type filter checklist (#1798) (#1803)
## Summary

Fixes #1798. Extends the Packets-page `typeMap` in `public/packets.js`
to include three firmware payload types that were previously missing
from the multi-select checklist:

- `10` — Multipart
- `11` — Control
- `15` — Raw Custom

Other surfaces (`public/packet-filter.js` `FW_PAYLOAD_TYPES`,
`public/live.js` `TYPE_COLORS`, `public/map.js`) already knew about
these types; only the Packets-page checklist UI omitted them, forcing
operators to hand-type filter expressions to filter on them.

## Red → green

- Red commit: `359e3645ac41506e563c19dfbd49983fb4ec9638` — adds E2E that
opens `#typeMenu` and asserts each new `data-type-id="10|11|15"`
checkbox renders with the exact label. Fails on assertion (DOM selectors
return null) against the pre-fix `typeMap`.
- Green commit: `f484e8cb88659b14fed7aa7fefcdfb3f0eb6c186` — single-line
literal extension; test goes green.

## E2E assertion added

`test-e2e-playwright.js:576` — `Packets type filter includes
Multipart/Control/Raw Custom (#1798)` (asserts the three new
`data-type-id` checkboxes render with their exact labels in the rendered
Packets-page checklist DOM).

## Files touched

- `public/packets.js` — extend `typeMap` literal
- `test-e2e-playwright.js` — new E2E test asserting the three checkboxes
render

## Browser verified

E2E test scrapes the rendered Packets-page DOM via Playwright; CI runs
it against the local Go server fixture in the `e2e-test` job.

Fixes #1798

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-28 01:23:47 -07:00
d5ceb27334 fix(#1792): decode GRP_DATA channel hash + inner data_type/len/blob in details cell (#1796)
Red commit: 11d8c51e8a (CI:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1792)

## What

Render GRP_DATA (PAYLOAD_TYPE 0x06) channel hash + (when decrypted)
inner `data_type` / `data_len` / blob hex in the packets table "details"
cell, mirroring the existing GRP_TXT branch in `public/packets.js
getDetailPreview()`.

Previously these packets showed only the opaque payload bytes —
operators had no way to see channel distribution or recognize specific
data_type values at a glance.

## How

`public/packets.js` — one new branch right after the GRP_TXT branch:

- Always renders `Ch 0xNN` (from `channelHashHex` or computed from
`channelHash`).
- `decryptionStatus = no_key | decryption_failed` → status label, same
shape as GRP_TXT.
- `decryptionStatus = decrypted` → adds `type=0xNNNN len=N` plus a
`<code>` block with the blob hex, truncated to 32 hex chars (16 bytes)
with `…` when longer.

Inner layout per `firmware/src/helpers/BaseChatMesh.cpp:382-385` (uint16
LE data_type, u8 data_len, blob).

## TDD

- **Red commit** `11d8c51e`: 4 new assertion-shaped failures in
`test-packets.js` getDetailPreview suite (`packets.js tests: 70 passed,
17 failed` → +4 vs baseline 13).
- **Green commit** `3466ed09`: 17 → 13 failed (baseline
emoji-vs-Phosphor drift, unrelated). All 4 GRP_DATA assertions pass.

## Optional check #2 — backend JSON parity

Confirmed `cmd/server/decoder.go` and `cmd/ingestor/decoder.go` use
identical JSON tags (`channelHashHex`, `decryptionStatus`, `dataType`,
`dataLen`, `decryptedBlob`). No backend change needed — server emits
envelope-only fields, ingestor adds the inner fields when a channel key
matches; the frontend handles both shapes.

## Scope

- 2 files: `public/packets.js` (+17 lines), `test-packets.js` (+48 lines
test).
- No public API change. No CSS. No migration.

Preflight clean (PII, branch scope, red commit, CSS, LIKE-on-JSON,
sync/async migration, XSS sinks — all pass).

Fixes #1792

---------

Co-authored-by: Kpa-clawbot <bot@example.com>
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: Kpa-clawbot <bot@openclaw.local>
2026-06-28 02:17:27 +00:00
770749a8ce fix(#1791): add 'Group Data' (payload_type=6) to packets type filter (#1797)
Fixes #1791.

## What

Adds `6:'Group Data'` to the `typeMap` in `public/packets.js` so the
Packets-view "message type" multi-select shows a Group Data checkbox.
The filter pipeline already keys by integer payload_type, so this just
registers the missing option. Also aligns the Live-view legend label in
`public/live.js` to "Group Data" for cross-view consistency.

## Why

Triage (in #1791) confirmed payload_type=6 (GRP_DATA) was the only
ordinary type omitted from the static `typeMap`. `packet-filter.js`,
`live.js`, `app.js`, and `map.js` all already know about it — only the
Packets-page checklist was missing it.

## Test (TDD red → green)

Branch history (4 production commits before round-1 review):

- `19ed5beb` — **test-only red commit**: adds Playwright E2E that opens
the type-filter menu, asserts a `data-type-id="6"` checkbox labeled
"Group Data" exists, selects it, and asserts every visible row's type
badge reads "Group Data". Also seeds one GRP_DATA packet into the CI
fixture (`.github/workflows/deploy.yml`) so the filter has a row to
match.
- `823a7d8d` — adds the one-line `typeMap` entry. First CI run on this
commit failed on an unrelated test (not the #1791 assertion); the #1791
test ran and passed.
- `eec2428` — fixture cleanup: `path_json=[]`/`resolved_path=[]` so the
seeded GRP_DATA hop-row count matches the raw_hex `path_len=0`. CI
green.
- `8f85f5f` — labels the type-6 entry "Group Data" (was briefly "Grp
Data"). CI green.

E2E assertion: `test-e2e-playwright.js` block `Packets type filter
includes Group Data (#1791)`.

## Round-1 review follow-ups

- `e3651c99` — `public/live.js` legend: `'Grp Data'` → `'Group Data'`.
- `4475c2f7` — test cleanup hardening: error string aligned to
assertion, duplicated selector extracted, regex tightened to strict
equality, `#typeMenu` explicitly closed, `meshcore-time-window`
localStorage key cleared, page reloaded so the in-memory `selectedTypes`
Set is reset.
- `b90bc33f` — `.github/workflows/deploy.yml`: drop self-referential
`#1797` citation from fixture comment, switch synthetic fixture id from
`-1` to `-1000000` sentinel with explanatory comment.

## Scope

Single-line typeMap registration plus its E2E test scaffolding, fixture
seed, and the live.js label alignment.

---------

Co-authored-by: clawbot <bot@openclaw.dev>
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-27 19:09:18 -07:00
d954ea7444 feat(#1668): axe-core CI gate for WCAG AA color-contrast (M5) (#1696)
Partial fix for #1668 (M5 of 6).

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

## What's in the box

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

## Allowlist format

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

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

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

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

## TDD red→green

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

## Scope discipline

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

## CI wiring

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

## Deps

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

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-06-12 20:00:35 -07:00
a4af0285fd fix(#1692): parallelize loadObservers + loadPackets in /packets init() (#1693)
## Summary

Fixes #1692 — `public/packets.js::init()` serialized `loadObservers()`
and `loadPackets()`, blocking `/api/packets` behind `/api/observers`. On
loaded CI runners the cumulative wait pushed first-row render to 25–40s,
which is the root cause of the persistent #1662 slideover flake and a
real operator-felt latency on slow links.

## Fix (Option B — `Promise.all`)

```js
// before
await loadObservers();
loadPackets();

// after
await Promise.all([loadObservers(), loadPackets()]);
```

Option B chosen over fire-and-forget (Option A) because `renderLeft()`
synchronously iterates `observers` to build the observer-filter dropdown
(`for (const o of observers)` at packets.js:1636). With Option A the
menu would render empty on first paint and not refresh until the next
user-triggered render. Promise.all preserves the existing render
contract while halving worst-case latency — the two fetches now run in
parallel and the slower one gates `renderLeft()`.

## TDD

- **RED `c7184188`** — `test-issue-1692-packets-init-parallel-e2e.js`
stubs `/api/observers` with a 4s delay via `page.route()`, asserts first
`tr[data-hash]` < 3000ms. Fails on serial init (blocked at 4s).
- **GREEN `903020c5`** — init refactor + wire test into
`.github/workflows/deploy.yml` deploy job.

## Out of scope (separate PR per #1692 acceptance #2/#3)

The 30s row-wait timeout and 3-iter flake-gate in
`test-slideover-1056-e2e.js` + `deploy.yml` were stop-gaps for the
underlying serialization. They stay in this PR — they should be reverted
in a follow-up after operators confirm the latency fix holds in
production.

## Preflight

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

## Browser verification

Local headless chromium on this sandbox crashes on the heavy `/packets`
page (small `/dev/shm`, ARM constraints documented in AGENTS.md). Test
is gated on CI runner where the harness runs.

---------

Co-authored-by: CoreScope Bot <bot@corescope.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
2026-06-12 16:23:08 -07:00
79cf453660 feat(#1633): customizer toggle to hide 1-byte path hops everywhere (#1689)
## What

Customize-v2 toggle **Hide 1-byte path hops** (Display tab). Default OFF
— operators opt in. When ON, 1-byte path-hash prefixes are filtered at
every render site without touching what's stored or what the firmware
does.

Render sites wired:
- **Packets list / detail** (`packets.js renderPath`) — group header,
child observations, detail dt/dd, BYOP overlay. Empty result renders
`(1-byte filtered)`.
- **Map polylines** (`map.js drawPacketRoute`) — intermediate hops
tagged `_hopHex`; origin/destination (from payload, no `_hopHex`) always
survive.
- **Route view** (`route-view.js`) — unique-paths picker + group counts
key on the filtered hop list, so routes that only differ by 1-byte hops
collapse.
- **Analytics route patterns** (`analytics.js`) — filters INPUT rows
whose `rawHops` contain any 1-byte token; header reports filtered/total.

## Why

1-byte hashes collide ~8-way at ~2k relay nodes (Cascadia scale). The
collisions inflate polyline noise, route-pattern row counts, and chip
clutter without adding signal. See #1633 for the full hypothesis.

## How (pure render-time)

New `public/hop-filter.js`:
- `MC_getHide1ByteHops()` / `MC_setHide1ByteHops(on)` — localStorage
`meshcore-hide-1byte-hops`, default OFF.
- `MC_isVisibleHop(hop, opts)` — predicate.
- `MC_filterPathHops(hops, opts)` — non-mutating array filter.

Nothing in the ingest / store / decode path changes. The hop hex stays
in `path_json`; only the render iterators drop it.

## Tests

`test-issue-1633-hide-1byte-hops.js` — 8 assertions:
- Default OFF (back-compat).
- `hopByteLen` semantics.
- `isVisibleHop` ON drops 1-byte, keeps 2/3-byte.
- `filterPathHops` non-mutating.
- `HopDisplay.renderPath` chip set after filter.
- Map polyline positions[] filter preserves origin/destination.
- Analytics route-pattern aggregation key collapses on filtered hops.

Wired into `.github/workflows/deploy.yml`.

Red commit: `6baa3f13` (5/8 ON-branch assertions failed on stubs).
Green commit: `5c0bbdba` (8/8 pass).

## Browser verify

Staging deploy of changed files. Packet `99ef781f42eb7249` (all 1-byte
path):
- BEFORE (toggle OFF): `3 HOPS — Station Rat → KO6IFX-R5 → little
russia`.
- AFTER (toggle ON): `3 HOPS — (1-byte filtered)`.
Customizer toggle visible + working in Display tab.

Fixes #1633.

---------

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

**Fixes #1648.**

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

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

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

## Carry-forwards addressed

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

## Lint gate (M6 headline)

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

## routes.go smoke check

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

## PR closing list

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

---------

Co-authored-by: Bot <bot@corescope>
2026-06-11 05:44:37 -07:00
Kpa-clawbotandbot 2b6809cd28 M4: emoji → Phosphor Icons — map & route overlays (#1648) (#1652)
Draft for milestone 4 of #1648 — emoji → Phosphor Icons (map & route
overlays).

Currently at the red commit (failing test only). Implementation follows.

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

---------

Co-authored-by: bot <bot@corescope>
2026-06-11 03:58:29 -07:00
Kpa-clawbotandopenclaw-bot 3062745437 M2: emoji → Phosphor Icons — page headers & table chrome (#1648) (#1650)
Red commit: df6a406a89 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2F1648-m2-headers-tables)

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

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

## Per-file swap counts

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

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

## Tests

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

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

## Browser verification

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

## Out of scope (carried forward)

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

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

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-11 00:42:33 -07:00
efitenandClaude Opus 4.8 9002b25bce fix(nodes): paginate /api/nodes across map/live/analytics/packets/area-map (500-row cap) (#1637)
## Summary

The server clamps `/api/nodes` `?limit` to **500** (DoS guard, PR #1540
/ v3.8.3) and orders by `last_seen DESC`. Every node-list consumer
issued a single big-`?limit` fetch and trusted it as the full set, so on
>500-node meshes the top-500-by-advert window silently hid the tail.

Because `nodes.last_seen` is updated **only on self-adverts** (never on
relay traffic; `UpsertNode` is called solely from the advert path), a
repeater that relays constantly but last advertised hours ago fell
outside that window and **vanished from the map and live view** — while
still showing "Active" in its detail panel and (since #1606) in the
paginated Nodes list.

#1606 fixed only the Nodes page (`nodes.js`). This generalizes that fix
to the deferred siblings.

## Changes

- **`public/app.js`** — new shared `fetchAllNodes(extraQuery, opts)`:
pages `limit=500` + `offset` until a short page (the server's `total` is
unreliable — clamped to the page size and overwritten with the filtered
length under area/region filters, so we stop on a short page, not on
`total`), dedups by `public_key`, returns the real deduped count as
`total`.
- **`public/map.js`**, **`public/live.js`** (keeps the
`LIVE_MAP_MAX_NODES` ceiling via `safetyCap`), **`public/analytics.js`**
(×2), **`public/packets.js`** now use the helper.
- **`public/area-map.html`** is standalone (cross-origin `baseUrl`, no
`app.js`) so it gets an inline copy of the same loop.
- **`.eslintrc.json`** — declare `fetchAllNodes` global (no-undef).

## Tests

- **`test-fetch-all-nodes-pagination.js`** — unit-tests the helper via
the real `api()`+`fetch` path: pagination past 500, short-page stop vs.
the unreliable server `total`, dedup across a page boundary, counts
pass-through, `safetyCap` bound. 5/5.
- **`test-map-nodes-pagination-e2e.js`** — browser E2E (Playwright)
proving `map.js` surfaces a 501st node reachable only on page 2 and
renders its marker. Verified **red→green**: against the pre-fix single
fetch all 3 assertions fail (500 nodes, page-2 node absent, no marker);
after the fix all pass. Wired into `deploy.yml`.

## Verification

- unit 5/5, E2E 3/3, `test-frontend-helpers.js` 611/611, `npx eslint
public/*.js` → 0 errors.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 04:24:08 -07:00
37a7a92730 fix(#1616): detach slide-over panel on close (architectural focus-restore fix) + --repeat-each=20 CI gate (#1617)
Fixes #1616. Supersedes the soften-and-track approach from #1172 (now
closed).

## What

Architectural fix for the slide-over close path so it no longer
transitions through a `focused-but-hidden` state. Chromium-headless
cannot deterministically order focus/blur events when `panel.hidden =
true` happens in the same microtask as a delegated table re-render —
root cause of the flake family that was blocking ~8 unrelated PRs at a
time and flipping master CI ~50%.

## How (three changes per #1616 acceptance criteria)

1. **Panel detach on close.** `open()` attaches panel + backdrop to
`<body>`; `close()` removes them. `isOpen()` is now a boolean flag
(`panelOpen`) instead of `(!panel.hidden)` — the closed panel literally
does not exist in the document tree, so there is no focused-but-hidden
window.
2. **Focus restore by `data-value` lookup at restore time.** Sync
`tr.focus()` BEFORE detach. If `document.activeElement !== tr` after the
sync call, attach a one-shot `MutationObserver` on the table's `tbody`;
on a matching row re-attach, call `.focus()` once and `disconnect()`.
Observer has a 2s timeout fallback so it doesn't leak when the row is
genuinely gone.
3. **Permanent CI flake-gate.** New step in
`.github/workflows/deploy.yml`: runs `test-slideover-1056-e2e.js` 20
consecutive times. Any single non-zero exit aborts. If this step ever
turns red post-merge, the focused-but-hidden state has crept back in.

## Hard-asserted (no more soft-warn)

All three deferred assertions are now `assert(...)`:

- `focus-restore@800: Escape returns focus to originating row`
- `focus-restore@800: X-button click returns focus to originating row`
- `resize@800→1440 nodes: cleanup releases panel, backdrop, scroll-lock,
focus` (focusRestored portion)

## Commits

- `fce39304` — RED: un-skip the two soft-skipped assertions
- `cead78df` — GREEN: architectural fix (detach + MutationObserver)
- `4f6d5c47` — CI: permanent `--repeat-each=20` flake-gate

## Verification

The 20-run gate is the verification. Watch the new `Slide-over E2E
flake-gate (#1616, --repeat-each=20)` step on this PR's CI; merge only
if it passes.

## Why this is the right fix

Five prior patches (`7891b70`, `366af4f`, `36ebecc`, `df5397f`,
`d681505`) all targeted the focus call ordering and all flaked in CI
Chromium-headless. The unfixable bit is "hidden-but-was-focused" —
Chromium reorders blur/focus across that transition
non-deterministically. Removing the transition (detach instead of hide)
removes the race entirely.

Closes #1616. Closes #1172 (already closed).

---------

Co-authored-by: openclaw-bot <bot@openclaw>
Co-authored-by: CoreScope bot <bot@corescope.local>
Co-authored-by: clawbot <bot@clawbot.local>
2026-06-06 17:43:08 -07:00
Eldoon Nemar 1be0aec808 fix(frontend): reliably restore row focus on panel close (#1602)
fix for the focus-restore@800 E2E test that's currently failing on
master (see runs 26990436988, 26986419081)

Chromium headless is notorious for dropping synchronous or rAF-based
focus restores when elements are hidden. By manually blurring the active
element before hiding the panel, and staggering the focus restore with a
setTimeout macrotask after the rAF, we ensure the focus call lands after
the browser has completed all implicit focus resets and event handlers.

Furthermore, dynamically evaluating the focus resolver directly inside
the deferred focus attempt prevents the target element from becoming
stale if a live WebSocket packet triggers a background table re-render
in the intervening milliseconds.
2026-06-05 05:44:37 -07:00
5fd8900cfc feat(packets): add Path symbols legend disclosure (closes #1504) (#1570)
## Summary

Closes #1504. Adds a tiny, dismissible "Path symbols" legend next to the
Path column header on the Packets page (and reused on the Nodes page's
"Paths Through This Node" card), explaining the three
otherwise-undiscoverable path glyphs:

- `⚠N` — regional conflict count (multiple candidates for the hop's
prefix in this region)
- `⚠️` — unreliable name resolution (best-guess pubkey couldn't be
confirmed)
- dashed underline — ambiguous / global-fallback resolution

## Rationale (from triage)

- **Tufte**: integrate words and graphics. A hidden per-row tooltip
violates "don't make the viewer cross-reference." A small, persistent
inline key next to the column header is dense, on-data, and dismissible.
- **Avoid a modal** — chartjunk for a 3-glyph vocabulary.
- **Munger** rejected the reporter's option #2 (hover overlay that
pauses live updates): a power-user table must not stall from accidental
hovers.
- Single shared constant on `HopDisplay` so the Nodes page reuses the
same vocabulary without drift.

## Files

- `public/hop-display.js` — export `PATH_SYMBOLS_LEGEND` constant +
`renderPathSymbolsLegend()` helper (no changes to existing badge
rendering logic)
- `public/packets.js` — wire renderer into the Path `<th>` header
- `public/nodes.js` — reuse renderer on `#fullPathsSection` h4
- `public/style.css` — minimal styling (subtle dotted-underline trigger
+ floating disclosure panel, all via theme vars)
- `test-frontend-helpers.js` — 5 new assertions (TDD red→green)

## TDD red → green

- RED commit `46741267` — adds 5 assertion-shaped tests; all fail on the
assertion (not on import/build).
- GREEN commit `fab27ec5` — implements the constant, renderer, wiring,
and CSS; all 607 frontend-helper tests pass.

## Tested via

- DOM-grep assertions on the rendered `<details>` markup (`<summary>Path
symbols</summary>`, all three glyphs present, dashed-underline
description).
- Static grep that `packets.js` invokes the shared renderer adjacent to
the Path column.
- Full `test-frontend-helpers.js`, `test-packet-filter.js`,
`test-aging.js` pass.

## Hard rules honored

- No modal, no pause-on-hover, no changes to `hop-display.js`'s badge
rendering logic.
- No `<img>`/SVG additions, no new CSS vars (uses existing theme vars),
no Go changes.
- PII grep clean on every commit and on this body.

Browser verified: manual smoke pending — disclosure is closed-by-default
and uses standard `<details>` semantics; renders inline with column
header.

E2E assertion added: `test-frontend-helpers.js` — `#1504:
renderPathSymbolsLegend returns <details> disclosure with "Path symbols"
summary + all glyphs` (and 4 sibling assertions).

---------

Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer>
Co-authored-by: clawbot <bot@openclaw.local>
2026-06-04 10:30:35 -07:00
f15b677981 fix(security): escape mesh node names before HTML render — stored XSS (#1536) (#1537)
## This PR fixes the stored XSS in full (closes #1536)

Mesh-advertised node names (`adv_name`) and observer names were rendered
into the dashboard DOM **without HTML-escaping** in multiple places —
the same class as the publicly disclosed MeshCore dashboard XSS
(CVE-2026-45323). `adv_name` has no protocol-level validation and the Go
`sanitizeName()` keeps `< > " &`, so a payload like `<img src=x
onerror=...>` reaches the frontend intact and executes.

**I audited every name/sender/text/channel render in `public/` and this
PR escapes all unescaped sinks. There are no known remaining XSS sinks
of this class after this change.**

### Sinks fixed (all escaped via the existing global `escapeHtml`, plus
a local helper for the standalone `area-map.html`)

| File | Sink |
|------|------|
| `app.js` | global search dropdown — node name + channel name |
| `nodes.js` | nodes-table row name; node-detail Leaflet popups (×2) |
| `observers.js` | observers-table name cell |
| `packets.js` | observer-name cells via `obsNameOnly` (×4) + observer
multi-select checkbox label |
| `live.js` | node-filter `<option>` + map marker tooltip |
| `analytics.js` | topology map node tooltip |
| `route-view.js` | hop + union marker tooltips (×2) |
| `area-map.html` | node popups (×2) — added a local `escapeHtml` (file
is standalone) |

### Already-safe (verified, not changed)
`map.js` popups (`safeEsc`), live-feed text (`escapeHtml(preview)`),
packet-detail text, channel messages (`channels.js`), `route-render.js`
popups, `hop-display.js`.

### Why escape at the sink (not the backend)
`sanitizeName()` only strips control chars; HTML-escaping stored names
server-side would be lossy and corrupt legitimate names containing `& <
>`, and break the `meshcore://` deep-links / exports. Output-encoding at
render is the correct OWASP fix and matches `meshcore-card` v0.3.3.

### Tests
- Added 6 `escapeHtml` regression tests including the CVE payload `<img
src=x onerror=alert(1)>` and an attribute-breakout payload.
- `node test-frontend-helpers.js`: **568 passed / 32 failed** — the 32
are pre-existing sandbox limitations (e.g. `AreaFilter is not defined`),
identical to the untouched baseline (562/32). Zero new failures.

### Cache busting
Automatic — the server rewrites `__BUST__` in `index.html` with a
restart timestamp, so no manual bump is needed.

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

---------

Co-authored-by: CoreScope Bot <bot@meshcore>
Co-authored-by: Kpa-clawbot <bot@clawbot.local>
2026-06-03 10:55:02 -07:00
Kpa-clawbotandmc-bot 7fcb226cd8 fix(#1486): collapse chevron no longer reopens closed detail panel (#1492)
## Summary

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

## Root cause

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

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

## Fix

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

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

## Tests

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

## Reproduction (manual, against staging or local)

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

---------

Co-authored-by: mc-bot <bot@meshcore.local>
2026-05-29 15:17:16 +00:00
efitenandClaude Sonnet 4.6 d4280befd4 fix(packets): use route-aware path byte offset for HB column (#1469)
## Summary

- The **HB** (hash bytes) column in the packet list always read byte 1
of `raw_hex` to compute the hash size
- For TRANSPORT routes (`route_type` 0 or 3), the path_len byte sits at
offset 5 — bytes 1–4 are transport codes
- Reading byte 1 for these packets produced the wrong hash size (e.g.
`0xBB` → bits 7-6 = `10` → **3** instead of the correct **2**)
- Fix: use `getPathLenOffset(route_type)` at all three render sites
(grouped header, grouped children, flat row)
- For grouped children that have no `raw_hex`, fall back to deriving
hash size from the path_json hop string lengths

## Test plan

- [ ] Open a TRANSPORT FLOOD packet (`route_type=0`) in the packet list
— HB column now shows the correct value (e.g. 2 instead of 3)
- [ ] Verify FLOOD packets (`route_type=1`) still show the correct hash
size (byte 1 unchanged for non-transport routes)
- [ ] Expand a grouped packet row and confirm child rows show correct
hash size from path_json hop lengths

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 00:52:53 -07:00
Kpa-clawbotandopenclaw-bot b2d654bf61 fix(#1415, #1458): packets layout + mobile chrome + semantic-first detail (#1459)
## Closes #1415 — packets cross-viewport jank
## Closes #1458 — Tufte mobile-packets P0 findings (folded into same
branch)

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

### #1415 — column priority + chrome compaction

Locked column-priority tiers (operator spec):

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

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

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

### #1458 — Tufte mobile P0 findings

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

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

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

### TDD

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

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

---------

Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-28 05:38:28 -07:00
77d1925f30 Route view v2 — Tufte redesign (packet context, multi-path picker, mobile bottom-sheet, CB-preset live colors) (#1423)
# Route view v2 redesign

Fixes #1418, Fixes #1419, Fixes #1422

This is the route-view redesign that came out of a long iterative QA
cycle. The first commit (`a3c39636`) landed the v1 sidebar timeline +
multi-path baseline; this PR's second commit (`0e2e913f`) is the v2
polish covering packet context, multi-path picker, mobile bottom-sheet,
CB-preset live colors, and dozens of operator-driven UX fixes.

## The journey, in one line

> "The data is a sequence. Geography is annotation. The packet is the
cargo, the route is the road — show both."

## New surfaces

### 1. Packet context block (sidebar header)
Above the multi-path chip, a per-type fact list explaining **what** is
traveling. Operator was tired of "the route view shows the road but not
the cargo."

| Type | Chip | Facts |

|-------------|-----------------|---------------------------------------------------------|
| ADVERT | 📡 ADVERT | name · role · sig ✓ · self-reported GPS · pubkey
prefix |
| TXT_MSG | ✉ DM | src → dst · 🔒 encrypted |
| REQ/RESPONSE| 🔒/🔓 REQUEST/…| src → dst · 🔒 encrypted |
| GRP_TXT | # CHANNEL MSG | #channel · 🔓 decrypted · "…content preview…"
· sender |
| TRACE | ⌖ TRACE | Official: N hops · Observed: M |
| PATH | 🔀 PATH | src → dst (with "from payload" chip on SRC/DST rows) |

Sources merge `pkt.decoded_json` + `obs.decoded_json` (channel data
often lives at packet level) and fall back to byte-level `raw_hex`
parsing for encrypted DMs and unkeyed channel msgs.

### 2. Multi-path picker
The header lists every unique observer-path with `<count>/<total>` chip
+ hex hop string. Click a path → full-clear and redraw that path only
(Tufte v6's "replace + retain subpath weights"). "All" →
edge-deduplicated UNION view (each unique edge drawn once, stroke =
observer count, single accent color, no seq numbers because there's no
single ordering).

### 3. Deep-link URLs
`#/map?packet=<hash>&obs=<id>` — bookmarkable, shareable, the single
source of truth. sessionStorage flow removed. "Back to packet" preserves
the obs id.

### 4. Hop resolution
Priority: server `resolved_path` → shared `window.HopResolver` (same
resolver as packets page, observer-IATA-aware) → raw prefix. Eliminates
a whole class of "route view named hops differently than packet detail"
bugs.

### 5. Markers (v5/v6/v7)
- All markers same 22 px filled circle, seq number rendered **inside**
- SRC + DST get a 2 px hollow endpoint ring
- SRC = DST loop → **double concentric ring** (ring grammar extended, no
new glyph)
- Spider-fan within 14 px collisions (16 px arc, dashed hairline),
re-runs on `zoomend` only, debounced

### 6. CB preset live colors
- Each preset gets a `routeRamp` (5 stops): default/trit = viridis,
deut/prot = plasma, achromat = pure luminance
- `cb-presets.js` writes `--mc-rt-ramp-0..4` CSS vars; route reads them
via `getComputedStyle`
- `cb-preset-changed` + `theme-changed` listeners hot-recolor without
re-render

### 7. Desktop chrome
- **Resize handle** on right edge of sidebar (drag, persisted to
`localStorage["mc-rt-sidebar-width"]`)
- **Collapse button** = round chevron **centered on the right edge**
(Material/Drive style — not in the top-right corner, doesn't collide
with the close X)
- Collapsed = 36 px strip with rotated "ROUTE" label, expand on click

### 8. Mobile (bottom sheet)
- Anchored above bottom-nav (`bottom: 56px + safe-area-inset`)
- Collapsed = thin summary line `TYPE · N hops · X km · M obs` + hex
preview, tap chevron to expand to ~75 vh
- Drag-grip removed (conflicted with browser pull-to-refresh +
CoreScope's own pull-to-reconnect)
- Desktop collapse / resize affordances hidden on mobile (sheet is the
mobile collapse affordance)
- Map controls toggle floats top-right, panel collapses on route entry,
reachable via toggle click
- All three mobile detail panels (`pktRight`, `.slide-over-panel`,
`#mobileDetailSheet`) explicitly closed when entering route view

### 9. Map fit / centering
- Manual layer-children walk because `L.LayerGroup.getBounds()` doesn't
aggregate (only `FeatureGroup` does)
- Mobile padding: `paddingTopLeft: [30, 70]`, `paddingBottomRight: [30,
190]` to clear top-nav + sheet+nav stack
- Re-fits on: initial render, isolate, All, `window.resize` (iOS URL-bar
collapse)
- Staggered timers 0/200/600/1400 ms (and 2800 ms on initial render) to
survive layout settles

### 10. Hop drill-in refinements
- SNR sparkline suppresses connecting polyline when n < 3 (two points
implies a trend across time it can't represent — dots only)
- "Node details" link properly chip-styled with aria-label including
node name + route count

## Edge weight scales

| View                            | Range          |
|---------------------------------|----------------|
| Single-path                     | 5 px flat      |
| Multi-path interior             | 3..9           |
| Origin→hop1 / last-hop→dest     | proxy via max adjacent edge count |
| Union overlay                   | 2..8           |

Boundary edges (SRC→first hop, last hop→DST) used to render thin because
`edgeCounts` only tracks `path_json` transitions. Now they take the
strongest adjacent edge count as proxy (every observer who saw the
packet implicitly transited that boundary edge).

## Files

- **NEW** `public/route-tufte.js` (~1700 lines) — the route renderer +
sidebar
- **NEW** `public/route-tufte.css` (~750 lines) — all styling
- **MOD** `public/map.js` — async draw functions, deep-link loader,
`__mc_nodes` exposure, raw_hex extraction
- **MOD** `public/packets.js` — View Route → deep-link URL only, closes
all mobile panels
- **MOD** `public/cb-presets.js` — `routeRamp` per preset + CSS var
write
- **MOD** `public/index.html` — script + stylesheet tags

## Testing

Manually CDP-validated across desktop and mobile-emulator viewports for
every major change. Fixtures cover:
- ADVERT (4 hops, single-obs)
- DM (TXT_MSG, raw_hex parse)
- GRP_TXT (#test channel, decrypted text)
- PATH (operator's bug case)
- TRACE (3-hop)
- 1-hop edge case
- Multi-path (75-observer 4-hop with 47 unique paths)
- 32-hop stress
- Loop (SRC = DST)
- Bay Area dense cluster (spider-fan)

Per AGENTS.md net-new-UI exemption, no failing-test-first; existing
tests stay green. **TODO**: Playwright E2E follow-up PR.

## What's deferred to v2.1 / follow-ups

- **Glyph overlay on SRC marker** for packet type (e.g. 📡 corner glyph
on ADVERT marker, ⌖ on TRACE)
- **Per-hop SNR sparkline for TRACE packets** (their payload contains
real per-hop SNR contributions, distinct from observer-derived SNR)
- **GRP_TXT full content preview** (currently truncated at 80 chars;
could expand inline)
- **Playwright E2E test** covering the deep-link → isolate → All flow

## Screenshots

(would be useful here — CDP screenshots captured during dev show:
desktop with sidebar + multi-path picker, mobile with bottom sheet +
overlay toggle, isolated-path view, union view, spider-fan on Bay Area
cluster, packet context for each of the 5 main types)

## Operator's frustration patterns (lessons for next time)

1. **Browser-validate every UI change, not just compute state** —
CDP-screenshot before claiming a UI fix is done. Verifying
`display:none` resolves correctly is necessary but not sufficient; the
visual layout matters.
2. **Edge-deduplicated drawing beats per-path overlays** for union views
(Tufte v6) — operator's instinct was correct from the start.
3. **Material/Drive UI conventions exist** because they work — center
collapse handles on borders, don't pile them in corners.
4. **Mobile = different problem than desktop** — bottom-sheet, no
drag-grip near pull-to-refresh zone, asymmetric fitBounds padding,
redundant refits to survive iOS URL-bar collapse.

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

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-27 08:01:15 +00:00
317b59ab10 feat: area-based visual node filter — attribute packets by transmitter GPS (#804) (#839)
## Summary

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

## Changes

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

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

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

## Test plan

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

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-21 14:00:15 -07:00
2329639f45 feat: scoped/unscoped transport-route statistics (#899) (#915)
@
## What this PR does

Implements region-scoped transport-route packet tracking with two
sub-features:

### Feature 1 — Scope statistics (`scope_name`)
- At ingest, transport-route packets (route_type 0/3) with Code1 !=
`0000` are HMAC-matched against configured `hashRegions` keys (mirroring
the `hashChannels` pattern). Matched region name (or `""` for unknown)
stored in new `transmissions.scope_name` column via migration
`scope_name_v1`.
- New `GET /api/scope-stats?window=` endpoint (1h/24h/7d, 30s
server-side TTL) returning transport totals, scoped/unscoped counts,
per-region breakdown, and time-series.
- New **Scopes** tab in Analytics with summary cards, per-region table,
and two-line SVG chart. Auto-refreshes every 60s.

### Feature 2 — Node default scope (`default_scope`)
- Per-node `default_scope` column on `nodes`/`inactive_nodes` (migration
`nodes_default_scope_v1`) tracks the most recently matched region for
each node, derived from transport-scoped ADVERT packets.
- `GET /api/nodes` response includes `default_scope` field when column
is present.
- Node detail panel displays the default scope badge.
- Async startup backfill (`BackfillDefaultScopeAsync`) populates the
column for nodes with pre-existing ADVERT data.

### Config
Add `hashRegions` to `config.json` (see `config.example.json`). One
entry per region name (with or without leading `#`).
@

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-21 14:00:06 -07:00
Kpa-clawbotandbot 749fdc114f feat(decoder+ui): close remaining P2 items from #1279 — payloadTypeNames, legend, TransportCodes, Feat1/2, RAW_CUSTOM, sensor docs (#1291)
RED commit: `dc4c0800` — CI:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1279-p2

Closes the remaining six 🟢 P2 items in umbrella #1279 (PR #1280 shipped
P0+P1, PR #1276 shipped ACK/RESPONSE/PATH legend rows).

### Item-by-item

| # | Item | Where | Test |
|---|---|---|---|
| 1 | `payloadTypeNames` parity | `cmd/server/store.go` |
`cmd/server/issue1279_p2_test.go::TestPayloadTypeNamesAll13` |
| 2 | Legend rows: Anon Req / Grp Data / Multipart / Control / Raw
Custom | `public/live.js` | `test-issue-1279-legend-p2-e2e.js`
(Playwright) |
| 3 | TransportCodes detail-row + `code1=` / `code2=` filter grammar |
`public/packets.js`, `public/packet-filter.js` |
`test-issue-1279-p2-code-filter.js` (6 cases) |
| 4 | Multibyte capability badge on node detail/list rows |
`public/nodes.js::renderNodeBadges` | `n.hash_size >= 2` (observable
Feat1/Feat2 proxy; firmware `AdvertDataHelpers.h:14-16`) |
| 5 | RAW_CUSTOM (0x0F) `{rawLength, firstByteTag}` decode + detail-row
| `cmd/server/decoder.go`, `cmd/ingestor/decoder.go`,
`public/packets.js` | `TestDecodeRawCustomExposesLengthAndTag` × 2 +
updated `TestDecodePayloadRAWCustom` |
| 6 | Sensor advert telemetry firmware-derivation comments |
`cmd/ingestor/decoder.go:363-380` | pure comments — exempt per AGENTS |

### Firmware refs cited inline
- `firmware/src/Packet.h:19-32` — PAYLOAD_TYPE_* constants
- `firmware/src/Packet.h:46` — TransportCodes wire layout
- `firmware/src/Mesh.cpp:577` — `createRawData`
- `firmware/src/helpers/SensorMesh.{h,cpp}` — sensor advert telemetry
derivation
- `firmware/src/helpers/AdvertDataHelpers.h:14-16` — Feat1/Feat2

### TDD
Red `dc4c0800` proves the assertions gate behavior:
- `payloadTypeNames` had only 12 entries (no 0x0F).
- RAW_CUSTOM decoded as `UNKNOWN` with no envelope fields.

Green `<HEAD>` makes both green; per-item tests included.

### Cross-stack note
Cross-stack: justified — items 1/5 add decoder output fields; items
2/3/4/5 surface those fields in the UI in the same PR per #1279
acceptance.

### Out of scope
Item 4 surfaces the observable multibyte capability via the persisted
`hash_size` (Feat1/Feat2 wire bits are only on transient adverts and not
stored per-node today); persisting raw Feat1/Feat2 per-node is left for
a follow-up.

Fixes #1279

---------

Co-authored-by: bot <bot@corescope>
2026-05-19 08:08:28 -07:00
Kpa-clawbotandbot e2d320449b fix(#1281): hide empty Location row + theme map link via --accent (#1284)
## Summary
Minimal fix for #1281 — two surgical changes to the packet detail pane:

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

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

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

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

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

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

Fixes #1281

---------

Co-authored-by: bot <bot@local>
2026-05-18 23:37:04 -07:00
Kpa-clawbotandOpenClaw Bot 43203b09b7 fix(#1249): IATA badge missing on fixture + mobile clipping (#1252)
Failing test commit: `bdb4eefb` (added in #1189 R1) — original CI
failure:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25995819598

Fixes #1249.

## Root cause

Two independent bugs surfaced by the same E2E test:

1. **Fixture join broken.** `scripts/capture-fixture.sh` wrote the text
observer hash into `observations.observer_idx`, but the v3 join in
`cmd/server` is `observers.rowid = observations.observer_idx`. The join
silently nulled out `observer_id` / `observer_iata` for every packet.

2. **Mobile clipping.** `.col-observer` had `data-priority=3` (hides at
≤1024px) and was in the narrow-viewport `defaultHidden` list, so at
375px the cell collapsed to `display:none` and `.badge-iata` had a 0×0
box.

## Changes

- `test-fixtures/e2e-fixture.db`: remap `observer_idx` text hash →
integer rowid (500/500 rows resolved).
- `scripts/capture-fixture.sh`: build an `observer_id → rowid` map
before insert; skip rows whose observer isn't in the fixture. Comment
explains the trap.
- `public/packets.js`: bump `.col-observer` priority `3 → 1` and drop
`observer` from narrow-viewport `defaultHidden`.

## Verification

All three sub-tests in `test-observer-iata-1188-e2e.js` pass locally
against the freshened fixture. `curl /api/packets?limit=5` returns real
IATA codes (OAK / MRY / SFO) instead of empty strings.

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.dev>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-17 16:13:11 +00:00
ab34d9fb65 fix(#1206): keep VCR bar from occluding the live packet feed (#1213)
Red commit: `bcfc74de` (CI:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1206)

Fixes #1206.

## Problem
On Live Map the VCR (timeline/playback) bar overlays the bottom of the
viewport. Bottom-pinned overlays — the live packet feed, the legend, any
corner panel — used hard-coded `bottom: 58–88px` offsets that are
smaller than the real bar height (two-row mobile layout +
`env(safe-area-inset-bottom)` push it to ~80px and beyond). The last N
packet-feed rows slid under the bar and became unreadable / unclickable.

## Fix
Publish the bar's measured height as a CSS variable on the live page
and bind every bottom-anchored overlay to it.

- `public/live.js` — new `initVCRHeightTracker()` runs after init; uses
  `ResizeObserver` + `resize` / `visualViewport.resize` to keep
  `--vcr-bar-height` on `.live-page` in sync with `#vcrBar`.
- `public/live.css` — `.live-feed`, `.feed-show-btn`, and the
  `.live-overlay[data-position="bl"|"br"]` corner slots now use
  `bottom: calc(var(--vcr-bar-height, 58px) + 10px)`. The feed's
  `max-height` is also capped against `100dvh - top - vcr - margin`
  so its scroll container can never extend past the bar.
- Stale per-breakpoint overrides (the `@supports(env(safe-area-inset))`
  hard-coded `78px + safe-area` for feed/legend) are removed in favor
  of the single tracked variable.

## TDD
- Red commit `bcfc74de` adds `test-issue-1206-vcr-overlap-e2e.js`:
  asserts `#liveFeed.getBoundingClientRect().bottom <= #vcrBar.top`
  (and same for the last row) at desktop 1280x800 and mid 720x800.
  Verified locally that reverting the green commit makes the feed-bottom
  assertions fail (feed bottom 742px > VCR top 721px) — see PR body for
  exact numbers from the local run.
- Green commit `1ad17e7f` makes all 5 assertions pass.

## Browser verified
Local Go server with `test-fixtures/e2e-fixture.db`, headless Chromium
via the new E2E test — all 5 assertions green.

## E2E assertion added
`test-issue-1206-vcr-overlap-e2e.js:84` (bottom-row vs VCR-top) plus
container check at `:74`.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <bot@corescope.local>
2026-05-16 05:55:21 +00:00
89d644dd72 fix(#1056): row-detail slide-over panel at narrow widths (AC #4) (#1168)
Red commit: 8ac568bac3 (CI run: pending)

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

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

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

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

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

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

Fixes #1056

---------

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

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

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

### What changed (per gap)

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

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

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

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

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

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

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

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

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

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

Plus three viewport-agnostic assertions:

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

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

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

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

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

### TDD discipline (red → green)

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

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

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

### Acceptance criteria status

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

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

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

### Files changed

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

Refs #1128, follows #1131

---------

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

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

Fixes #1128

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

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

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

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

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

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

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

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

## TDD

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

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

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

## E2E

Local run against the e2e fixture:

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

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

## Files

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

---------

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

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

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

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

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

---------

Co-authored-by: clawbot <clawbot@example.com>
Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-05 18:12:33 -07:00
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-clawbotandmeshcore-bot f7d8a7cb8f feat(packets): filter UX — in-UI docs + autocomplete + right-click + saved filters (#966) (#1083)
## Summary

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

Closes #966.

## Surfaces

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

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

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

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

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

## Implementation

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

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

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

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

## Tests

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

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

## Browser validation

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

## Out of scope (deferred per the issue)

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

## Acceptance criteria

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

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:50:12 -07:00
Kpa-clawbotandclawbot 8b924cd217 feat(ui): encode view & filter state in URL hash (#749) (#1072)
## Summary

Encodes view + filter state in the URL hash so deep links restore the
exact page state (issue #749).

## Changes

New shared helper `public/url-state.js` exposing `URLState`:
- `parseSort('col:asc')` → `{column, direction}` (defaults to `desc`)
- `serializeSort('col', 'desc')` → `'col'` (omits default direction)
- `parseHash('#/nodes/abc?tab=x')` → `{route: 'nodes/abc', params:
{tab:'x'}}`
- `buildHash(route, params)` and `updateHashParams(updates,
currentHash)` for round-tripping while preserving subpaths.

Wired into:

- **packets.js** — sort column/direction now in
`#/packets?sort=col[:asc]`, restored on init (overrides localStorage).
Subpath `#/packets/<hash>` preserved.
- **nodes.js** — sort encoded as `#/nodes?sort=col[:asc]`, restored on
init. Subpath `#/nodes/<pubkey>` preserved.
- **analytics.js** — both selected tab (`tab=topology`) AND time-window
picker value (`window=7d`) now round-trip via URL. Subview keys used by
rf-health (`range/observer/from/to`) cleared when switching tabs to keep
URLs clean.

Existing deep links (`#/nodes/<pubkey>`, `#/packets/<hash>`,
`?filter=…`, `?node=…`, `?observer=…`, `?channel=…`, `?timeWindow=…`,
`?region=…`) all keep working — additive change only.

## Tests

TDD red→green:
- Red: `5e1482e` (stub throws "not implemented"; 18/18 tests fail on
assertions)
- Green: `512940e` (helper implemented; 18/18 pass)

Wired `test-url-state.js` into `test-all.sh`.

Fixes #749

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 01:17:22 -07:00
Kpa-clawbotandbot eaf14a61f5 fix(css): 48px touch targets, :active states, hover→tap (#1060) (#1067)
## Summary

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

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

## Acceptance criteria

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

## TDD

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

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

Fixes #1060

---------

Co-authored-by: bot <bot@corescope>
2026-05-05 01:11:08 -07:00
Kpa-clawbotandyou c186129d47 feat: parse and display per-hop SNR values for TRACE packets (#1007)
## Summary

Parse and display per-hop SNR values from TRACE packets in the Packet
Byte Breakdown panel.

## Changes

### Backend (`cmd/server/decoder.go`)
- Added `SNRValues []float64` field to Payload struct
(`json:"snrValues,omitempty"`)
- In the TRACE-specific block, extract SNR from header path bytes before
they're overwritten with route hops
- Each header path byte is `int8(SNR_dB * 4.0)` per firmware — decode by
dividing by 4.0

### Frontend (`public/packets.js`)
- Added "SNR Path" section in `buildFieldTable()` showing per-hop SNR
values in dB when packet type is TRACE
- Added TRACE-specific payload rendering (trace tag, auth code, flags
with hash_size, route hops)

## TDD

- Red commit: `4dba4e8` — test asserts `Payload.SNRValues` field
(compile fails, field doesn't exist)
- Green commit: `5a496bd` — implementation passes all tests

## Testing

- `go test ./...` passes (all existing + 2 new TRACE SNR tests)
- No frontend test changes needed (no existing TRACE UI tests; rendering
is additive)

Fixes #979

---------

Co-authored-by: you <you@example.com>
2026-05-03 11:17:25 -07:00
Kpa-clawbotandyou aea0a9caee fix(packets): preserve scroll position on filter change + group expand/collapse (closes #431) (#996)
## Summary

Closes #431. Preserves scroll position on the packets page when filters
change or groups are expanded/collapsed.

## Problem

When an operator scrolls down through packet history then changes a
filter (type, observer, packet-filter expression) or expands/collapses a
group, `renderTableRows()` rebuilds the DOM which resets `scrollTop` to
0. This forces the user back to the top — frustrating when digging
through hundreds of packets.

## Fix

Save `scrollContainer.scrollTop` at the start of `renderTableRows()`,
restore it after DOM rebuild completes. Two restore points:
1. **Empty-results path** (line ~1821): after `tbody.innerHTML = ...` 
2. **Normal virtual-scroll path** (line ~1840): after
`renderVisibleRows()`

### Key lines changed
- `public/packets.js` lines 1748–1749: save scrollTop
- `public/packets.js` line 1821: restore after empty-state DOM write  
- `public/packets.js` line 1840: restore after renderVisibleRows

## TDD evidence

- **Red commit:** a99ba21 — test asserts scrollTop preserved; fails
without fix
- **Green commit:** 35cc4bf — adds save/restore; test passes

## Anti-tautology

Removing the `scrollContainer.scrollTop = savedScrollTop` lines causes
the test to fail (scrollTop becomes 0 instead of 500). Verified locally.

## Verification

- `node test-packets.js` — 83 passed, 0 failed
- `node test-packet-filter.js` — 62 passed, 0 failed

---------

Co-authored-by: you <you@example.com>
2026-05-02 21:03:01 -07:00
Kpa-clawbotandyou 53ab302dd6 fix(packets): clear-filters button (rebased + addresses greybeard) (closes #964) (#975)
Rebased version of #973 onto current master, with greybeard review
fixes.

## Changes from #973
- **Stowaway revert dropped**: The original PR branched from older
master and inadvertently reverted PR #926's MQTT connect-retry fix
(`cmd/ingestor/main.go` + `cmd/ingestor/main_test.go`). After rebasing
onto current master (which includes #926 + #970), these files no longer
appear in the diff.
- **Greybeard M1 fixed**: Time-window filter (`savedTimeWindowMin`,
`fTimeWindow` dropdown, `localStorage 'meshcore-time-window'`) is now
reset by the clear-filters button. The clear-button visibility predicate
also accounts for non-default time window.
- **Greybeard m1 fixed**: Replaced 7 tautological source-grep tests with
8 behavioral vm-sandbox tests that extract and execute the actual clear
handler + `updatePacketsUrl`, asserting real state transitions.

## Original feature (from #973)
Clear-filters button for the packets page — resets all filter state
(hash, node, observer, channel, type, expression, myNodes, time window,
region) and refreshes. Button visibility auto-toggles based on active
filter state.

Closes #964
Supersedes #973

## Tests
- `node test-clear-filters.js` — 8 behavioral tests ✅
- `node test-packets.js` — 82 tests ✅
- `cd cmd/ingestor && go test ./...` — ✅

---------

Co-authored-by: you <you@example.com>
2026-05-02 12:12:51 -07:00
Kpa-clawbotandyou 4f0f7bc6dd fix(ui): fill remaining gaps in payload-type lookup tables (10/11/15) (#967)
## Summary

Fill the remaining gaps in payload-type lookup tables noted out-of-scope
on #965. Every firmware-defined payload type (0–11, 15) now has entries
in all four frontend tables.

## Changes

Three types were missing from one or more tables:

| Type | Name | `PAYLOAD_COLORS` (app.js) | `TYPE_NAMES` (packets.js) |
`TYPE_COLORS` (roles.js) | `TYPE_BADGE_MAP` (roles.js) |

|------|------|--------------------------|--------------------------|-------------------------|---------------------------|
| 10 | Multipart | added | added | added `#0d9488` | added |
| 11 | Control | added | ✅ (already) | added `#b45309` | added |
| 15 | Raw Custom | added | added | added `#c026d3` | added |

## Color choices

- **MULTIPART** `#0d9488` (teal) — multi-fragment stitching, distinct
from PATH's `#14b8a6`
- **CONTROL** `#b45309` (amber) — warm brown, distinct hue from ACK's
grey `#6b7280`
- **RAW_CUSTOM** `#c026d3` (fuchsia) — magenta, distinct from TRACE's
pink `#ec4899`

All pass WCAG 3:1 contrast against both white and dark (#1e1e1e)
backgrounds.

## Tests

- `test-packets.js`: 82/82 ✅
- `test-hash-color.js`: 32/32 ✅

Badge CSS auto-generation: `syncBadgeColors()` in `roles.js` iterates
`TYPE_BADGE_MAP` keyed against `TYPE_COLORS`, so the three new entries
automatically get `.type-badge.multipart`, `.type-badge.control`, and
`.type-badge.raw-custom` CSS rules injected at page load.

Firmware source: `firmware/src/Packet.h:19-32` — types 0x00–0x0B and
0x0F. Types 0x0C–0x0E are not defined.

Follows up on #965.

---------

Co-authored-by: you <you@example.com>
2026-05-02 11:17:34 -07:00
Kpa-clawbotandKpa-clawbot c67f3347ce fix(ui): add GRP_DATA (type 6) to filter dropdown + color tables (#965)
## Bug

Packet type 6 (`PAYLOAD_TYPE_GRP_DATA` per `firmware/src/Packet.h:25`)
was missing from three frontend lookup tables:
- `public/app.js:7` — `PAYLOAD_COLORS` had no entry for 6 → badge color
fell back to `unknown` (grey)
- `public/packets.js:29` — `TYPE_NAMES` (used by the Packets page
type-filter dropdown) had no entry for 6 → "Group Data" missing from the
menu
- `public/roles.js:17,24` — `TYPE_COLORS` and `TYPE_BADGE_MAP` had no
`GRP_DATA` entry → no dedicated CSS class

The packet detail page already handled it (via `PAYLOAD_TYPES` in
`app.js:6` which had `6: 'Group Data'`) so individual GRP_DATA packets
render correctly. The gap was only in the filter UI + badge styling.

## Fix

Add the missing entry in each table. 4 lines across 3 files.

- `app.js`: add `6: 'grp-data'` to `PAYLOAD_COLORS`
- `packets.js`: add `6:'Group Data'` to `TYPE_NAMES`
- `roles.js`: add `GRP_DATA: '#8b5cf6'` to `TYPE_COLORS` and `GRP_DATA:
'grp-data'` to `TYPE_BADGE_MAP`

Color choice `#8b5cf6` (violet) — distinct from GRP_TXT's blue but
visually adjacent so operators read them as related types.

## Verification (rule 18 + 19)

Built server locally, served the JS files, grepped the rendered output:

```
$ curl -s http://localhost:13900/packets.js | grep TYPE_NAMES
const TYPE_NAMES = { ... 5:'Channel Msg', 6:'Group Data', 7:'Anon Req' ... };

$ curl -s http://localhost:13900/app.js | grep PAYLOAD_TYPES
const PAYLOAD_TYPES = { ... 5: 'Channel Msg', 6: 'Group Data', 7: 'Anon Req' ... };

$ curl -s http://localhost:13900/roles.js | grep GRP_DATA
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', GRP_DATA: '#8b5cf6', ...
ADVERT: 'advert', GRP_TXT: 'grp-txt', GRP_DATA: 'grp-data', ...
```

Frontend tests pass: `test-packets.js` 82/82, `test-hash-color.js`
32/32.

## Out of scope

Consolidating the duplicated PAYLOAD_TYPES / TYPE_NAMES tables into a
single source of truth is a separate cleanup. Two parallel name maps
continues to be a footgun (this is the second time a new type's been
added to one but not the other).

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-05-02 09:55:09 -07:00
Kpa-clawbotandKpa-clawbot 04c8558768 fix(spa): data-loaded setAttribute in finally so it fires on errors (#959)
## Bug

PR #958 added `data-loaded="true"` attributes for E2E sync, but placed
the `setAttribute` call inside the `try` block of `loadNodes()` /
`loadPackets()` / `loadNodes()` (map). When the API call failed (e.g.
`/api/observers` returns 500, or any other exception), the `catch`
swallowed the error and `setAttribute` was never reached. E2E tests then
waited 15s for `[data-loaded="true"]` and timed out.

This blocked PR #954 CI repeatedly with `Map page loads with markers:
page.waitForSelector: Timeout 15000ms exceeded`.

## Fix

Move `setAttribute('data-loaded', 'true')` to a `finally` block in all
three handlers (`map.js`, `nodes.js`, `packets.js`). The attribute now
fires on both success and error paths, so E2E tests proceed (test still
asserts on the actual rendered state — markers, rows, etc — so an empty
page still fails the right assertion, just much faster).

Removed the duplicate setAttribute calls inside the try blocks (the
finally is the single source of truth now).

## Verification

- `node test-packets.js` 82/82 ✅
- `node test-hash-color.js` 32/32 ✅
- Code reading: each `finally` runs after either success or catch, sets
the same attribute on the same container element.

## Why CI didn't catch this on #958

The PR #958 tests passed because the staging fixture happened to load
successfully when those tests ran. The flake only manifests when an
upstream fetch fails (e.g. observer API returning unexpected shape,
network blip, server still warming).

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-05-01 10:49:21 -07:00
Kpa-clawbotandyou 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
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
Kpa-clawbotandKpa-clawbot f84142b1d2 fix(packets): hash filter must bypass saved region filter (#939)
## Summary

Direct packet links like `/#/packets?hash=<HASH>` silently returned zero
rows when the user's saved region filter excluded the packet's observer
region. The packet existed and rendered in the side panel (which fetches
without region filter), but the main packet table was empty — leaving
the user with no rows to click and no obvious diagnostic.

## Root cause

`loadPackets()` in `public/packets.js` always added the `region` query
param to `/api/packets`, even when `filters.hash` was set. The
time-window filter is already correctly suppressed when `filters.hash`
is present (see line 619: `if (windowMin > 0 && !filters.hash)`); the
region filter should follow the same rule. A specific hash is an exact
identifier — the user wants THAT packet regardless of where their saved
region selection points.

## Change

Extracted the param-building logic into a pure helper
`buildPacketsParams(...)` so it's testable, then suppressed the `region`
param when `filters.hash` is set.

## Tests

Added 7 unit tests in `test-packets.js` covering:

- hash filter suppresses region (the bug)
- hash filter suppresses region with default windowMin=0
- region applies normally when no hash filter
- empty regionParam doesn't produce spurious `region=` param
- node/observer/channel filters still pass through alongside a hash
- groupByHash=true / false flag handling

Anti-tautology gate verified: reverting the one-line fix (`!filters.hash
&&` → removed) causes 3 of the 7 new tests to fail. The fix is the
smallest change that makes them pass.

`node test-packets.js`: 80 passed, 0 failed.

## Reproduction

1. Set region filter to e.g. `SJC`
2. Open `/#/packets?hash=<HASH_FROM_ANOTHER_REGION>`
3. Before fix: empty table, no diagnostic
4. After fix: packet renders

---------

Co-authored-by: Kpa-clawbot <bot@example.invalid>
2026-04-30 19:51:53 -07:00
Kpa-clawbotandyou 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-clawbotandyou 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