Commit Graph

1689 Commits

Author SHA1 Message Date
Kpa-clawbot f2ee74c8f3 ci: update frontend-coverage.json [skip ci] 2026-05-05 06:29:24 +00:00
Kpa-clawbot f676c146ae ci: update e2e-tests.json [skip ci] 2026-05-05 06:29:23 +00:00
Kpa-clawbot 227f375b4a test(ingestor): regression test for observer metadata persistence (#1044) (#1047)
Adds end-to-end test proving that `extractObserverMeta` +
`UpsertObserver` correctly stores model, firmware, battery_mv,
noise_floor, uptime_secs from a real MQTT status payload.

Test passes — confirms the code path works. #1044 was caused by upstream
observers not including metadata fields in their status payloads (older
`meshcoretomqtt` client versions), not a code bug.

Closes #1044

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-05 06:18:47 +00:00
Kpa-clawbot 2e959145aa ci: update go-server-coverage.json [skip ci] 2026-05-05 04:06:12 +00:00
Kpa-clawbot 72dd377ba1 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 04:06:12 +00:00
Kpa-clawbot 8a536c5899 ci: update frontend-tests.json [skip ci] 2026-05-05 04:06:11 +00:00
Kpa-clawbot f3a7d0d435 ci: update frontend-coverage.json [skip ci] 2026-05-05 04:06:10 +00:00
Kpa-clawbot ccc7cf5a77 ci: update e2e-tests.json [skip ci] 2026-05-05 04:06:09 +00:00
Kpa-clawbot 67da696a42 fix(channels): hide raw psk:* in header, label share button, red delete button (#1041)
## Channel UX round 2 (follow-up to #1040)

Three UX issues reported after #1040 landed:

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

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

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

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

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

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

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

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

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-04 20:56:01 -07:00
Kpa-clawbot 5829d2328d ci: update go-server-coverage.json [skip ci] 2026-05-05 03:19:29 +00:00
Kpa-clawbot df60f324e9 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 03:19:28 +00:00
Kpa-clawbot 0aeb33f757 ci: update frontend-tests.json [skip ci] 2026-05-05 03:19:26 +00:00
Kpa-clawbot e334f8611e ci: update frontend-coverage.json [skip ci] 2026-05-05 03:19:25 +00:00
Kpa-clawbot 32ba77eaf8 ci: update e2e-tests.json [skip ci] 2026-05-05 03:19:24 +00:00
Kpa-clawbot 724a96f35b ci: update go-server-coverage.json [skip ci] 2026-05-05 03:12:40 +00:00
Kpa-clawbot 849bf1c335 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 03:12:39 +00:00
Kpa-clawbot a0b791254c ci: update frontend-tests.json [skip ci] 2026-05-05 03:12:38 +00:00
Kpa-clawbot 62a2a13251 ci: update frontend-coverage.json [skip ci] 2026-05-05 03:12:38 +00:00
Kpa-clawbot c94ba05c01 ci: update e2e-tests.json [skip ci] 2026-05-05 03:12:37 +00:00
Kpa-clawbot c00b585ee5 fix(channels): UX follow-ups to #1037 (touch target, '0 messages', share, locality, #meshcore) (#1040)
## Summary

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

## Fixes

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

## TDD

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

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

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

---------

Co-authored-by: clawbot <clawbot@local>
2026-05-04 19:57:53 -07:00
Kpa-clawbot e2bd9a8fa2 ci: update go-server-coverage.json [skip ci] 2026-05-05 02:07:56 +00:00
Kpa-clawbot 1f3c8130ef ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 02:07:55 +00:00
Kpa-clawbot e5606058c1 ci: update frontend-tests.json [skip ci] 2026-05-05 02:07:54 +00:00
Kpa-clawbot 47b4021346 ci: update frontend-coverage.json [skip ci] 2026-05-05 02:07:53 +00:00
Kpa-clawbot c93c008867 ci: update e2e-tests.json [skip ci] 2026-05-05 02:07:52 +00:00
Kpa-clawbot cea2c70d12 feat(#1034): channel UX redesign PR1 — Add Channel modal + sectioned sidebar (#1037)
## Summary

PR 1 of 3 for #1034 — channel UX redesign. Replaces the cramped inline
"type a name or 32-hex blob" form with a clear modal dialog, and
reorganizes the sidebar into three labeled sections.

**Scope of this PR:** Modal UI + sectioned sidebar. QR generation/scan
is deferred to PR #2 (placeholders are wired and ready).
`channel-decrypt.js` crypto is untouched.

## What changed

### New modal: `[+ Add Channel]`

Triggered by the new sidebar button. Three sections:

1. **Generate PSK Channel** — name + `[Generate & Show QR]` →
`crypto.getRandomValues(16)` → hex → `ChannelDecrypt.storeKey`. QR
rendering ships in PR #2; for now `#qr-output` surfaces the hex key as
text.
2. **Add Private Channel (PSK)** — 32-hex input (regex-validated),
optional display name, `[Add]`. `[📷 Scan QR]` placeholder is present but
`disabled` (PR #2 wires it).
3. **Monitor Hashtag Channel** — non-editable `#` prefix + free text +
case-sensitivity warning + `[Monitor]`. Reuses
`ChannelDecrypt.deriveKey`.

Privacy footer: _"🔒 Keys stay in your browser. CoreScope is a passive
observer..."_

Close ✕, backdrop click, and Escape all dismiss.

### Sectioned sidebar

`renderChannelList()` rewritten to render three sections:

- **My Channels** — `userAdded` channels. ✕ always visible. Last sender
+ relative time.
- **Network** — server-known cleartext channels.
- **Encrypted (N)** — collapsed by default (toggle persists in
`localStorage`). Shows hash byte + packet count.

The legacy "🔒 No key" checkbox and `#chShowEncrypted` toggle are removed
entirely. Encrypted channels are always fetched; the renderer groups
them.

## Tests

- **Unit** — `test-channel-modal-ux.js` (33 assertions): added to
`test-all.sh`. Covers sidebar button, modal markup, three sections, QR
placeholders, privacy footer, sectioned sidebar, modal handlers (incl.
`crypto.getRandomValues(16)`).
- **E2E** — `test-channel-modal-e2e.js` (Playwright, 14 steps). Covers
modal open/close, section rendering, invalid-hex error, valid-hex
storage, encrypted-section toggle. Run with:
  ```
CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:38201
node test-channel-modal-e2e.js
  ```
- `test-channel-psk-ux.js` — updated to reference `#chPskName` (was
`#chKeyLabelInput`).

### Red→green proof

- Red commit (`7ee421b`): test added with 31 expected assertion
failures, no source change.
- Green commit (`897be8f`): implementation lands, test passes 33/33.

## Browser-validated

Built `cmd/server/`, ran against `test-fixtures/e2e-fixture.db`,
exercised modal open → invalid hex → valid hex → key persisted → modal
closes → sectioned sidebar renders + Encrypted toggle expands. All 14
E2E steps pass.

## What's NOT in this PR

- QR code rendering (PR #2)
- Camera/QR scanning (PR #2)
- Migration of legacy localStorage format (PR #3, if needed — current
key format is unchanged)
- `channel-decrypt.js` changes (none — UI-only PR)

## Acceptance criteria from #1034

- [x] Modal opens on `[+ Add Channel]` click
- [x] Three sections clearly separated with labels
- [x] Add PSK: accepts 32-hex (QR scan = PR #2)
- [x] Monitor Hashtag: derives key, case-sensitivity warning shown
- [x] Privacy footer present
- [x] Sidebar: three sections (My Channels / Network / Encrypted)
- [x] ✕ button visible and functional on My Channels entries
- [x] "No key" checkbox removed
- [ ] Generate PSK QR display — text fallback only; QR is PR #2
- [ ] Old stored keys migrate seamlessly — no migration needed (storage
format unchanged)

Refs #1034

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
2026-05-04 18:40:46 -07:00
Kpa-clawbot 71f82d5d25 ci: update go-server-coverage.json [skip ci] 2026-05-05 01:39:54 +00:00
Kpa-clawbot 81430cf4c4 ci: update go-ingestor-coverage.json [skip ci] 2026-05-05 01:39:53 +00:00
Kpa-clawbot 1178bae18f ci: update frontend-tests.json [skip ci] 2026-05-05 01:39:52 +00:00
Kpa-clawbot 27c8514d70 ci: update frontend-coverage.json [skip ci] 2026-05-05 01:39:51 +00:00
Kpa-clawbot a24ec6e767 ci: update e2e-tests.json [skip ci] 2026-05-05 01:39:50 +00:00
Kpa-clawbot c1d0daf200 feat(#1034): channel QR generate + scan module (PR 2/3) (#1035)
## PR #2 of channel UX redesign (#1034) — QR generation + scanning

Self-contained QR module for MeshCore channel sharing. Wirable but **not
wired** — PR #3 wires this into the modal placeholders shipped by PR #1.

### What's in
- **`public/channel-qr.js`** — new module exporting `window.ChannelQR`:
- `buildUrl(name, secretHex)` →
`meshcore://channel/add?name=<urlencoded>&secret=<32hex>`
- `parseChannelUrl(url)` → `{name, secret}` or `null` (strict: scheme,
path, hex32 secret)
- `generate(name, secretHex, target)` — renders QR (via vendored
qrcode.js) + the URL string + a "Copy Key" button into `target`
- `scan()` → `Promise<{name, secret} | null>` — opens a camera overlay,
decodes with jsQR, parses, auto-closes on first valid match. Graceful
no-camera/permission-denied fallback ("Camera not available — paste key
manually").
- **`public/vendor/jsqr.min.js`** — vendored jsQR 1.4.0
- **`public/index.html`** — loads `vendor/jsqr.min.js` + `channel-qr.js`
after `channel-decrypt.js`
- **`test-channel-qr.js`** + wired into `test-all.sh` — 16 assertions on
`buildUrl` / `parseChannelUrl` (DOM/camera paths covered by Playwright
in #3)

### TDD
- Red commit `d6ba89e` — stub module + failing assertions on `buildUrl`
/ `parseChannelUrl` (compiles, runs, fails on assertion)
- Green commit `25328ac` — real impl, 16/16 pass

### License note
Brief specified jsQR as MIT — it's actually **Apache-2.0**
(https://github.com/cozmo/jsQR/blob/master/package.json). Apache-2.0 is
permissive and compatible with the repo's ISC license; flagging here so
reviewers can confirm. Cited in the file header.

### Independence guarantees
- Does **not** touch `channels.js` or `channel-decrypt.js`
- Does not call any UI from `channels.js`; PR #3 will call
`ChannelQR.generate(...)` into `#qr-output` and wire `#scan-qr-btn` to
`ChannelQR.scan()`

Refs #1034

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-04 18:29:48 -07:00
Kpa-clawbot d967170dd3 fix(channels): sidebar layout for user-added (PSK) rows — nested <button> bug (#1033)
## Problem

Channel sidebar layout broke for user-added (PSK) channels. Visible
symptoms in the screenshot:

- No ✕ (delete) button on user-added rows
- 🔑 emoji floating in the wrong position
- Message preview text (e.g. `KpaPocket: Тест`) orphaned **between**
channel entries instead of inside the row
- Spinner/loading dots misaligned

## Root cause

**HTML5 forbids nested `<button>` elements.** The `.ch-item` row is a
`<button>`, and #1024 added a `<button class="ch-remove-btn">` inside
it. The HTML parser implicitly closes the outer `.ch-item` the moment it
sees the inner `<button>`, then re-parents everything after it (✕ and
the `.ch-item-preview` line) outside the row.

Resulting DOM tree (parser-corrected, simplified):

```
<button class="ch-item">[icon] Levski 🔑</button>   <-- closes early
<button class="ch-remove-btn">✕</button>            <-- orphaned, "floating"
<div class="ch-item-preview">KpaPocket: Тест</div>  <-- orphaned
<button class="ch-item">[icon] #bookclub …</button>
```

Compounded by `.ch-remove-btn { opacity: 0 }` (only visible on row
hover), which made the ✕ undiscoverable on touch devices even before the
parser bug.

## Fix

`public/channels.js`
- Replace the inner `<button class="ch-remove-btn">` with `<span
class="ch-remove-btn" role="button" tabindex="0">`. Click delegation
already keys off `[data-remove-channel]` so behavior is unchanged.
- Add `keydown` (Enter / Space) handler on `#chList` so the role=button
span stays keyboard-accessible.
- Relabel the ambiguous `🔒 No key` toggle to `🔒 Show encrypted (no
key)`, with an explanatory `title` ("Show encrypted channels you don't
have a key for (locked, can't decrypt)") so users understand it controls
visibility of channels they haven't added a PSK for.

`public/style.css`
- `.ch-remove-btn`: drop `opacity: 0` default. Now `0.55` idle, `0.9` on
row hover, `1` on direct hover/focus. Added `:focus` outline removal +
`display: inline-flex` so the ✕ centers cleanly.
- Add `.ch-user-badge` rule (was unstyled — contributed to the
misalignment of the 🔑).

## TDD

- Red commit `eeb94ad` — `test-channel-sidebar-layout.js` (7 assertions,
3 failing on master).
- Green commit `2959c3d` — fix; all 7 pass.
- Wire commit `4d6100d` — added to `test-all.sh`.

Existing channel test files still pass (`test-channel-psk-ux.js`,
`test-channel-live-decrypt.js`,
`test-channel-live-decrypt-userprefix.js`,
`test-channel-decrypt-m345.js`,
`test-channel-decrypt-insecure-context.js`).

## Files changed
- `public/channels.js`
- `public/style.css`
- `test-channel-sidebar-layout.js` (new)
- `test-all.sh`
2026-05-04 18:29:45 -07:00
Kpa-clawbot 2f0c97604b feat(map): cluster markers with Leaflet.markercluster (#1036) (#1038)
## Summary
Implements map marker clustering for large meshes (500+ nodes) using
vendored `Leaflet.markercluster@1.5.3`. Closes the long-standing no-op
`Show clusters` checkbox.

## What changed
**Vendored library** — `public/vendor/leaflet.markercluster.js` +
`MarkerCluster.css` + `MarkerCluster.Default.css`. No CDN: this runs
offline on mesh-operator deployments.

**`map.js`**
- `createClusterGroup()` instantiates `L.markerClusterGroup` with:
  - `chunkedLoading: true` (no frame drops on initial render)
- `removeOutsideVisibleBounds: true` (viewport culling — key win at 2k+
nodes)
  - `disableClusteringAtZoom: 16` (fully expanded at high zoom)
  - `spiderfyOnMaxZoom: true` (fan out at max zoom)
  - `showCoverageOnHover: false`
  - `animate` disabled on mobile UA for perf
- `makeClusterIcon(cluster)` produces a CoreScope-themed `L.divIcon`:
  - Bold total count, centered
- Up to 4 role-color mini-pills (repeater / companion / room / sensor /
observer) using `ROLE_COLORS`
- Bucketed `mc-sm` / `mc-md` / `mc-lg` background (info / warning /
accent CSS vars)
- `#mcClusters` checkbox repurposed from no-op `Show clusters` →
`Cluster markers`, default **ON**, persisted to
`localStorage['meshcore-map-clustering']`
- Render branches at the marker-add step: clustering ON → `addLayers()`
to `clusterGroup`, skip `deconflictLabels` + `_updateOffsetIndicator`
polylines + `_repositionMarkers` on zoom/resize. Clustering OFF →
original flow unchanged.
- Route polylines (`drawPacketRoute`) already removed both layers — no
change needed beyond actually instantiating `clusterGroup`.
- `?node=PUBKEY` deep-link lookup now searches both `markerLayer` and
`clusterGroup` so it works in either mode.

**`style.css`** — cluster bubble + role-pill styles using `--info` /
`--warning` / `--accent` CSS variables; hover scale.

**`index.html`** — vendor CSS + JS tags after the Leaflet bundle
(cache-busted via `__BUST__`).

## TDD
- **Red commit** `e10af23` — `test-map-clustering.js` + stub
`createClusterGroup`/`makeClusterIcon` returning null/empty divIcon.
Compiles, runs, fails 4/5 on assertions.
- **Green commit** `482ea2e` — real implementation. 5/5 pass.

```
=== map.js: clustering ===
   exposes test hooks (__meshcoreMapInternals)
   createClusterGroup returns an L.MarkerClusterGroup with required options
   cluster group accepts markers via addLayer
   makeClusterIcon: includes total count and role-pill counts
   makeClusterIcon: bucket sm/md/lg by total
```

## Behavior preserved
- Clustering OFF (existing checkbox unchecked) → all original behavior
intact: deconfliction spiral, offset-indicator polylines, per-zoom
reposition.
- Default ON. Operators with small meshes can disable via the checkbox;
choice persists.
- Spiderfying enabled at max zoom (built-in markercluster behavior).

## Performance target
Smooth pan/zoom at 2000 nodes — `chunkedLoading` keeps the main thread
responsive during initial add, `removeOutsideVisibleBounds` keeps DOM
bounded to the viewport. Per AGENTS.md rule 0: complexity is O(n) for
the initial add (chunked across frames), per-zoom re-cluster is internal
to markercluster (well-tested at 10k+ scale).

## Out of scope (filed as follow-ups in spec)
- Canvas marker renderer — only if 5k+ nodes per viewport materializes
- Server-side viewport culling (`/api/nodes?bbox=`)
- Cluster-by-role split groups
- 2k-node fixture + Playwright DOM assertions — repo doesn't currently
ship a `fixture=` query param; the unit test exercises the integration
deterministically.

Fixes #1036

---------

Co-authored-by: corescope-bot <bot@corescope>
2026-05-04 18:29:42 -07:00
Kpa-clawbot 0b0fda5bb2 ci: update go-server-coverage.json [skip ci] 2026-05-04 23:54:17 +00:00
Kpa-clawbot e966ecc71a ci: update go-ingestor-coverage.json [skip ci] 2026-05-04 23:54:16 +00:00
Kpa-clawbot e7aa8eded8 ci: update frontend-tests.json [skip ci] 2026-05-04 23:54:16 +00:00
Kpa-clawbot d652b7c39d ci: update frontend-coverage.json [skip ci] 2026-05-04 23:54:15 +00:00
Kpa-clawbot 6a8ed98d8f ci: update e2e-tests.json [skip ci] 2026-05-04 23:54:14 +00:00
Kpa-clawbot 26daa760cd fix(channels): live PSK decrypt for user-added channels (#1029 follow-up) (#1031)
## Problem

PR #1030 added live PSK decrypt for GRP_TXT WS packets, but in
production it still didn't work for **user-added** PSK channels. New
messages never appeared in real time on a channel added via the sidebar
key form — users had to refresh the page to see them via the REST fetch
path (regression #1029).

## Root cause

`decryptLivePSKBatch` rewrites the payload with the raw channel name:

```js
payload.channel = dec.channelName;   // e.g. "medusa"
```

But user-added channels live in `channels[]` under the key produced by
`addUserChannel`:

```js
hash: 'user:' + name,                // e.g. "user:medusa"
```

`selectedHash` also uses the `user:`-prefixed key while a user-added
channel is open. Downstream in `processWSBatch`:

| Line | Check | Result |
|---|---|---|
| 962 | `c.hash === channelName` | `"medusa" !== "user:medusa"` → user
channel never matched |
| 982 | `channelName === selectedHash` | `"medusa" !== "user:medusa"` →
message never appended to open chat |
| 974 | `channels.push({ hash: channelName, ... })` | duplicate plain
`"medusa"` entry pushed into sidebar |

The unread bumper (`channels.js:1086`) compared `chName === prior` with
the same mismatch, so it bumped an unread badge on the channel currently
being viewed.

Verified end to end against staging WS traffic (live `decryption_status:
"decrypted"` packets observed; user-added channel never updated,
duplicate entry created).

## Fix

`decryptLivePSKBatch` now also stamps a canonical sidebar key on the
payload:

```js
payload.channelKey = hasUserCh ? ('user:' + dec.channelName) : dec.channelName;
```

`processWSBatch` and the unread bumper route on `payload.channelKey`
(falling back to `payload.channel` for server-known CHAN packets — no
behavior change there).

After the fix:
-  live message appends to the open user-added chat
-  sidebar row's `lastMessage` / `messageCount` / `lastActivityMs`
update
-  no duplicate non-prefixed sidebar entry
-  unread bumped only on channels NOT being viewed

## TDD

Red commit `f1719a8` — `test-channel-live-decrypt-userprefix.js`, fails
6/9 on assertions (NOT build error) on pristine `channels.js`.
Green commit `da87018` — minimal fix in `channels.js`, all 9/9 pass.

Verified red gates the change: stashed `public/channels.js`, re-ran test
on red commit alone → 6 assertion failures (open channel got 0 messages,
duplicate sidebar entry, unread bumped on viewed channel).

## Files changed

- `public/channels.js` — stamp/route on `channelKey`
- `test-channel-live-decrypt-userprefix.js` (new) — red-then-green
regression test

---------

Co-authored-by: corescope-bot <bot@corescope>
v3.7.1
2026-05-04 16:44:35 -07:00
Kpa-clawbot c196030ec0 ci: update go-server-coverage.json [skip ci] v3.7.0 2026-05-04 05:06:19 +00:00
Kpa-clawbot 7b07761fb9 ci: update go-ingestor-coverage.json [skip ci] 2026-05-04 05:06:19 +00:00
Kpa-clawbot e47257222e ci: update frontend-tests.json [skip ci] 2026-05-04 05:06:18 +00:00
Kpa-clawbot 6f2d70599a ci: update frontend-coverage.json [skip ci] 2026-05-04 05:06:17 +00:00
Kpa-clawbot c120b5eef2 ci: update e2e-tests.json [skip ci] 2026-05-04 05:06:16 +00:00
Kpa-clawbot 3290ff1ed5 fix(channels): auto-decrypt PSK channels on WebSocket live feed (#1029) (#1030)
Closes #1029.

## Problem

PSK-decrypted channels show new messages only after a full page refresh.
The WebSocket live feed delivers `GRP_TXT` packets as encrypted blobs
and the channel UI has no hook to auto-decrypt them with stored keys.
The REST fetch path (used on initial load + on `selectChannel`) already
decrypts; the WS path silently dropped on the floor.

## Fix

Two new helpers in `public/channel-decrypt.js`:

- `buildKeyMap()` → `Map<channelHashByte, { channelName, keyBytes,
keyHex }>`
  built from `getStoredKeys()`. Cached and invalidated on `saveKey` /
  `removeKey`, so the WS hot path is O(1) per packet after the first
  build.
- `tryDecryptLive(payload, keyMap)` → returns
`{ sender, text, channelName, channelHashByte }` when the payload is an
  encrypted `GRP_TXT` whose channel hash matches a stored key and whose
  MAC verifies; `null` otherwise.

`public/channels.js` wraps `debouncedOnWS` with an async pre-pass
(`decryptLivePSKBatch`) that:

1. Skips the work entirely when no encrypted `GRP_TXT` is in the batch
   or no PSK keys are stored.
2. For each match, rewrites `payload.channel`, `payload.sender`, and
   `payload.text` so the existing `processWSBatch` consumes the packet
   exactly the same way it consumes a server-decrypted `CHAN`.
3. Bumps a per-channel `unread` counter for any decrypted message
   whose channel is not currently selected. The badge renders in the
   sidebar (`.ch-unread-badge`) and resets on `selectChannel`.

`processWSBatch` itself is untouched, so the existing channel-view
behavior, dedup-by-packet-hash, region filtering, and timestamp ticker
all continue to work as before.

## TDD

- **Red** (`2e1ff05`): `test-channel-live-decrypt.js` asserts the new
  helpers + the channels.js integration contract. With stub
  `buildKeyMap`/`tryDecryptLive` returning empty/null, the test compiles
  and runs to completion with **8/14 assertion failures** (no crashes,
  no missing-symbol errors).
- **Green** (`1783658`): real implementation lands; **14/14 pass**.

## Verification (Rule 18)

- `node test-channel-live-decrypt.js` → 14/14 pass
- All other channel tests still pass:
  - `test-channel-decrypt-ecb.js` 7/7
  - `test-channel-decrypt-insecure-context.js` 8/8
  - `test-channel-decrypt-m345.js` 24/24
  - `test-channel-psk-ux.js` 19/19
- `cd cmd/server && go build ./...` clean
- Booted the server against the fixture DB and curled
  `/channel-decrypt.js`, `/channels.js`, `/style.css` — all three serve
  the new code with the auto-injected `__BUST__` cache buster.

## Performance

The WS pre-pass is gated by a quick scan: zero-cost when no encrypted
`GRP_TXT` is present in the batch. When PSK keys exist, the key map is
cached (sig-keyed on the stored-keys snapshot) so `crypto.subtle.digest`
runs once per stored key per change, not per packet. Each match costs
one MAC verify + one ECB decrypt — the same work
`fetchAndDecryptChannel`
already does, just amortized over time instead of in a single batch.

## Out of scope

- Decoupling the badge from the live feed (server should ideally tag
  packets with `decryptionStatus` before broadcast). Tracked separately.
- Persisting the `unread` counter across reloads (currently in-memory).

---------

Co-authored-by: clawbot <bot@corescope.local>
2026-05-04 04:56:43 +00:00
Kpa-clawbot 505206feb4 ci: update go-server-coverage.json [skip ci] 2026-05-04 04:52:13 +00:00
Kpa-clawbot 41762a873a ci: update go-ingestor-coverage.json [skip ci] 2026-05-04 04:52:12 +00:00
Kpa-clawbot 7ab05c5a19 ci: update frontend-tests.json [skip ci] 2026-05-04 04:52:11 +00:00
Kpa-clawbot c3138a96f7 ci: update frontend-coverage.json [skip ci] 2026-05-04 04:52:10 +00:00