Files
trx-rs/docs/frontend-architecture.md
T
sjgandClaude Opus 5 0d4c657b97
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
[fix](trx-frontend-http): route feature bundles through the host contract
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>
2026-08-02 11:20:17 +02:00

4.5 KiB

Frontend architecture

The HTTP frontend is a strict TypeScript application built with esbuild. Rust embeds deterministic JavaScript output from assets/web/generated; Cargo does not run Node.js or contact the network.

Runtime graph

frontend/src/bootstrap.ts is the only first-party script referenced by the HTML document. Its imports establish startup order for WebGL support, shared UI services, decoder dispatch, the local Leaflet AIS adapter, and the application coordinator. Leaflet and the Opus decoder remain isolated vendored scripts.

Feature entries under frontend/src/plugins are ESM bundles. The typed plugin loader imports them by feature group and keeps expensive map, scheduling, and decoder behavior lazy. Shared code is emitted as content-hashed chunks. The decode-history worker is an independent entry compiled against Web Worker globals.

Dependencies point from application and feature code toward core and api. api/generated.ts contains Rust wire formats; api/client.ts and focused parsers validate untrusted HTTP, SSE, WebSocket, and worker data before it is used as typed application state.

Browser host contract

Separate lazy bundles cannot share module instances with the stable application entry, so they use three intentional host namespaces:

Global Purpose Mutation policy
window.trx Application state, core services, and feature registrations The root is frozen; lazy features may register only their documented modules.* service.
window.trxPluginRuntime Typed decoder registration and message dispatch Runtime object is installed once; plugins register lifecycle handlers through its API.
window.trxUi Notifications, confirmations, tab accessibility, and control presentation Installed once by ui-core.ts; consumers call methods but do not replace them.

frontend/src/plugins/host.ts declares the typed view of window.trx.state and window.trx.core that feature bundles consume. Feature entries import it instead of re-deriving the contract, so a service that moves out of the application entry is added in one place. Application state that a feature needs belongs in trx.state, and shared behavior belongs in trx.core; a feature that has to intercept application behavior registers a modules.* method the application calls, as the virtual-channel entry does for tuning, mode, bandwidth, and frequency display.

The WebGL adapter exposes createTrxWebGlRenderer, trxParseCssColor, trxHslToRgba, and trxClearCssColorCache for the application bundle. Leaflet adds L.TrxAisTrackSymbol and L.trxAisTrackSymbol to the vendored Leaflet namespace.

The following transitional properties are explicitly part of the lazy-feature host contract and are declared in app.ts: lastSpectrumData, lastFreqHz, currentBandwidthHz, ft8BaseHz, getDecodeHistoryRetentionMs, applyDecodeHistoryRetention, getDecodeRigMeta, renderRdsOverlays, buildAisVesselUrl, trxScheduleUiFrameJob, takeSchedulerControlForDecoderDisable, navigateToTab, _syncRecorderState, and refreshRdsUi. Optional callbacks owned by lazy features are refreshCwTonePicker, updateFt8RfDisplay, clearSatPredictionDom, syncWefaxToggle, updateAisBar, updateVdesBar, updateAprsBar, updateFt8Bar, updateSatLiveState, applyCwAutoUi, and applyCwAutoUiFromServer.

This list is closed: new standalone mutable window properties are not an accepted integration mechanism. Extend an existing typed service or introduce an imported interface instead. Removing transitional properties as feature boundaries become directly importable remains preferable.

Build and verification

The generated directory is removed before every build, preventing orphaned compatibility bundles. Stable feature entry names are allowlisted by the Rust asset manifest; shared chunks use content hashes. The generic Rust handler rejects unknown names and unsupported MIME types and serves embedded assets with compression, ETags, and immutable caching.

CI installs from package-lock.json, caches only npm downloads, type-checks the window and worker environments separately, lints, runs unit and DOM tests, starts the application in Chromium, regenerates Rust contracts and bundles, checks for drift, and runs REUSE validation after generation.

See frontend/src/README.md for local commands and docs/ts-migration-plan.md for the migration decisions and completion gates.