[fix](trx-frontend-http): restore bookmark host contract #23

Merged
sjg merged 2 commits from fix/bookmark-host-contract into main 2026-08-02 11:24:07 +02:00
Owner

What broke

The TypeScript migration turned app.js from a classic script into an ES module, so its top-level declarations stopped being shared globals. The converted feature entries kept reading them as window properties — window.postPath, window.serverLat, window.authRole, window.setRigFrequency and others — none of which app.ts publishes.

Reproduced by running the shipped bookmarks bundle in headless Chromium against a stub host:

addBtn: "none"     Add Bookmark / Select All permanently hidden (auth check saw undefined)
decoderBoxes: 0    decoder checkboxes never built
scopeOptions: 1    per-rig scopes missing from scope picker and move target
Tune:              zero network calls — "TypeError: bridge.postPath is not a function"

The same defect ran through the other entries:

Entry Read as undefined Effect
ais, aprs, hf-aprs serverLat, serverLon, haversineKm empty distance on every positioned packet
ais, aprs, hf-aprs, cw, sat, vdes, wefax, wspr postPath clear-history and decoder toggles threw
scheduler authRole lazy-load path never self-initialized; Settings opened an inert scheduler
background-decode authEnabled control gating fell back to role-only
vchan 15 values and services mode/bandwidth sync, out-of-band hint, RX audio restart and the frequency field all no-opped on a virtual channel
ftx-family fmtTime decode bar timestamps rendered empty

Two further instances were structural rather than a plain read:

  • vchan wrapped window.refreshFreqDisplay, capturing an undefined original exactly as it did for setRigFrequency, so leaving a virtual channel never restored the application's own frequency display.
  • _audioChannelOverride was a const nothing could assign — TypeScript had inferred it from the only remaining write site — so RX audio always subscribed to the primary channel.

The fix

docs/frontend-architecture.md closes the standalone window property list, so this extends the typed host contract rather than restoring globals.

  • plugins/host.ts (new) declares the window.trx.state / window.trx.core view once; feature entries import it instead of re-deriving it. bookmarks.ts was migrated onto it too, so there is a single declaration.
  • app.tstrx.state publishes authEnabled, jogUnit, rxActive, audioChannelOverride, and makes lastModeName writable; trx.core publishes the tuning, RDS, WFM, jog and RX-audio services the entries need.
  • vchan.ts — the dead setRigFrequency and refreshFreqDisplay window wrappers become interceptFrequency() and interceptFreqDisplay() service methods that the application calls, matching the existing interceptMode / interceptBandwidth pattern. This also restores virtual-channel redirection for the application's own tuning, which had been silently lost since app.ts's internal calls no longer went through the wrapper.
  • scheduler.tsrenderTimelineNeedle guards its result but schedulerEl throws, so the now-initializing scheduler crashed on the timeline needle group that its own SVG creates. Same defect class as the bookmarks decoder checkboxes; both now use a nullable lookup.

Tests

Feature tests move onto a shared tests/host-fixture.mjs that mirrors the contract. Entries that now import a common module are bundled through the existing bundleEntry helper, as the other shared-module entries already were. New coverage for the two regressions with the most machinery behind them: scheduler self-initialization, and the receiver-position distance path.

Verification

Node is available now, so the project's own gates ran — all green:

npm run typecheck        pass
npm run lint             pass
npm test                 34/34 pass
npm run test:browser     pass  (Chromium, zero page errors)
npm run verify-generated pass  (no drift)
cargo test -p trx-frontend-http   54/54 pass

The browser smoke test is a genuine check here: it previously passed only because the scheduler never started. With initialization restored it caught the timeline-needle crash, which is fixed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz

## What broke The TypeScript migration turned `app.js` from a classic script into an ES module, so its top-level declarations stopped being shared globals. The converted feature entries kept reading them as `window` properties — `window.postPath`, `window.serverLat`, `window.authRole`, `window.setRigFrequency` and others — none of which `app.ts` publishes. Reproduced by running the shipped bookmarks bundle in headless Chromium against a stub host: ``` addBtn: "none" Add Bookmark / Select All permanently hidden (auth check saw undefined) decoderBoxes: 0 decoder checkboxes never built scopeOptions: 1 per-rig scopes missing from scope picker and move target Tune: zero network calls — "TypeError: bridge.postPath is not a function" ``` The same defect ran through the other entries: | Entry | Read as `undefined` | Effect | | --- | --- | --- | | `ais`, `aprs`, `hf-aprs` | `serverLat`, `serverLon`, `haversineKm` | empty distance on every positioned packet | | `ais`, `aprs`, `hf-aprs`, `cw`, `sat`, `vdes`, `wefax`, `wspr` | `postPath` | clear-history and decoder toggles threw | | `scheduler` | `authRole` | lazy-load path never self-initialized; Settings opened an inert scheduler | | `background-decode` | `authEnabled` | control gating fell back to role-only | | `vchan` | 15 values and services | mode/bandwidth sync, out-of-band hint, RX audio restart and the frequency field all no-opped on a virtual channel | | `ftx-family` | `fmtTime` | decode bar timestamps rendered empty | Two further instances were structural rather than a plain read: - `vchan` wrapped `window.refreshFreqDisplay`, capturing an `undefined` original exactly as it did for `setRigFrequency`, so leaving a virtual channel never restored the application's own frequency display. - `_audioChannelOverride` was a `const` nothing could assign — TypeScript had inferred it from the only remaining write site — so RX audio always subscribed to the primary channel. ## The fix `docs/frontend-architecture.md` closes the standalone `window` property list, so this extends the typed host contract rather than restoring globals. - **`plugins/host.ts` (new)** declares the `window.trx.state` / `window.trx.core` view once; feature entries import it instead of re-deriving it. `bookmarks.ts` was migrated onto it too, so there is a single declaration. - **`app.ts`** — `trx.state` publishes `authEnabled`, `jogUnit`, `rxActive`, `audioChannelOverride`, and makes `lastModeName` writable; `trx.core` publishes the tuning, RDS, WFM, jog and RX-audio services the entries need. - **`vchan.ts`** — the dead `setRigFrequency` and `refreshFreqDisplay` window wrappers become `interceptFrequency()` and `interceptFreqDisplay()` service methods that the application calls, matching the existing `interceptMode` / `interceptBandwidth` pattern. This also restores virtual-channel redirection for the application's *own* tuning, which had been silently lost since app.ts's internal calls no longer went through the wrapper. - **`scheduler.ts`** — `renderTimelineNeedle` guards its result but `schedulerEl` throws, so the now-initializing scheduler crashed on the timeline needle group that its own SVG creates. Same defect class as the bookmarks decoder checkboxes; both now use a nullable lookup. ## Tests Feature tests move onto a shared `tests/host-fixture.mjs` that mirrors the contract. Entries that now import a common module are bundled through the existing `bundleEntry` helper, as the other shared-module entries already were. New coverage for the two regressions with the most machinery behind them: scheduler self-initialization, and the receiver-position distance path. ## Verification Node is available now, so the project's own gates ran — all green: ``` npm run typecheck pass npm run lint pass npm test 34/34 pass npm run test:browser pass (Chromium, zero page errors) npm run verify-generated pass (no drift) cargo test -p trx-frontend-http 54/54 pass ``` The browser smoke test is a genuine check here: it previously passed only because the scheduler never started. With initialization restored it caught the timeline-needle crash, which is fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
sjg added 1 commit 2026-08-02 10:39:24 +02:00
[fix](trx-frontend-http): restore bookmark host contract
CI / lint (pull_request) Failing after 1s
CI / test (pull_request) Failing after 1s
CI / frontend (pull_request) Failing after 42s
CI / reuse (pull_request) Failing after 0s
23dbcac5b6
The TypeScript migration turned app.js from a classic script into an ES
module, so its top-level declarations stopped being shared globals.
bookmarks.ts was converted verbatim and kept reading them as window
properties, which app.ts no longer publishes.

Every bookmark interaction read undefined: the Add Bookmark and Select
All buttons stayed hidden because the auth check saw no authEnabled or
authRole, per-rig scopes were missing from the scope picker and the move
target, decoder checkboxes were never built, and Tune threw on
bridge.postPath before issuing a single request.

Extend the typed window.trx host contract instead of restoring globals,
as docs/frontend-architecture.md closes the standalone window property
list.  trx.state publishes authEnabled; trx.core publishes
setRigFrequency, applyLocalTunedFrequency, armOptimisticFrequency,
syncBandwidthInput, scheduleSpectrumDraw, and onDecoderRegistryReady.

Replace the vchan setRigFrequency wrapper with an interceptFrequency
service method, matching interceptMode and interceptBandwidth.  The
wrapper captured an undefined original and silently dropped every tune;
routing interception through setRigFrequency also restores virtual
channel redirection for the application's own tuning.

Read registry-built elements through bmOptionalEl, since bmEl throws and
the decoder checkboxes and decode toggle buttons are legitimately absent
until the registry arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg added 1 commit 2026-08-02 11:20:38 +02:00
[fix](trx-frontend-http): route feature bundles through the host contract
CI / lint (pull_request) Failing after 2s
CI / test (pull_request) Failing after 1s
CI / frontend (pull_request) Failing after 41s
CI / reuse (pull_request) Failing after 2s
CI / lint (push) Failing after 2s
CI / test (push) Failing after 2s
CI / frontend (push) Failing after 36s
CI / reuse (push) Failing after 1s
0d4c657b97
The bookmark fix addressed one instance of a defect the TypeScript
migration left across the feature entries.  app.js stopped being a
classic script, so its top-level declarations are no longer shared
globals, but the converted entries kept reading them as window
properties that nothing publishes.

Restore the broken behavior:

- ais, aprs, hf-aprs read serverLat, serverLon and haversineKm as
  undefined, so every positioned packet rendered an empty distance.
- ais, aprs, hf-aprs, cw, sat, vdes, wefax, wspr called an undefined
  postPath, so clear-history and decoder toggles threw.
- scheduler read authRole as undefined, so the lazy-load path never
  self-initialized and the Settings tab opened an inert scheduler.
- background-decode read authEnabled as undefined, so control gating
  fell back to role-only.
- vchan read fifteen application values and services as undefined:
  mode and bandwidth sync, the out-of-band hint, RX audio restart, and
  the frequency field all silently no-opped on a virtual channel.
- vchan wrapped window.refreshFreqDisplay, capturing an undefined
  original exactly as it did for setRigFrequency, so leaving a channel
  never restored the application's own frequency display.
- _audioChannelOverride was a const that nothing could assign, so RX
  audio always subscribed to the primary channel.
- ftx-family read fmtTime, a helper legacy ft8.js owned locally, so
  decode bar timestamps rendered empty.

Declare the contract once in plugins/host.ts and import it from the
feature entries, rather than restoring globals that
docs/frontend-architecture.md excludes.  trx.state gains jogUnit,
rxActive and audioChannelOverride, and makes lastModeName writable;
trx.core gains the tuning, RDS, WFM, jog and RX audio services the
entries need.  vchan interception moves to an interceptFreqDisplay
service method that refreshFreqDisplay calls, matching the frequency,
mode and bandwidth interception it already registers.

Reading registry-built elements through a strict lookup is the same
defect as in bookmarks: renderTimelineNeedle guards its result, but
schedulerEl throws, so the now-initializing scheduler crashed on the
timeline needle group that its own SVG creates.

Feature tests move onto a shared host fixture, and entries that now
import a common module are bundled through bundleEntry like the other
shared-module entries.  Covers scheduler self-initialization and the
distance path that the bare window reads broke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg merged commit 0d4c657b97 into main 2026-08-02 11:24:07 +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#23