[fix](trx-frontend-http): keep the mini views to the rig on screen #53

Merged
sjg merged 2 commits from fix/mini-view-active-rig into main 2026-08-07 07:43:32 +02:00
Owner

Fixes #49 — the mini APRS view over the spectrum showed frames from background rigs.

Cause

/decode and /decode/history are not rig-scoped: every rig's decodes reach the browser, each tagged with the rig_id the client stamps on it as it leaves the audio connection. The decoder panels want that — they aggregate the whole station — but the overlays riding over the waterfall caption the spectrum underneath, and they read the same unfiltered histories.

The mode gate didn't save them either: it reads the mode of the rig on screen, so a background rig's APRS traffic popped the bar up whenever the active rig happened to be in PKT.

Fix

A shared isActiveRigDecode(rigId) (src/plugins/active-rig.ts) compares a decode's rig against hostState.lastActiveRigId — the per-tab selection that already drives the spectrum and the audio. A decode that names no rig, and a session that hasn't learnt its rig list yet, still show everything.

All five overlays share the defect, so all five are filtered:

  • aprs.ts, ais.ts, vdes.ts — filter the bar's frames.
  • ftx-family.ts — its normalizer was dropping rig_id on the floor; it now keeps it, and the FT8/FT4/FT2 bar filters on it.
  • cw.ts — CW needed more than a filter. Characters accumulate into a line, so two rigs copying at once braided their text into one unreadable line. Lines in progress are now kept per rig.
  • app.ts — the bar repaints in render() become refreshDecodeBars(), which the rig switch calls too (otherwise the outgoing rig's frames sat on screen until the next state update) and which finally includes the CW bar.

The decoder panels are untouched: they still list every rig.

Verification

  • 59 unit tests pass, including 6 new ones in tests/mini-view-rig.test.mjs — one per overlay, plus the no-rig-known fallback.
  • decode-flow.mjs now serves a second vessel from rig-b and asserts it reaches the AIS panel but not the mini view.
  • typecheck, lint, and the other four browser tests pass. Generated bundles rebuilt.

Left alone

Two related things outside this issue: the CW panel still appends every rig's text into one pane (same braiding, but that pane is the only place a background rig's CW is visible at all), and the CW WPM/tone readouts auto-fill from any rig's event.

Fixes #49 — the mini APRS view over the spectrum showed frames from background rigs. ## Cause `/decode` and `/decode/history` are not rig-scoped: every rig's decodes reach the browser, each tagged with the `rig_id` the client stamps on it as it leaves the audio connection. The decoder panels want that — they aggregate the whole station — but the overlays riding over the waterfall caption the spectrum underneath, and they read the same unfiltered histories. The mode gate didn't save them either: it reads the mode of the rig *on screen*, so a background rig's APRS traffic popped the bar up whenever the active rig happened to be in PKT. ## Fix A shared `isActiveRigDecode(rigId)` (`src/plugins/active-rig.ts`) compares a decode's rig against `hostState.lastActiveRigId` — the per-tab selection that already drives the spectrum and the audio. A decode that names no rig, and a session that hasn't learnt its rig list yet, still show everything. All five overlays share the defect, so all five are filtered: - `aprs.ts`, `ais.ts`, `vdes.ts` — filter the bar's frames. - `ftx-family.ts` — its normalizer was dropping `rig_id` on the floor; it now keeps it, and the FT8/FT4/FT2 bar filters on it. - `cw.ts` — CW needed more than a filter. Characters accumulate into a line, so two rigs copying at once braided their text into one unreadable line. Lines in progress are now kept per rig. - `app.ts` — the bar repaints in `render()` become `refreshDecodeBars()`, which the rig switch calls too (otherwise the outgoing rig's frames sat on screen until the next state update) and which finally includes the CW bar. The decoder panels are untouched: they still list every rig. ## Verification - 59 unit tests pass, including 6 new ones in `tests/mini-view-rig.test.mjs` — one per overlay, plus the no-rig-known fallback. - `decode-flow.mjs` now serves a second vessel from `rig-b` and asserts it reaches the AIS panel but not the mini view. - `typecheck`, `lint`, and the other four browser tests pass. Generated bundles rebuilt. ## Left alone Two related things outside this issue: the CW *panel* still appends every rig's text into one pane (same braiding, but that pane is the only place a background rig's CW is visible at all), and the CW WPM/tone readouts auto-fill from any rig's event.
sjg added 1 commit 2026-08-07 00:47:02 +02:00
[fix](trx-frontend-http): keep the mini views to the rig on screen
CI / lint (pull_request) Successful in 2m23s
CI / test (pull_request) Successful in 8m34s
CI / frontend (pull_request) Failing after 1m32s
CI / reuse (pull_request) Successful in 6s
15ff686542
The decode SSE stream and the history behind it are not rig-scoped: every
rig's decodes reach the browser, each carrying the rig that heard it.  The
panels on the decoder tabs want that — they aggregate the whole station —
but the mini views over the waterfall caption the spectrum underneath, and
they were reading the same unfiltered histories.  A background rig copying
APRS on another band put its frames over the active rig's waterfall.  The
mode gate did not help: it reads the mode of the rig on screen, so those
frames appeared whenever that rig happened to be in PKT.

Filter each overlay on the rig it belongs to, through one shared predicate
that compares a decode's rig_id with the per-tab active rig already driving
the spectrum and the audio.  A decode that names no rig, and a session that
has not learnt its rig list yet, still show everything.

The FTx normalizer was dropping rig_id on the floor, so it now keeps it.
CW needed more than a filter: its lines accumulate character by character,
so two rigs copying at once braided their text into one unreadable line.
Lines in progress are now kept per rig.

The bar repaints in render() move into refreshDecodeBars(), which the rig
switch calls as well — otherwise the outgoing rig's frames stayed on screen
until the next state update — and which finally includes the CW bar.

Closes #49

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg added 1 commit 2026-08-07 02:47:11 +02:00
[test](trx-frontend-http): type the frequency instead of filling it
CI / lint (pull_request) Successful in 2m20s
CI / test (pull_request) Successful in 8m32s
CI / frontend (pull_request) Successful in 4m27s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m26s
CI / test (push) Successful in 7m42s
CI / frontend (push) Successful in 3m35s
CI / reuse (push) Successful in 5s
86dd36312e
tune-links drove the dial with Playwright's fill(), which writes a value
into the field without a keystroke.  The app arms its guard against its own
refreshes on the first keydown, so a filled field stays unguarded: any state
update landing between the fill and the Enter rewrites the field with the
frequency the radio is already on, and the Enter then re-applies that.  The
window is a few milliseconds wide on a developer's machine and wide enough
to lose on a loaded CI runner, where the test failed claiming the tuning had
landed on the frequency it started from.

Type it the way an operator does: select the field, then send the characters
as keystrokes.  The select arms the guard before a single character changes.

Under CPU throttling that reproduced the failure — 1 in 6 runs with fill(),
on this branch and on main alike — typing came through 8 runs clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
Author
Owner

The tune-links failure is not from this change — it reproduces on main too, and it's a race in the test harness rather than in the app.

tune-links drove the dial with Playwright's fill(), which writes a value into the field without a keystroke. The app arms freqDirty on the first keydown (app.ts:4316), and until it is armed, applyLocalTunedFrequency() refreshes the field from the rig's current frequency (app.ts:2355). So a filled field is unguarded: any state update landing in the gap between the fill and the Enter puts 14.074M back into the box, and the Enter re-applies the frequency the radio was already on — exactly the + 14074000 / - 7040000 CI reported.

The window is a few milliseconds wide locally and wide enough to lose on a loaded runner. Under CDP CPU throttling I could reproduce it on demand:

variant throttle result
fill(), this branch 12× 1 failure in 6 runs
fill(), main @ b78c4a4 12× 1 failure in 6 runs
typed (this commit) 12× and 14× 0 failures in 8 runs

86dd363 types the frequency the way an operator does — select the field, then send the characters as keystrokes — so the guard is armed before a single character changes. Nothing in the app changed for it.

Worth noting the app behaviour this leans on is correct: a real operator's first keystroke arms the guard, so mid-edit state updates never overwrite what is being typed. Only fill() skipped it.

The `tune-links` failure is not from this change — it reproduces on `main` too, and it's a race in the test harness rather than in the app. `tune-links` drove the dial with Playwright's `fill()`, which writes a value into the field without a keystroke. The app arms `freqDirty` on the first `keydown` (`app.ts:4316`), and until it is armed, `applyLocalTunedFrequency()` refreshes the field from the rig's current frequency (`app.ts:2355`). So a filled field is unguarded: any state update landing in the gap between the fill and the Enter puts `14.074M` back into the box, and the Enter re-applies the frequency the radio was already on — exactly the `+ 14074000 / - 7040000` CI reported. The window is a few milliseconds wide locally and wide enough to lose on a loaded runner. Under CDP CPU throttling I could reproduce it on demand: | variant | throttle | result | |---|---|---| | `fill()`, this branch | 12× | 1 failure in 6 runs | | `fill()`, `main` @ b78c4a4 | 12× | 1 failure in 6 runs | | typed (this commit) | 12× and 14× | 0 failures in 8 runs | 86dd363 types the frequency the way an operator does — select the field, then send the characters as keystrokes — so the guard is armed before a single character changes. Nothing in the app changed for it. Worth noting the app behaviour this leans on is correct: a real operator's first keystroke arms the guard, so mid-edit state updates never overwrite what is being typed. Only `fill()` skipped it.
sjg merged commit 86dd36312e into main 2026-08-07 07:43:32 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sjg/trx-rs#53