Fixes#1105.
Polish follow-ups from #1104's independent review
(https://github.com/Kpa-clawbot/CoreScope/pull/1104#issuecomment-4381850096).
All 9 MINORs addressed.
## Hardening (`public/app.js`, commit fa58cb6)
1. **`GUTTER = 24` magic constant** → live
`getComputedStyle(navLeft).columnGap` read. The "matches `--space-lg`"
assertion now lives in CSS, not a stale JS literal.
2. **`fits()` conflated two distinct gaps** → reads `.nav-left`'s gap
(between brand/links/more/right cells) and `.nav-links`'s gap (between
link items) separately. Today both are `--space-lg=24px`, but a future
divergence won't silently miscompute fit.
3. **Implicit 1101px media-query flip dependency** → comment added
explaining that `.nav-stats` toggles `display:none ↔ flex` at the
boundary, and the rAF-debounced resize handler runs *after* the layout
flip so `navRightEl.scrollWidth` reflects the post-flip value.
4. **Outer null-guard widened** → now also covers `linksContainer`,
`navRightEl`, `navLeft`, `navTop`. Belt-and-braces.
5. **Cloned link listener parity** → More-menu clones now also get
`closeNav()` in addition to `closeMoreMenu()`, matching the listener
inline links get at hamburger init. Clicks from the More menu now
collapse the hamburger panel just like inline link clicks.
6. **`overflowQueue` ordering** → comment added documenting the
`data-priority="high"` signal + reverse construction; explicit
numeric-priority migration path noted.
7. **`moreW` hard-coded `70` fallback** → now caches the live measured
width the first time the More button is rendered visible;
`MORE_BTN_RESERVE_PX = 70` only used as the conservative initial guess
until that capture happens.
## Tests (`test-nav-priority-1102-e2e.js`, commit 5e9872c)
8. **Identity, not cardinality** (MINOR 7): at 1080/800px the test
asserts the visible set is EXACTLY `[#/home, #/packets, #/map, #/live,
#/nodes]`. A buggy queue that hid Home and showed Lab would still pass
`visibleCount >= 5` — that's no longer enough.
9. **Active-mirroring** (MINOR 9): new case navigates to `#/observers`
at 1080px (a route whose link overflows into the More menu) and asserts
the inline link is overflowed, the More-menu clone has `.active`, and
`#navMoreBtn` has `.active`. Exercises `rebuildMoreMenu`'s
active-mirroring path, which depends on `applyNavPriority` running on
`hashchange` after the route handler.
10. **CI hookup** (MINOR 8): `deploy.yml` now runs
`test-nav-priority-1102-e2e.js` with `CHROMIUM_REQUIRE=1`, so a Chromium
provisioning regression fails the build instead of silently SKIPing
(matching the existing `test-nav-fluid-1055-e2e.js` invocation).
## Why no red-then-green
Per AGENTS.md TDD section: hardening commit is a pure
code-quality/null-guard refactor — existing tests stay green and
unaltered (the loose `visibleCount >=` assertions still pass against the
new code). Test-improvement commit tightens assertions for behaviour
that already works (high-priority pinning, active-mirroring); there's no
production change to gate. Both branches of "exempt from red→green" are
documented in the commit messages.
## E2E / browser validation
Test runs against the Go server fixture (`-port 13581 -db
test-fixtures/e2e-fixture.db`). All 5 cases (4 viewport cases + new
active-mirror case) expected to pass; CI will run them with
`CHROMIUM_REQUIRE=1` so any Chromium provisioning regression hard-fails.
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
## Summary
Strips the Share Channel modal (shipped in #1090) down to its
essentials. Removes redundant affordances that the QR already provides.
## What changed
**Removed from the Share modal:**
- The URL text printed inside the QR box (the QR encodes the URL)
- The inline Copy Key button inside the QR box (overlapped the image)
- The `meshcore://` URL input field below the QR
- The Copy URL button next to the URL field
**Result — the modal now contains exactly:**
- Title `Share: <Channel Name>`
- QR code (just the QR `<img>`, nothing else in that box)
- Hex Key field with a single Copy button BELOW the QR
- Privacy warning
- ✕ close button (top right)
## Implementation
- `public/channels.js` — drop the `meshcore://` URL field-group from
share modal markup; `openShareModal()` no longer looks up `#chShareUrl`
or builds a URL into a field; pass `{ qrOnly: true }` when calling
`ChannelQR.generate` so the QR box renders ONLY the QR image.
- `public/channel-qr.js` — `generate(name, secret, target, opts)` now
accepts `opts.qrOnly` which short-circuits before appending the inline
URL line + Copy Key button. Default behaviour (no opts) unchanged, so
the Add-Channel "Generate & Show QR" flow is untouched.
## Tests (TDD: red → green)
- New: `test-channel-issue-1101.js` (static grep) — asserts the URL
field is gone from markup, `openShareModal` no longer references it, and
`ChannelQR.generate` honours `qrOnly`.
- Updated: `test-channel-issue-1087.js` and
`test-channel-issue-1087-e2e.js` — those previously asserted the URL
field's presence (which is exactly what #1101 removes); they now assert
ONLY the hex key field exists, AND that `#chShareQr` contains exactly
one `<img>` and no `.channel-qr-url` / `.channel-qr-copy` children.
- Wired into `.github/workflows/deploy.yml` `node-test` job.
Commit history shows red (test commit `c0c254a`) → green (fix commit
`6315a19`) per AGENTS.md TDD requirement.
E2E assertion added: test-channel-issue-1087-e2e.js:184
## Acceptance criteria
- [x] Share modal contains only: QR, "Copy Key" button, privacy warning
- [x] No "Copy URL" affordance anywhere in the modal
- [x] No duplicated hex key field below
- [x] E2E test asserts the absence of the removed elements
Fixes#1101
---------
Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
## Summary
Fixes#1102 — regression from PR #1097 polish where Priority+ collapsed
too aggressively at wide widths and the "More" menu didn't reflect what
was actually hidden.
## Root cause
Two bugs, one root: the post-#1097 CSS rule
```css
.nav-links a:not([data-priority="high"]) { display: none; }
```
unconditionally hid 6 of 11 links at every width ≥768px — including
2560px where everything fits comfortably. The "More" menu populator
(`querySelectorAll('.nav-links a:not([data-priority="high"])')`) ran
exactly once on load against that same selector, so it always held the
same 6 links and never reflected the actual viewport fit.
## Fix
Replace the static CSS hide with a JS measurement pass
(`applyNavPriority` in `public/app.js`):
1. Start with **all** links visible inline.
2. Compute `needed = brand + gutters + visible-links + More + nav-right
+ safety` and compare to `window.innerWidth`.
3. If it doesn't fit, mark the rightmost (lowest-priority) link
`.is-overflow` and re-measure. Repeat. High-priority links are queued
last so they're kept visible as long as possible.
4. Rebuild the "More ▾" menu from whatever currently has `.is-overflow`.
When nothing overflows, hide the More wrap entirely (`.is-hidden`).
5. Re-run on resize (rAF-debounced), on `hashchange` (active-link
padding shifts), and after fonts load.
Why JS, not CSS: the breakpoint where each link drops depends on label
width, gutters, active-link padding, and the nav-stats badge — none of
which are addressable from a media query.
## TDD trail
- Red commit `8507756`: `test-nav-priority-1102-e2e.js` — fails 2/4
(2560 and 1920 only show 5/11).
- Green commit `3e84736`: implementation — passes 4/4.
## E2E
`test-nav-priority-1102-e2e.js` asserts:
- 2560px → all 11 visible, More hidden
- 1920px → ≥9 visible
- 1080px → 5+ visible AND More menu contains every hidden link
- 800px → 5+ visible AND More menu non-empty
Local run on the e2e fixture: **4/4 pass**. Existing
`test-nav-fluid-1055-e2e.js` also stays green: **20/20 pass** (no
overlap, no overflow at 768/1024/1280/1440/1920 across 4 routes).
---------
Co-authored-by: meshcore-bot <bot@meshcore.local>
## Summary
Makes the channels page sidebar + message area fluid as part of the
parent #1050 fluid-layout effort. Replaces the hardcoded
`.ch-sidebar { width: 280px; min-width: 280px }` with
`width: clamp(220px, 22vw, 320px); min-width: 220px`. Adds an
`@container` query (via `container-type: inline-size` on `.ch-layout`)
that stacks the sidebar above the message area when the channels
page itself is narrow (≤700px container width) — independent of
the global viewport, so it adapts even when an outer panel is
consuming width. Removes the legacy `@media (max-width: 900px)`
fixed 220px override; the clamp + container query handle that range.
`.ch-main` already used `flex: 1`, so it absorbs all remaining width
including ultrawides. The existing mobile (≤640px) overlay rules and
the JS resize handle in `channels.js` are untouched and still work
(user drag still wins via inline width).
Fixes#1057.
## Scope
- `public/style.css` — channels section only
- (no `public/channels.js` changes needed)
## Tests
TDD: red commit (failing tests) → green commit (implementation).
- `test-channel-fluid-layout.js` (new): static CSS assertions
- `.ch-sidebar` uses `clamp()` for width (not fixed px)
- `.ch-sidebar` keeps a sane `min-width` (200–280px)
- `.ch-main` keeps `flex: 1`
- `.ch-layout` declares `container-type` (container query root)
- `@container` rule scopes channels stacking
- legacy `@media (max-width: 900px) .ch-sidebar { width: 220px }` is
gone
- `test-channel-fluid-e2e.js` (new): Playwright E2E at
768 / 1080 / 1440 / 1920 (wide) and 480 (narrow). Asserts:
- no horizontal scroll on the body
- sidebar AND message area both visible side-by-side at ≥768px
- sidebar consumes ≤45% of viewport, main ≥40%
- at 480px the layout stacks (or overlays) — no overflow
Wired into `test-all.sh` and the unit + e2e steps of
`.github/workflows/deploy.yml`.
## Verification
- Static unit test: 6/6 pass on the green commit, 4/6 fail on the
red commit (only the two trivially-true assertions pass).
- Local Go server boot: `corescope-server` serves the updated
`style.css` containing `container-type: inline-size`,
`clamp(220px, 22vw, 320px)`, and `@container chlayout (max-width:
700px)`.
- Local Chromium on the dev sandbox is musl-incompatible
(Playwright fallback build crashes with `Error relocating ...:
posix_fallocate64: symbol not found`), so the E2E was not run
locally. CI will run it on Ubuntu runners.
---------
Co-authored-by: clawbot <clawbot@example.com>
Co-authored-by: meshcore-bot <bot@meshcore.local>
## Summary
Make the top-nav use the **Priority+ pattern at all widths** (not just
768–1279px), so the nav-right cluster never gets pushed off-screen or
visually overlapped by the link strip.
Fixes#1055.
## What changed
**`public/style.css`** — nav section only (clearly fenced):
- Removed the upper bound on the Priority+ media query (`max-width:
1279px`). The rule now applies at any viewport `>= 768px`. Above that
breakpoint, only `data-priority="high"` links render inline; the rest
collapse into the existing `More ▾` overflow menu.
- Swapped nav-only hardcoded spacing/type to the fluid `clamp()` tokens
shipped in #1054:
- `.top-nav` padding → `var(--gutter)`
- `.nav-left` gap → `var(--space-lg)`
- `.nav-brand` gap → `var(--space-sm)`, font-size → `var(--fs-md)`
- `.nav-links` gap → `var(--space-xs)`
- `.nav-link` padding → `clamp(8px, 0.6vw + 4px, 14px)`, font-size →
`var(--fs-sm)`
- `.nav-right` gap → `var(--space-sm)`
- Mobile (<768px) hamburger layout, the More-menu markup, and the JS
that builds the menu in `public/app.js` are unchanged — they already
supported this pattern.
`public/index.html` did not need changes — the `data-priority="high"`
markup, `nav-more-wrap`, `navMoreBtn`, and `navMoreMenu` are already in
place from earlier work.
## Why the bug existed
The previous Priority+ rule was scoped `@media (min-width: 768px) and
(max-width: 1279px)`. From 1280px–~1599px the full 11-link strip
rendered but didn't fit alongside `.nav-stats` + `.nav-right`. The
parent `overflow: hidden` masked the symptom, but the rightmost links
physically rendered underneath `.nav-right` and were unreachable.
## E2E assertion added
New `test-nav-fluid-1055-e2e.js` — Playwright multi-viewport test
(768/1024/1280/1440/1920) that asserts:
1. `.nav-right.right` ≤ `document.documentElement.clientWidth` (no
horizontal overflow)
2. Last visible `.nav-link.right` ≤ `.nav-right.left` (no overlap
underneath the right cluster)
3. `.top-nav.scrollWidth` ≤ `.top-nav.clientWidth` (no scrolled-off
content)
Wired into the `e2e-test` job in `.github/workflows/deploy.yml`.
**TDD evidence:**
- Red commit `466221a`: test passes 3/5 (1024/768/1920) — fails at 1280
(253px overlap) and 1440 (93px overlap).
- Green commit `1aa939a`: test passes 5/5.
## Acceptance criteria (from #1055)
- [x] Priority+ at ALL widths (not just mobile).
- [x] No nav link overflow at 1080px (or any tested width).
- [x] Overflow menu accessible via keyboard + touch (existing
`navMoreBtn` aria-haspopup wiring; verified by existing app.js
handlers).
- [x] Active route still highlighted when in overflow (existing logic in
`app.js` adds `.active` to the cloned link in `navMoreMenu`).
- [x] Tested at 768/1024/1280/1440/1920 — visible link count adapts (5
priority links + More menu at all desktop widths; full 11 inline only on
hamburger mobile when expanded).
---------
Co-authored-by: bot <bot@corescope>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: meshcore-bot <bot@meshcore.local>
## Summary
Fixes#1059 — Task 6 of #1050. Makes map controls + modals fluid and
safely capped so they work across 768px–2560px viewports.
## Changes
`public/style.css` only — modal section + map-controls section (per task
scope).
### Map controls (`.map-controls`)
- `width: clamp(160px, 18vw, 240px)` — fluid, scales with viewport.
- `max-width: calc(100vw - 24px)` — never overflows narrow viewports.
- Eliminates horizontal scroll on the map page at
768/1024/1440/1920/2560.
### Modal box (`.modal`)
- `max-height: 80vh → 90vh` (spec §3).
- `width: min(90vw, 500px)` — fluid, drops to 90vw below 555px.
- `position: relative` so sticky descendants anchor to the modal box.
- `.modal-overlay` gets `padding: clamp(8px, 2vw, 24px)` for edge
breathing room.
### BYOP modal sticky close
- `.byop-header { position: sticky; top: 0 }` with `var(--card-bg)`
backdrop and bottom border — the title bar + ✕ stay reachable while the
body scrolls.
- `.byop-x` restyled with border, hit area, hover state.
### Untouched (intentional)
- `public/map.js` did not need changes — the `.map-controls` element is
the only narrow-viewport offender; the markup stays identical.
- Channel modals (`.ch-modal*`, `.ch-share-modal*`) already have their
own width/max-width tokens from #1034/#1087 and are out of scope for
this task.
## TDD
- **Red commit** `b69e992`: `test-map-modal-fluid-e2e.js` asserts (a) no
horizontal scroll on `/#/map` at 1024/1440/1920/2560, (b)
`.map-controls` right edge inside viewport at 768px wide, (c) BYOP modal
at 1024×768 has `height ≤ 90vh`, `overflow-y: auto|scroll`, and close
button is `position: sticky` and reachable. All assertions fail against
the previous CSS (fixed-width 220px controls overflow at narrow widths;
modal max-height was 80vh, not 90vh; close button was `position:
static`).
- **Green commit** `3e6df9d`: CSS changes above; all assertions pass.
## E2E
- Wired into `.github/workflows/deploy.yml` after the channel-1087 E2E:
```
BASE_URL=http://localhost:13581 node test-map-modal-fluid-e2e.js
```
## Acceptance criteria
- [x] Map controls do not overlap markers at narrow viewports (fluid
clamp width + max-width).
- [x] Map fills extra space on ultrawide (panel caps at 240px, leaflet
flex:1 takes the rest — already true; controls no longer steal grow
room).
- [x] Modals: `max-height: 90vh`, internal scroll, sticky close button,
max-width via `min()`.
- [x] No modal can exceed viewport height at any tested width.
- [x] Verified via E2E at 768/1024/1440/1920/2560.
## Out of scope (left for sibling tasks under #1050)
- Tab bars / nav (Task 1050-1, blocker).
- Filter bars and table chrome (other 1050-N tasks).
---------
Co-authored-by: corescope-bot <bot@corescope.local>
## Summary
Makes the analytics chart grid fluid and auto-stacking based on its
**own** width rather than the viewport's. Implements task 5 of #1050.
## What changed
- `public/style.css` — `.analytics-charts` section only:
- Replaced `grid-template-columns: 1fr 1fr` with `repeat(auto-fit,
minmax(min(100%, 380px), 1fr))` so columns wrap when intrinsic space is
too narrow.
- Added `container-type: inline-size` so the grid is a query container
and descendants/future tweaks can size against its own width rather than
the viewport. The `auto-fit minmax` already handles the stack-on-narrow
case, so the previously-included `@container (max-width: 800px)` rule
was redundant and has been dropped to keep one source of truth.
- `min-width: 0` on cards and `max-width: 100%; height: auto` on
`<svg>`/`<canvas>` (descendant selector, robust to wrapper elements
between the card and the chart media) to prevent intrinsic-content
overflow.
- Switched hardcoded `12px` / `16px` spacing to the #1054 tokens
`--space-sm` / `--space-md`.
- Removed the redundant `@media (max-width: 768px) { .analytics-charts {
grid-template-columns: 1fr; } }` rule (the fluid grid supersedes it).
No `analytics.js` / `node-analytics.js` markup changes were required —
the existing classes are reused.
## TDD
- **Red commit (47f56e9)** — `test-analytics-fluid-charts.js`: failing
E2E that loads `public/style.css` against a sized harness and asserts no
overflow + correct stacking. On master: assertion failures on
container-type opt-in + wide-viewport / narrow-container stacking.
- **Green commit (d300dfa)** — CSS fix; all assertions pass.
## E2E (mandatory frontend coverage)
`node test-analytics-fluid-charts.js` — Playwright + Chromium against a
`file://` harness, 8/8 assertions:
- `.analytics-charts` opts in to container queries (`container-type:
inline-size`)
- viewport 1440 / wrapper 1300px → side-by-side (≥2 cols), no overflow
- viewport 1080 / wrapper 1040px → no horizontal overflow
- viewport 768 / wrapper 760px → cards stack to 1 column, no overflow
- viewport 1440 / wrapper 600px → cards stack via fluid grid (the
original bug)
- viewport 1920 / wrapper 1880px → side-by-side (≥2 cols), no overflow
(AC4)
- viewport 2560 / wrapper 2520px → side-by-side (≥2 cols), no overflow
(AC4)
- AC3: open at 1440px wide (side-by-side), shrink wrapper to 760px /
viewport to 768px, assert layout reflows to 1 column (charts redraw on
resize, not stuck at initial value)
`node test-fluid-scaffolding.js` — still green (15/15), confirms #1054
tokens are unaffected.
Partial fix for #1058
---------
Co-authored-by: meshcore-bot <bot@meshcore.local>
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>
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>
## 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>