## What
Two Phosphor lint-gate breaches found by the v3.8.4 manual-test executor
— app-controlled UI labels still shipping raw emoji glyphs that the M6
final sweep (#1648) missed. One PR, two sprite swaps, same playbook as
#1657.
### Findings
| Test ID | Surface | Glyph | File:line | Fix |
|---|---|---|---|---|
| v384-1.2 | `/observers` `.obs-clock-naive-chip` | `⚠️` (U+26A0) ×14 |
`public/observers.js:30` | `ph-warning` sprite |
| v384-12.18 | `/analytics?tab=channels` encrypted row name cells | `🔒`
(U+1F512) ×158 | `public/analytics.js:978–979` | `ph-lock` sprite |
Finding 2 is a different surface from the M3/#1657 fix (which swapped
the section-header label, not the per-row `displayName`). The
unknown-encrypted row's `displayName` carried a raw `🔒 Encrypted (0xNN)`
text label that then flowed through `esc()` into the rendered name cell
as an escaped emoji glyph — exactly the same `innerText → innerHTML`
class of bug. Refactored to mirror the section-header pattern:
`displayNameHtml` carries the sprite-bearing raw HTML; `displayName`
stays plain text for sort/aria/tests.
## TDD
- **RED** `cde12370` — `test-issue-1648-followup-phosphor-leaks.js`
asserts ph-warning sprite + zero ⚠ in chip output, and ph-lock sprite +
zero 🔒 in analytics row labels. 6 assertions failed on master.
- **GREEN** `f1c64b17` — sprite swaps applied. All 9 assertions pass.
- **Anti-tautology proven both directions**: reverting only
`public/observers.js` → 2 chip-related assertions fail; reverting only
`public/analytics.js` → 4 analytics-related assertions fail.
## Verify
- ✅ `node test-issue-1648-followup-phosphor-leaks.js` — 9/9 pass
- ✅ `node test-issue-1648-m6-final-sweep.js` — 0 violations
- ✅ `node test-observer-naive-clock-1478.js` — 8/8 pass (existing chip
test accepts ph-warning sprite)
- ✅ `node test-analytics-channels-integration.js` — pre-existing
unrelated `Channel Analytics` failure only; encrypted-row assertions all
pass with new plain-text `displayName`
- ✅ pr-preflight all gates green (PII, branch-scope, red-commit,
CSS-var, LIKE-on-JSON, async-migration, XSS sinks)
- ✅ Browser-verified on staging: 11 chips render ph-warning sprite (0
emoji), 156 ph-lock sprites in row name cells (0 lock emoji on page)
Browser verified: http://analyzer-stg.00id.net/#/observers +
/#/analytics?tab=channels (hot-patched)
E2E assertion added: `test-issue-1648-followup-phosphor-leaks.js:67`
(chip), `test-issue-1648-followup-phosphor-leaks.js:147` (row cell)
---------
Co-authored-by: meshcore-bot <bot@meshcore.dev>
## 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>