Commit Graph

1743 Commits

Author SHA1 Message Date
Kpa-clawbot 2ee09142d1 ci: update frontend-coverage.json [skip ci] 2026-05-05 10:35:53 +00:00
Kpa-clawbot 043b6dfd58 ci: update e2e-tests.json [skip ci] 2026-05-05 10:35:52 +00:00
Kpa-clawbot ac0cf5ac7d fix(channels): #1087 QR library + share modal + PSK persistence (#1090)
Red commit: 5def4d073c (CI run pending —
see Checks tab)

Fixes #1087

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

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

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

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

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

---------

Co-authored-by: bot <bot@corescope>
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 03:24:52 -07:00
Kpa-clawbot 3935b0028f ci: update go-server-coverage.json [skip ci] 2026-05-05 09:54:05 +00:00
Kpa-clawbot aad00ff70b ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 09:54:04 +00:00
Kpa-clawbot fc19fcd0d8 ci: update frontend-tests.json [skip ci] 2026-05-05 09:54:03 +00:00
Kpa-clawbot 4288d48d28 ci: update frontend-coverage.json [skip ci] 2026-05-05 09:54:01 +00:00
Kpa-clawbot fc558bfbfa ci: update e2e-tests.json [skip ci] 2026-05-05 09:54:00 +00:00
Kpa-clawbot 36ee71d17e feat(#1085): fold Roles page into Analytics tab (#1088)
Red commit: 35e1f46b36 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25367951904)

Fixes #1085

## What changed

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

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

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

## Acceptance criteria (from #1085)

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

## Tests

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

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

## TDD trail

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

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 02:43:41 -07:00
Kpa-clawbot 5fa3b56ccb fix(#662): GetRepeaterRelayInfo also looks up byPathHop by 1-byte prefix (#1086)
## Summary

Partial fix for #662.

`GetRepeaterRelayInfo` was reporting "never observed as relay hop" /
`RelayCount24h=0` for nodes that clearly DO have packets passing through
them — visible on the same node detail page in the "Paths seen through
node" view.

## Root cause

The `byPathHop` index is keyed by **both**:
- full resolved pubkey (populated when neighbor-affinity resolution
succeeds), and
- raw 1-byte hop prefix from the wire (e.g. `"a3"`)

`GetRepeaterRelayInfo` only looked up the full-pubkey key. Many ingested
non-advert packets only carry the raw 1-byte hop — so any repeater whose
path appearances are all raw-hop entries returned 0, even though the
path-listing endpoint (which prefix-matches) renders them.

Example node: an `a3…` repeater on staging has ~dozens of paths through
it in the UI but the relay-info function returns 0.

## Fix

Look up under both keys (full pubkey + 1-byte prefix) and de-dup by tx
ID before counting.

## Trade-off

The 1-byte prefix CAN over-count when multiple nodes share a first byte.
This trades a possible over-count for clearly false zeros. The richer
disambiguation done by the path-listing endpoint (resolved-path SQL
post-filter via `confirmResolvedPathContains`) is out of scope for this
partial fix — adding it here would mean disk I/O inside what is
currently a pure in-memory lookup. Worth a follow-up if over-counting
shows up in practice.

## TDD

- Red commit (`test: failing test for relay-info prefix-hop mismatch`):
adds `TestRepeaterRelayActivity_PrefixHop` that builds a non-advert
packet with `PathJSON: ["a3"]`, indexes it via `addTxToPathHopIndex`,
then asserts `RelayCount24h>=1` for the full pubkey starting with `a3…`.
Fails on the assertion (got 0), not a build error.
- Green commit (`fix: GetRepeaterRelayInfo also looks up byPathHop by
1-byte prefix`): the lookup change. All five
`TestRepeaterRelayActivity_*` tests pass.

## Scope

This is a **partial** fix — addresses the read-side prefix mismatch
only. Issue #662 is a 4-axis epic (also covers ingest indexing
consistency, UI surfacing, and schema). Leaving #662 open.

---------

Co-authored-by: corescope-bot <bot@corescope>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 02:33:27 -07:00
Kpa-clawbot 55302a362e ci: update go-server-coverage.json [skip ci] 2026-05-05 09:12:37 +00:00
Kpa-clawbot f5c9680e78 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 09:12:36 +00:00
Kpa-clawbot b4080c25d3 ci: update frontend-tests.json [skip ci] 2026-05-05 09:12:35 +00:00
Kpa-clawbot 73c1253cea ci: update frontend-coverage.json [skip ci] 2026-05-05 09:12:34 +00:00
Kpa-clawbot 39b545f6b7 ci: update e2e-tests.json [skip ci] 2026-05-05 09:12:33 +00:00
Kpa-clawbot 282074b19d feat(#1034): wire QR generate + scan into channel modal (PR 3/3) (#1081)
## Summary

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

### Changes

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

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

### TDD

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

### E2E (rule 18)

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

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

### CI registration

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

### Closes

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

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
2026-05-05 01:59:17 -07:00
Kpa-clawbot 136e1d23c8 feat(#730): foreign-advert detection — flag instead of silent drop (#1084)
## Summary

**Partial fix for #730 (M1 only — M2 frontend and M3 alerting
deferred).**

Today the ingestor **silently drops** ADVERTs whose GPS lies outside the
configured `geo_filter` polygon. That's the wrong default for an
analytics tool — operators get zero visibility into bridged or leaked
meshes.

This PR makes the new default **flag, don't drop**: foreign adverts are
stored, the node row is tagged `foreign_advert=1`, and the API surfaces
`"foreign": true` so dashboards / map overlays can be built on top.

## Behavior

| Mode | What happens to an ADVERT outside `geo_filter` |
|---|---|
| (default) flag | Stored, marked `foreign_advert=1`, exposed via API |
| drop (legacy) | Silently dropped (preserves old behavior for ops who
want it) |

## What's done (M1 — Backend)
- ingestor stores foreign adverts instead of dropping
- `nodes.foreign_advert` column added (migration)
- `/api/nodes` and `/api/nodes/{pk}` expose `foreign: true` field
- Config: `geofilter.action: "flag"|"drop"` (default `flag`)
- Tests + config docs

## What's NOT done (deferred to M2 + M3)

- **M2 — Frontend:** Map overlay showing foreign adverts as distinct
markers, foreign-advert filter on packets/nodes pages, dedicated
foreign-advert dashboard
- **M3 — Alerting:** Time-series detection of bridging events, alert
when foreign advert rate spikes, identify bridge entry-point nodes

Issue #730 remains open for M2 and M3.

---------

Co-authored-by: corescope-bot <bot@corescope>
2026-05-05 01:58:52 -07:00
Kpa-clawbot f7d8a7cb8f feat(packets): filter UX — in-UI docs + autocomplete + right-click + saved filters (#966) (#1083)
## Summary

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

Closes #966.

## Surfaces

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

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

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

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

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

## Implementation

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

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

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

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

## Tests

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

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

## Browser validation

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

## Out of scope (deferred per the issue)

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

## Acceptance criteria

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

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:50:12 -07:00
Kpa-clawbot e9c801b41a feat(live): filter incoming packets by IATA region (#1045) (#1080)
Closes #1045.

## What
Adds an optional region dropdown to the **Live** page that filters
incoming packets by observer IATA. When a user selects one or more
regions, only packets observed by repeaters in those regions render in
the feed/animation/audio.

## How
- New `liveRegionFilter` container in the live header toggles row,
initialised via the shared `RegionFilter` component in `dropdown` mode
(matches packets/nodes/observers pages).
- On page init, fetches `/api/observers` once and builds an `observer_id
→ IATA` map.
- `packetMatchesRegion(packets, obsMap, selected)` (pure helper, OR
across observations, case-insensitive) gates `renderPacketTree` next to
the existing favorite + node filters.
- Selection persists in localStorage via the existing `RegionFilter`
machinery — no per-page key needed.
- Listener cleanup hooked into the existing live-page teardown.

## TDD
- Red commit `55097ce`: `test-live-region-filter.js` asserts
`_livePacketMatchesRegion` exists and behaves correctly across 9 cases
(no-selection passthrough, single match, no-match, OR across
observations, multi-region selection, unknown observer, missing
observer_id, case-insensitivity, observer-map override). Fails with
`_livePacketMatchesRegion must be exposed` against master.
- Green commit `fdec7bf`: implements helper + UI wiring + CSS; test
passes.

Test wired into `.github/workflows/deploy.yml` JS unit-test step.

## Notes
- Server-side WS broadcast is unchanged — filtering is purely
client-side, as the issue requests ("something a user can activate
themselves, and not something that would be server wide").
- Pre-existing `test-live.js` / `test-live-dedup.js` failures on master
are not introduced or affected by this PR (verified by running both on
master HEAD).

---------

Co-authored-by: meshcore-bot <bot@openclaw.local>
2026-05-05 01:43:05 -07:00
Kpa-clawbot 3ab404b545 feat(node-battery): voltage trend chart + /api/nodes/{pubkey}/battery (#663) (#1082)
## Summary

Closes #663 (Phase 2 + 3 partial — time-series tracking + thresholds for
nodes that are also observers).

Adds a per-node battery voltage trend chart and
`/api/nodes/{pubkey}/battery` endpoint, sourced from the existing
`observer_metrics.battery_mv` samples populated by observer status
messages. No new ingest or schema changes — purely surfaces data we were
already collecting.

## Scope (TDD red→green)

**RED commit:** test(node-battery) — DB query, endpoint shape
(200/404/no-data), and config getters all asserted.
**GREEN commit:** feat(node-battery) — implementation only.

## Changes

### Backend
- `cmd/server/node_battery.go` (new):
- `DB.GetNodeBatteryHistory(pubkey, since)` — pulls `(timestamp,
battery_mv)` rows from `observer_metrics WHERE LOWER(observer_id) =
LOWER(public_key) AND battery_mv IS NOT NULL`. Case-insensitive join
tolerates historical pubkey casing variation (observers persist
uppercase, nodes lowercase in this DB).
- `Server.handleNodeBattery` — `GET /api/nodes/{pubkey}/battery?days=N`
(default 7, max 365). Returns `{public_key, days, samples[], latest_mv,
latest_ts, status, thresholds}`.
- `Config.LowBatteryMv()` / `CriticalBatteryMv()` — defaults 3300 / 3000
mV.
- `cmd/server/config.go` — `BatteryThresholds *BatteryThresholdsConfig`
field.
- `cmd/server/routes.go` — route registration alongside existing
`/health`, `/analytics`.

### Frontend
- `public/node-analytics.js` — new "Battery Voltage" chart card with
status badge (🔋 OK / ⚠️ Low / 🪫 Critical / No data). Renders dashed
threshold lines at `lowMv` and `criticalMv`. Empty-state message when no
samples in window.

### Config
- `config.example.json` — `batteryThresholds: { lowMv: 3300, criticalMv:
3000 }` with `_comment` per Config Documentation Rule.

## Status semantics

| latest_mv             | status     |
|-----------------------|------------|
| no samples in window  | `unknown`  |
| `>= lowMv`            | `ok`       |
| `< lowMv`, `>= critMv`| `low`      |
| `< criticalMv`        | `critical` |

## What this PR does NOT do (deferred)

The issue's full Phase 1 (writing decoded sensor advert telemetry into
`nodes.battery_mv` / `temperature_c` from server-side decoder) and Phase
4 (firmware/active polling for repeaters without observers) are out of
scope here. This PR delivers the requested Phase 2/3 surfacing for the
data path that already lands rows: `observer_metrics`. Repeaters that
are also observers (i.e. publish status to MQTT) will get a voltage
trend immediately; pure passive nodes won't until Phase 1 lands.

## Tests

- `TestGetNodeBatteryHistory_FromObserverMetrics` — case-insensitive
join, NULL skipping, ordering.
- `TestNodeBatteryEndpoint` — full happy path with thresholds + status.
- `TestNodeBatteryEndpoint_NoData` — 200 + status=unknown.
- `TestNodeBatteryEndpoint_404` — unknown node.
- `TestBatteryThresholds_ConfigOverride` — config getters + defaults.

`cd cmd/server && go test ./...` — green.

## Performance

Endpoint is per-pubkey (called once on analytics page open), indexed by
`(observer_id, timestamp)` PK on `observer_metrics`. No hot-path impact.

---------

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

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

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

## Changes

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

## TDD

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

Fixes #1046

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-05 01:36:08 -07:00
Kpa-clawbot 5f6c5af0cf fix(observers): correct column headings after Last Packet (#1039) (#1075)
## Summary

Fixes #1039 — the Observers page table had 10 `<td>` cells per row but
only 9 `<th>` headings, so labels drifted starting at the Packet Health
badge cell. The headings `Packets`, `Packets/Hour`, `Clock Offset`,
`Uptime` were each one column to the left of their data.

## Changes

- `public/observers.js`: added missing `Packet Health` heading (over the
`packetBadge()` cell) and renamed the count column header from `Packets`
to `Total Packets` to disambiguate from `Packets/Hour`.

## TDD

- **Red commit** (`7cae61c`): `test-observers-headings.js` asserts
`<th>` count equals `<td>` count and verifies the expected header order.
Both assertions fail on master (9 vs 10; `Packets` vs `Packet
Health`/`Total Packets`).
- **Green commit** (`8ed7f7c`): heading row updated; both assertions
pass.

## Test

```
$ node test-observers-headings.js
── Observers table headings (#1039) ──
  ✓ thead column count equals tbody row column count
  ✓ expected headings present and ordered
2 passed, 0 failed
```

Wired into `test-all.sh`.

## Risk

Frontend-only, static template change. No data flow / perf impact.

Fixes #1039

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-05 01:35:09 -07:00
Kpa-clawbot f33801ecb4 feat(repeater): usefulness score — traffic axis (#672) (#1079)
## Summary

Implements the **Traffic axis** of the repeater usefulness score (#672).
Does NOT close #672 — Bridge, Coverage, and Redundancy axes are deferred
to follow-up PRs.

Adds `usefulness_score` (0..1) to repeater/room node API responses
representing what fraction of non-advert traffic passes through this
repeater as a relay hop.

## Why traffic-axis-first

The issue proposes a 4-axis composite (Bridge, Coverage, Traffic,
Redundancy). Bridge/Coverage/Redundancy require betweenness centrality
and neighbor graph infrastructure (#773 Neighbor Graph V2). Traffic axis
can ship independently using existing path-hop data.

## Remaining work for #672

- Bridge axis (betweenness centrality — depends on #773)
- Coverage axis (observer reach comparison)
- Redundancy axis (node-removal simulation — depends on #687)
- Composite score combining all 4 axes

Partial fix for #672.

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:34:08 -07:00
Kpa-clawbot d05e468598 feat(memlimit): GOMEMLIMIT support, derive from packetStore.maxMemoryMB (#836) (#1077)
## Summary

Implements **part 1** of #836 — `GOMEMLIMIT` support so the Go runtime
self-throttles GC under cgroup memory pressure instead of getting
SIGKILLed.

(Parts 2 & 3 — bounded cold-load batching + README ops docs — land in
follow-up PRs.)

## Behavior

On startup `cmd/server/main.go` now calls `applyMemoryLimit(maxMemoryMB,
envSet)`:

| Condition | Action | Log |
|---|---|---|
| `GOMEMLIMIT` env set | Honor the runtime's parse, do nothing |
`[memlimit] using GOMEMLIMIT from environment (...)` |
| env unset, `packetStore.maxMemoryMB > 0` | `debug.SetMemoryLimit(maxMB
* 1.5 MiB)` | `[memlimit] derived from packetStore.maxMemoryMB=512 → 768
MiB (1.5x headroom)` |
| env unset, `maxMemoryMB == 0` | No-op | `[memlimit] no soft memory
limit set ... recommend setting one to avoid container OOM-kill` |

The 1.5x headroom covers Go's NextGC trigger at ~2× live heap (per #836
heap profile: 680 MB live → 1.38 GB NextGC).

## Tests (TDD red→green visible in commit history)

- `TestApplyMemoryLimit_FromEnv` — env wins, function does not override
- `TestApplyMemoryLimit_DerivedFromMaxMemoryMB` — verifies bytes
computation + `debug.SetMemoryLimit` actually applied at runtime
- `TestApplyMemoryLimit_None` — no env, no config → reports `"none"`, no
side effect

Red commit: `7de3c62` (assertion failures, builds clean)
Green commit: `454516d`

## Config docs

`config.example.json` `packetStore._comment_gomemlimit` documents
env/derived/override behavior.

## Out of scope

- Cold-load transient bounding (item 2 in #836)
- README container-size table (item 3)
- QA §1.1 rewrite

Closes part 1 of #836.

---------

Co-authored-by: corescope-bot <bot@corescope>
2026-05-05 01:33:23 -07:00
Kpa-clawbot d192330bdc feat(compare): asymmetric overlap stats for reference observer comparison (#671) (#1076)
## Summary

Adds asymmetric overlap percentages to the existing observer compare
page so it can be used as a **reference observer comparison** tool
(Uncle Lit's request, #671).

## What changed

`public/compare.js` (frontend only — no backend changes)

- New `computeOverlapStats(cmp)` helper that turns a
`comparePacketSets()` result into two-way coverage:
  - `aSeesOfB` — % of B's packets that A also saw
  - `bSeesOfA` — % of A's packets that B also saw
  - plus shared / onlyA / onlyB / totalA / totalB
- Two callout cards on the compare summary view:
  - `<A> saw N of <B>'s X packets` (Y%)
  - `<B> saw N of <A>'s X packets` (Y%)
- Existing "Only A / Only B / Both" tabs already identify unique
packets; that's the second half of the issue and is left intact.

## Operator workflow

Pick a known-good observer (LOS to key nodes) as the reference. Pair it
with a candidate. If the candidate's overlap with the reference is high
→ healthy. If low → investigate antenna, obstruction, or RF deafness.

## Out of scope (future work)

Issue lists several follow-on milestones — full Analytics sub-tab with
reference-vs-many table, SNR delta, geographic proximity filter,
server-side `/api/analytics/observer-comparison` endpoint. Those are
larger and tracked by the issue's M1-M4 milestones; this PR closes the
core ask (asymmetric overlap on the existing compare page) and leaves
the rest for follow-ups.

## Tests

`test-compare-overlap.js` — 6 unit tests via vm sandbox:

- exposes `computeOverlapStats` on `window`
- basic asymmetric scenario (8/10 vs 8/12)
- zero packets — no division by zero
- one observer empty — both percentages 0
- perfect overlap — 100% both ways
- disjoint observers — 0% both ways

TDD: red commit landed first with stub returning zeros (assertions
failed), green commit added the math.

Closes #671

---------

Co-authored-by: bot <bot@corescope.local>
2026-05-05 01:33:04 -07:00
Kpa-clawbot 2b01ecd051 ci: update go-server-coverage.json [skip ci] 2026-05-05 08:28:49 +00:00
Kpa-clawbot 34b418894a ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 08:28:48 +00:00
Kpa-clawbot 1860cb4c54 ci: update frontend-tests.json [skip ci] 2026-05-05 08:28:47 +00:00
Kpa-clawbot 6a715e6af7 ci: update frontend-coverage.json [skip ci] 2026-05-05 08:28:46 +00:00
Kpa-clawbot fc16b4e069 ci: update e2e-tests.json [skip ci] 2026-05-05 08:28:45 +00:00
Kpa-clawbot 45f30fcadc feat(repeater): liveness detection — distinguish actively relaying from advert-only (#662) (#1073)
## Summary

Implements repeater liveness detection per #662 — distinguishes a
repeater that is **actively relaying traffic** from one that is **alive
but idle** (only sending its own adverts).

## Approach

The backend already maintains a `byPathHop` index keyed by lowercase
hop/pubkey for every transmission. Decode-window writes also key it by
**resolved pubkey** for relay hops. We just weren't surfacing it.

`GetRepeaterRelayInfo(pubkey, windowHours)`:
- Reads `byPathHop[pubkey]`.
- Skips packets whose `payload_type == 4` (advert) — a self-advert
proves liveness, not relaying.
- Returns the most recent `FirstSeen` as `lastRelayed`, plus
`relayActive` (within window) and the `windowHours` actually used.

## Three states (per issue)

| State | Indicator | Condition |
|---|---|---|
| 🟢 Relaying | green | `last_relayed` within `relayActiveHours` |
| 🟡 Alive (idle) | yellow | repeater is in the DB but
`relay_active=false` (no recent path-hop appearance, or none ever) |
|  Stale | existing | falls out of the existing `getNodeStatus` logic |

## API

- `GET /api/nodes` — repeater/room rows now include `last_relayed`
(omitted if never observed) and `relay_active`.
- `GET /api/nodes/{pubkey}` — same fields plus `relay_window_hours`.

## Config

New optional field under `healthThresholds`:

```json
"healthThresholds": {
  ...,
  "relayActiveHours": 24
}
```

Default 24h. Documented in `config.example.json`.

## Frontend

Node detail page gains a **Last Relayed** row for repeaters/rooms with
the 🟢/🟡 state badge. Tooltip explains the distinction from "Last Heard".

## TDD

- **Red commit** `4445f91`: `repeater_liveness_test.go` + stub
`GetRepeaterRelayInfo` returning zero. Active and Stale tests fail on
assertion (LastRelayed empty / mismatched). Idle and IgnoresAdverts
already match the desired behavior under the stub. Compiles, runs, fails
on assertions — not on imports.
- **Green commit** `5fcfb57`: Implementation. All four tests pass. Full
`cmd/server` suite green (~22s).

## Performance

`O(N)` over `byPathHop[pubkey]` per call. The index is bounded by store
eviction; a single repeater has at most a few hundred entries on real
data. The `/api/nodes` loop adds one map read + scan per repeater row —
negligible against the existing enrichment work.

## Limitations (per issue body)

1. Observer coverage gaps — if no observer hears a repeater's relay,
it'll show as idle even when actively relaying. This is inherent to
passive observation.
2. Low-traffic networks — a repeater in a quiet area legitimately shows
idle. The 🟡 indicator copy makes that explicit ("alive (idle)").
3. Hash collisions are mitigated by the existing `resolveWithContext`
path before pubkeys land in `byPathHop`.

Fixes #662

---------

Co-authored-by: clawbot <bot@corescope.local>
2026-05-05 01:17:52 -07:00
Kpa-clawbot 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-clawbot 83881e6b71 fix(#688): auto-discover hashtag channels from message text (#1071)
## Summary

Auto-discovers previously-unknown hashtag channels by scanning decoded
channel message text for `#name` mentions and surfacing them via
`GetChannels`.

Workflow (per the issue):
1. New channel message arrives on a known channel
2. Decoded text is scanned for `#hashtag` mentions
3. Any mention that doesn't match an existing channel is surfaced as a
discovered channel (`discovered: true`, `messageCount: 0`)
4. Future traffic on that channel will populate the entry once it has
its own packets

## Changes

- `cmd/server/discovered_channels.go` — new file.
`extractHashtagsFromText` parses `#name` mentions from free text,
deduped, order-preserving. Trailing punctuation is excluded by the
character class.
- `cmd/server/store.go` — `GetChannels` now scans CHAN packet text for
hashtags after building the primary channel map, and appends any unseen
hashtag mentions as discovered entries.
- `cmd/server/discovered_channels_test.go` — new tests covering parser
edge cases (single, multi, dedup, punctuation, none, bare `#`) and
end-to-end discovery via `GetChannels`.

## TDD

- Red: `34f1817` — stub returns `nil`, both new tests fail on assertion
(verified).
- Green: `d27b3ed` — real implementation, full `cmd/server` test suite
passes (21.7s).

## Notes

- Discovered channels carry `messageCount: 0` and `lastActivity` set to
the most recent mention's `firstSeen`, so they sort naturally alongside
real channels.
- Names are matched against existing entries by both `#name` and bare
`name` so a channel that already has decoded traffic isn't
double-listed.
- The existing `channelsCache` (15s) covers the new code path; no
separate invalidation needed since the source data (`byPayloadType[5]`)
drives both maps.

Fixes #688

---------

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

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

## What changed

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

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

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

## TDD

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

## Acceptance criteria

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

## Notes for reviewers

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

Fixes #1054

---------

Co-authored-by: clawbot <bot@corescope.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:14:39 -07:00
Kpa-clawbot 78dabd5bda feat(filter): timestamp predicates (after/before/between/age) — #289 (#1070)
Fixes #289.

Adds Wireshark-style timestamp predicates to the client-side packet
filter
engine (`public/packet-filter.js`).

## New syntax

| Form | Meaning |
| --- | --- |
| `time after "2024-01-01"` | packets with timestamp strictly after the
given datetime |
| `time before "2024-12-31T23:59:59Z"` | packets strictly before |
| `time between "2024-01-01" "2024-02-01"` | inclusive range
(order-insensitive) |
| `age < 1h` | packets newer than 1 hour |
| `age > 24h` | packets older than 24 hours |
| `age < 7d && type == ADVERT` | composes with existing predicates |

Duration units: `s` / `m` / `h` / `d` / `w`. Datetime values use
`Date.parse`
(ISO 8601 + bare `YYYY-MM-DD`). `time` is also accepted as `timestamp`.

## Implementation

- `OP_WORDS` extended with `after`, `before`, `between`.
- New `TK.DURATION` token: lexer recognises `<number><unit>` and
pre-converts
  to seconds at lex time (no per-evaluation parsing cost).
- `between` is a two-value op handled in `parseComparison`.
- Field resolver:
- `time` / `timestamp` → epoch-ms; falls back to `first_seen` then
`latest`
    so grouped rows from `/api/packets?groupByHash=true` work.
  - `age` → seconds since `Date.now()`.
- Parse-time validation rejects invalid datetimes and unknown duration
units
(silent-fail would have been a footgun — every packet would just
disappear).
- Null/missing timestamps → predicate returns `false`, consistent with
the
  existing null-field behaviour for `snr` / `rssi`.

## Open questions from the issue

- **UTC vs local**: defaults to whatever `Date.parse` returns. Bare
dates like
`"2024-01-01"` are interpreted as UTC midnight by the spec. Tying this
to
  the #286 timestamp display setting can be a follow-up.
- **URL query string**: out of scope for this PR.

## Tests

- New `test-packet-filter-time.js`: 20 tests covering
`after`/`before`/`between`,
ISO datetimes, all duration units, composition with `&&`, null-timestamp
safety,
  invalid-datetime / invalid-unit errors, and `first_seen` fallback.
- Wired into `.github/workflows/deploy.yml` JS unit-test step.
- Existing `test-packet-filter.js` (69 tests) and inline self-tests
still pass.

## Commits

- Red: `5ccfad3` — failing tests + lexer-only stub (compiles, asserts
fail)
- Green: `976d50f` — implementation

---------

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
2026-05-05 01:13:48 -07:00
Kpa-clawbot e2050f8ec8 fix(docker): default BUILDPLATFORM so plain docker build works (fixes #884) (#1069)
## Summary

Plain `docker build .` (no buildx) fails immediately:

```
Step 1/45 : FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
failed to parse platform : "" is an invalid component of "": platform specifier
component must match "^[A-Za-z0-9_-]+$"
```

`$BUILDPLATFORM` is only auto-populated by buildx; under plain
BuildKit/`docker build` it's empty.

## Fix

Add `ARG BUILDPLATFORM=linux/amd64` before the `FROM` so the variable
always resolves.

## Multi-arch preserved

`docker buildx build --platform=linux/arm64,linux/amd64 .` still
overrides `BUILDPLATFORM` at invocation time — the ARG default only
applies when the caller doesn't set one. The existing CI multi-arch
workflow is unaffected.

Fixes #884

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 01:12:40 -07:00
Kpa-clawbot cbfd159f8e feat(ws): pull-to-reconnect on touch devices (Fixes #1063) (#1068)
## Summary

Reframes the browser's native pull-to-refresh on touch devices as a
**WebSocket reconnect** instead of a full page reload. On data pages
(Packets, Nodes, Channels — and globally, since the WS is shared) a
downward pull at `scrollTop=0` cycles the WS, which is what users
actually want when they reach for that gesture.

Fixes #1063.

## Behavior

- **Touch-only**: gated by `('ontouchstart' in window) ||
navigator.maxTouchPoints > 0`. Desktop is untouched.
- **Scroll-safe**: every handler re-checks `scrollTop > 0` and bails out
— never hijacks normal scroll.
- **Visual affordance**: a fixed chip slides down from the top with a
rotating ⟳ icon; opacity and rotation scale with pull progress (0 →
`PULL_THRESHOLD_PX = 80px`).
- **`preventDefault` is conservative**: only after `dy > 16px` and only
on `touchmove`, so taps and short swipes are not affected.
- **Result feedback**: a brief toast — green `Connected ✓` if WS was
already OPEN, `Reconnecting…` otherwise. Both auto-dismiss after ~1.8s.
- **Reconnect path**: closes the existing WS so the existing `onclose`
auto-reconnect fires immediately; an explicit `connectWS()` is also
called as a safety net when `ws` is null.
- **No regression** to existing WS auto-reconnect — same `connectWS` /
`setTimeout(connectWS, 3000)` chain, just kicked manually.

## TDD

- **Red commit** `f90f5e9` — adds `test-pull-to-reconnect.js` with 6
assertions; stub functions added to `app.js` so tests reach assertion
failures (not ReferenceError). 3/6 fail on behavior.
- **Green commit** `53adbd9` — full implementation; 6/6 pass.

## Files

- `public/app.js` — `pullReconnect()`, `setupPullToReconnect()`,
`_ensurePullIndicator()`, `_showPullToast()`, `_isTouchDevice()`. Wired
into `DOMContentLoaded` next to `connectWS()`. Touched the WS section
only.
- `test-pull-to-reconnect.js` — vm sandbox suite covering exposure,
WS-close, listener wiring, threshold trigger, scroll-position gate.

## Acceptance criteria check

-  Pull-down at scroll-top triggers WS reconnect + data refetch
(debounced cache invalidate fires on next WS message)
-  Visible affordance during pull (rotating chip)
-  Resolves on success (toast), shows status toast on disconnect path
-  Disabled when not at `scrollTop=0`
-  No regression to existing WS auto-reconnect

---------

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

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

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

## Acceptance criteria

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

## TDD

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

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

Fixes #1060

---------

Co-authored-by: bot <bot@corescope>
2026-05-05 01:11:08 -07:00
Kpa-clawbot b71c290783 ci: update go-server-coverage.json [skip ci] 2026-05-05 07:24:14 +00:00
Kpa-clawbot d7fbd4755e ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 07:24:13 +00:00
Kpa-clawbot 13b6eecc82 ci: update frontend-tests.json [skip ci] 2026-05-05 07:24:12 +00:00
Kpa-clawbot b18ebe1a26 ci: update frontend-coverage.json [skip ci] 2026-05-05 07:24:11 +00:00
Kpa-clawbot 9aa94166df ci: update e2e-tests.json [skip ci] 2026-05-05 07:24:09 +00:00
Kpa-clawbot 38703c75e6 fix(e2e): make Nodes WS auto-update test deterministic (#1051)
## Problem

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

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

## Root cause

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

## Fix

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

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

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

## Coverage

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

## TDD note

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

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

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

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

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

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

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

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

---------

Co-authored-by: bot <bot@corescope.local>
Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 00:05:09 -07:00
Kpa-clawbot 26a914274f ci: update go-server-coverage.json [skip ci] 2026-05-05 06:52:56 +00:00
Kpa-clawbot e4f358f562 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 06:52:55 +00:00
Kpa-clawbot 5ff9d4f31d ci: update frontend-tests.json [skip ci] 2026-05-05 06:52:54 +00:00
Kpa-clawbot db75dbee44 ci: update frontend-coverage.json [skip ci] 2026-05-05 06:52:52 +00:00
Kpa-clawbot 16e1ff9e6c ci: update e2e-tests.json [skip ci] 2026-05-05 06:52:51 +00:00