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.
## 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
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>
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 main2026-08-02 11:24:07 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What broke
The TypeScript migration turned
app.jsfrom a classic script into an ES module, so its top-level declarations stopped being shared globals. The converted feature entries kept reading them aswindowproperties —window.postPath,window.serverLat,window.authRole,window.setRigFrequencyand others — none of whichapp.tspublishes.Reproduced by running the shipped bookmarks bundle in headless Chromium against a stub host:
The same defect ran through the other entries:
undefinedais,aprs,hf-aprsserverLat,serverLon,haversineKmais,aprs,hf-aprs,cw,sat,vdes,wefax,wsprpostPathschedulerauthRolebackground-decodeauthEnabledvchanftx-familyfmtTimeTwo further instances were structural rather than a plain read:
vchanwrappedwindow.refreshFreqDisplay, capturing anundefinedoriginal exactly as it did forsetRigFrequency, so leaving a virtual channel never restored the application's own frequency display._audioChannelOverridewas aconstnothing 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.mdcloses the standalonewindowproperty list, so this extends the typed host contract rather than restoring globals.plugins/host.ts(new) declares thewindow.trx.state/window.trx.coreview once; feature entries import it instead of re-deriving it.bookmarks.tswas migrated onto it too, so there is a single declaration.app.ts—trx.statepublishesauthEnabled,jogUnit,rxActive,audioChannelOverride, and makeslastModeNamewritable;trx.corepublishes the tuning, RDS, WFM, jog and RX-audio services the entries need.vchan.ts— the deadsetRigFrequencyandrefreshFreqDisplaywindow wrappers becomeinterceptFrequency()andinterceptFreqDisplay()service methods that the application calls, matching the existinginterceptMode/interceptBandwidthpattern. 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—renderTimelineNeedleguards its result butschedulerElthrows, 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.mjsthat mirrors the contract. Entries that now import a common module are bundled through the existingbundleEntryhelper, 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:
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