<!--
SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>

SPDX-License-Identifier: GPL-2.0-or-later
-->

# TypeScript Migration Plan

> **Scope**: `src/trx-client/trx-frontend/trx-frontend-http/`
>
> **Status**: Complete (2026-08-01)

## Implementation result

The migration was completed on `feat/typescript-frontend-migration`. The
baseline and phased sections below are retained as the decision record; their
descriptions of JavaScript files and classic loading refer to the pre-migration
state.

Completion evidence:

- all first-party browser sources are strict `.ts` files, checked by separate
  DOM and Web Worker TypeScript projects with no JavaScript compatibility mode
  or suppression directives;
- `bootstrap.ts` is the single first-party HTML entry and esbuild represents
  startup order, lazy feature imports, shared hashed chunks, and the worker in
  its module graph;
- obsolete source and generated compatibility JavaScript was removed;
- Rust generates rig, status, capability, decoder, and flattened frontend
  metadata contracts into `api/generated.ts`; runtime guards validate HTTP,
  SSE, WebSocket, and worker boundaries;
- the generic embedded-asset handler serves a build-generated allowlist with
  constrained MIME types, compression, ETags, immutable caching, and no file
  system lookup;
- intentional browser host namespaces and transitional lazy-feature properties
  are documented in `docs/frontend-architecture.md`;
- CI uses locked npm dependencies, caches npm downloads rather than
  `node_modules`, runs strict type checking and linting, unit/DOM/worker tests,
  Chromium startup coverage, generated-output drift checks, and REUSE after
  generation;
- Cargo continues to consume committed generated assets without invoking Node
  or requiring network access.

The final local gate ran `npm ci`, type checking, linting, 30 frontend tests,
the Chromium smoke flow (startup, auth gate, audio controls, rig switching, map
initialization, and navigation), generated-contract and bundle verification,
workspace formatting, Clippy with warnings denied, all-target builds, workspace
tests, and REUSE 3.3 validation.

## 1. Decision

Migrating the web frontend to TypeScript is worthwhile, provided it is used to
remove implicit contracts and global coupling. Renaming JavaScript files to
`.ts` without changing their boundaries would add tooling without delivering
the main safety benefits.

The migration must be incremental. Every intermediate commit and pull request
must leave the frontend buildable and usable.

## 2. Baseline State

The frontend currently contains roughly 21,700 lines of first-party
JavaScript. Its largest components include:

- `app.js`: approximately 8,900 lines;
- `map-core.js`: approximately 3,500 lines;
- `plugins/scheduler.js`: approximately 1,500 lines;
- `plugins/bookmarks.js`: approximately 800 lines;
- `plugins/vchan.js`: approximately 560 lines.

Important characteristics of the current architecture are:

- scripts are embedded individually into the Rust binary with `include_str!`;
- the Rust HTTP server exposes an explicit route for each asset;
- plugins are loaded dynamically as classic scripts;
- script order is significant;
- modules communicate through `window.*`, `window.trx`, callbacks, and shared
  mutable state;
- the main HTML file contains the plugin loader;
- a Web Worker is loaded through a fixed URL;
- Cargo builds do not require Node.js;
- CI images contain Node.js, but CI currently runs only Rust and REUSE checks;
- Leaflet, Opus decoder, fonts, images, and other vendored assets are local.

These constraints make a big-bang rewrite unnecessarily risky.

## 3. Goals

The migration should:

1. Create checked contracts between Rust responses and browser code.
2. Replace global callbacks with explicit module interfaces.
3. Split `app.js` by responsibility.
4. Make rig, capability, decoder, scheduler, audio, and spectrum state explicit.
5. Preserve lazy loading for expensive features.
6. Add frontend type checking, linting, and automated tests to CI.
7. Keep ordinary Cargo builds independent of Node.js and network access.
8. Preserve existing browser behavior throughout the migration.

## 4. Non-Goals

The migration will not initially:

- introduce a UI framework;
- redesign the user interface;
- convert vendored JavaScript to TypeScript;
- change REST, SSE, WebSocket, or worker protocols unless required to make an
  existing contract unambiguous;
- make `build.rs` install packages or download frontend dependencies;
- convert all of `app.js` in one pull request.

## 5. Target Layout

```text
trx-frontend-http/
├── frontend/
│   ├── package.json
│   ├── package-lock.json
│   ├── tsconfig.json
│   ├── tsconfig.worker.json
│   ├── build.mjs
│   ├── src/
│   │   ├── bootstrap.ts
│   │   ├── api/
│   │   │   ├── client.ts
│   │   │   └── generated.ts
│   │   ├── core/
│   │   │   ├── dom.ts
│   │   │   ├── events.ts
│   │   │   ├── settings.ts
│   │   │   └── state.ts
│   │   ├── features/
│   │   │   ├── audio/
│   │   │   ├── bookmarks/
│   │   │   ├── map/
│   │   │   ├── navigation/
│   │   │   ├── radio/
│   │   │   ├── recorder/
│   │   │   ├── scheduler/
│   │   │   └── spectrum/
│   │   ├── decoders/
│   │   ├── workers/
│   │   └── legacy/
│   │       └── global-bridge.ts
│   └── tests/
└── assets/web/
    ├── generated/
    ├── vendor/
    ├── index.html
    ├── style.css
    └── themes.css
```

The exact feature directories may evolve, but dependencies should point from
features toward `core` and `api`, never from `core` back into features.

## 6. Tooling

Use TypeScript for type checking and esbuild for bundling. A framework-specific
development server is not needed.

Recommended compiler baseline:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM"]
  }
}
```

Worker code should use a separate configuration with `WebWorker` rather than
`DOM` globals.

The package scripts should provide at least:

```text
npm run typecheck
npm run lint
npm test
npm run build
npm run verify-generated
```

Pin dependencies in `package-lock.json`. New package and generated files must
carry or inherit valid REUSE licensing information.

## 7. Cargo and Asset Integration

Cargo must remain usable on machines without Node.js. Do not invoke `npm`
automatically from `build.rs`.

The initial integration should work as follows:

1. TypeScript and JavaScript sources live under `frontend/src`.
2. esbuild writes browser-ready output under `assets/web/generated`.
3. Generated browser output is committed to the repository.
4. Rust embeds generated output, not TypeScript source.
5. CI rebuilds the frontend and fails if committed output is stale.

During early phases, retain the existing public URLs, including:

```text
/app.js
/ui-core.js
/map-core.js
/scheduler.js
/decode-history-worker.js
```

Keeping these URLs stable avoids coupling the first migration phases to changes
in HTML loading, authentication rules, caching, or Rust routing.

After modules and code splitting are established, replace the explicit list of
Rust constants and handlers with an embedded generated-asset directory. The
server must still enforce an allowlist, correct MIME types, cache headers, and
path traversal protection.

## 8. Rust-to-TypeScript Contracts

Generate TypeScript definitions from Rust wire types instead of maintaining
parallel handwritten representations.

Initial candidates include:

- `RigState`, `RigInfo`, and `RigCapabilities`;
- the `/rigs` response;
- decoder registry entries;
- scheduler configuration and status;
- filter and spectrum state;
- recorder responses;
- SSE update payloads;
- decoded-message variants;
- WebSocket control and status messages.

A generator such as `ts-rs` can produce
`frontend/src/api/generated.ts`. Generated definitions should describe only
wire formats. UI state and view models should remain handwritten.

CI must regenerate these definitions and fail when the working tree changes.
Serde renames, tagged enums, optional fields, flattened values, and numeric
ranges must be checked explicitly during the initial generator integration.

TypeScript types do not validate data at runtime. Critical compatibility
boundaries should retain focused runtime checks for missing or malformed data,
particularly when clients and servers may run different versions.

## 9. Target Module Contracts

Plugins should eventually implement an explicit interface rather than assigning
callbacks to `window`:

```ts
interface TrxPlugin {
  readonly id: string;
  initialize(context: PluginContext): void | Promise<void>;
  dispose?(): void;
}

interface PluginContext {
  api: TrxApi;
  events: TrxEventBus;
  navigation: NavigationService;
  notifications: NotificationService;
  state: ReadonlyRadioState;
}
```

During the transition, `legacy/global-bridge.ts` may expose the minimum globals
required by unmigrated scripts. The bridge must shrink as migration progresses;
new feature code must not add new global callbacks.

State should be divided by responsibility rather than replaced by one large
typed global:

```ts
interface RadioState {
  activeRigId: string | null;
  capabilities: RigCapabilities | null;
  connection: ConnectionState;
  frequencyHz: number | null;
  mode: RigMode | null;
  bandwidthHz: number | null;
}

interface AudioState {
  rx: AudioStreamState;
  tx: AudioStreamState;
  volume: VolumeState;
}

interface DecoderState {
  registry: DecoderDescriptor[];
  status: ReadonlyMap<DecoderId, DecoderStatus>;
}
```

Commands should receive a rig ID explicitly. They must not infer their target
from mutable global selection state after an asynchronous operation begins.

## 10. Migration Phases

### Phase 0: Baseline and Measurements

- Record current asset names, sizes, and load order.
- Add a browser startup smoke test.
- Record existing global symbols and plugin callbacks.
- Confirm that generated bundles do not introduce remote runtime dependencies.

**Exit criterion:** current behavior and asset loading have an automated
baseline.

### Phase 1: Tooling Scaffold

- Add the frontend package, lockfile, TypeScript configuration, and esbuild.
- Allow existing JavaScript as build input without type checking it globally.
- Produce fixed-name outputs matching current URLs.
- Add frontend commands and CI checks.
- Document local frontend development commands.

**Exit criterion:** existing JavaScript passes through the frontend build with
no runtime changes, and CI detects stale generated output.

### Phase 2: Generated API Types

- Add Rust-to-TypeScript type generation.
- Generate the first status, rig, capability, and decoder contracts.
- Introduce a typed fetch/post client and typed SSE decoding boundary.
- Keep narrow runtime guards at compatibility boundaries.

**Exit criterion:** new API consumers cannot use untyped response objects.

### Phase 3: Core Browser Services

Convert or create:

- DOM lookup helpers;
- notifications and confirmations;
- settings and per-rig preferences;
- navigation;
- the event bus;
- application state stores;
- plugin registry and loader.

Convert `ui-core.js` as the first strict TypeScript entry.

**Exit criterion:** shared UI behavior is TypeScript, tested, and does not add
new globals.

### Phase 4: Independent Leaf Modules

Convert lower-coupling code first:

1. WebGL renderer;
2. decode-history worker;
3. screenshot support;
4. FT2 and FT4;
5. WSPR;
6. CW and other decoder views.

Workers must be separate build entries.

**Exit criterion:** each converted module has typed inputs, outputs, and tests.

### Phase 5: Plugin Loading

- Move the inline loader out of `index.html`.
- Replace classic-script injection with typed dynamic imports.
- Register plugins through `TrxPlugin`.
- Preserve lazy loading by tab and feature.
- Remove the corresponding global callbacks after each plugin migrates.

**Exit criterion:** plugin dependencies and loading order are represented by the
module graph rather than implicit script order.

### Phase 6: Split the Main Application

Extract `app.js` by responsibility:

1. authentication and API transport;
2. rig enumeration and switching;
3. radio commands and capabilities;
4. audio streaming;
5. spectrum state and rendering;
6. decode history;
7. recorder;
8. keyboard shortcuts;
9. application bootstrap.

Do not split by arbitrary line ranges. Each extraction must establish an
explicit interface and remove the corresponding globals.

**Exit criterion:** the bootstrap file composes services and features but does
not contain their implementations.

### Phase 7: High-Coupling Features

Convert the remaining large features after the core contracts are stable:

- scheduler and satellite scheduler;
- bookmarks;
- virtual channels;
- map and map-backed statistics;
- remaining decoder plugins.

**Exit criterion:** no first-party classic scripts or untyped plugin callbacks
remain.

### Phase 8: Asset-Server Consolidation

- Enable code splitting and hashed chunks.
- Embed the generated asset directory in Rust.
- Serve generated assets through a generic, allowlisted handler.
- Add appropriate immutable caching for hashed output.
- Retain stable handling for HTML and version metadata.
- Remove obsolete fixed-asset constants and handlers.

**Exit criterion:** Rust no longer needs a source edit for every generated
frontend chunk.

### Phase 9: Strictness and Cleanup

- Remove `allowJs`.
- Remove the legacy global bridge.
- Enable all selected strict compiler and lint rules.
- Remove obsolete generated compatibility bundles.
- Update architecture and contributor documentation.

**Exit criterion:** all first-party frontend source is strict TypeScript and the
browser runtime exposes only intentionally documented globals.

## 11. Testing Strategy

Frontend CI should run:

```sh
npm ci
npm run typecheck
npm run lint
npm test
npm run build
git diff --exit-code -- assets/web/generated frontend/src/api/generated.ts
```

Testing should cover four layers:

1. **Pure unit tests** for bandwidth calculations, formatting, state changes,
   capability decisions, and message routing.
2. **DOM component tests** for dialogs, tabs, workspace selection, rig
   switching, and collapsible control sections.
3. **Worker tests** for decode-history pruning, batching, and message formats.
4. **Browser smoke tests** for startup, authentication gating, plugin loading,
   navigation, rig switching, audio controls, and map initialization.

The existing dependency-free `ui-core` test can remain during the scaffold
phase. It should be moved to the standard TypeScript test runner once that
runner is established.

## 12. CI Changes

Add a dedicated frontend job rather than hiding frontend work inside the Cargo
jobs. The job should:

- use the Node.js version provided by the runner image;
- run with `npm ci`, never a mutable install;
- cache the npm download cache, not `node_modules`;
- type-check, lint, test, and build;
- verify generated artifacts and Rust-generated types are current;
- run REUSE validation after generated files are produced.

Rust lint and test jobs should continue to consume committed generated assets.

## 13. Pull Request Strategy

Use small, independently reversible pull requests. A recommended sequence is:

1. tooling and unchanged JavaScript build;
2. generated Rust wire types and typed API client;
3. `ui-core` conversion;
4. worker and WebGL conversion;
5. decoder plugin conversions in small groups;
6. plugin registry and dynamic imports;
7. one `app.js` responsibility per PR;
8. scheduler, bookmarks, and map conversions;
9. generic embedded asset serving;
10. removal of JavaScript compatibility mode.

Every PR should include:

- runtime behavior preserved or intentionally documented;
- frontend type checking and tests;
- strict Rust formatting and Clippy;
- generated-artifact drift verification;
- no newly introduced undocumented `window` globals;
- no remote runtime asset dependency.

## 14. Risks and Mitigations

### Superficial Conversion

**Risk:** globals receive declarations but remain coupled and mutable.

**Mitigation:** require every converted module to expose explicit inputs and
outputs and remove at least the globals it replaces.

### Cargo Becomes Dependent on Node

**Risk:** builds fail on systems without Node or network access.

**Mitigation:** commit deterministic generated output and keep npm outside
`build.rs`.

### Script-Order Regressions

**Risk:** switching to modules changes execution timing and scope.

**Mitigation:** retain current public entries initially, then replace script
loading only after the plugin registry is in place.

### Unreviewable `app.js` Rewrite

**Risk:** a large conversion is difficult to review, test, or bisect.

**Mitigation:** extract one responsibility at a time and keep bootstrap working
after every extraction.

### Rust and Browser Contracts Drift

**Risk:** TypeScript compiles against stale wire definitions.

**Mitigation:** generate types from Rust and enforce a clean working tree after
generation in CI.

### Generated Asset Noise

**Risk:** committed bundles make reviews noisy.

**Mitigation:** separate source and generated commits when useful, include
source maps, and require deterministic output.

### Bundle or Startup Regression

**Risk:** bundling loads too much code eagerly.

**Mitigation:** record the current baseline, preserve feature-level lazy loads,
and track entry/chunk sizes in CI.

## 15. Initial Proof of Concept

The first implementation PR should be deliberately limited to:

1. adding the frontend package and locked toolchain;
2. building existing JavaScript to fixed-name generated output;
3. adding frontend CI and drift checks;
4. generating initial rig/status TypeScript definitions;
5. introducing the typed API client;
6. converting `ui-core.js` to strict TypeScript;
7. preserving every existing asset URL and observable behavior.

This proof of concept is the decision gate for the remainder of the migration.
If it materially improves contract safety without making Cargo development
unwieldy, continue with the phased plan. If it does not, the repository can
retain the tooling and the converted core without committing to a full rewrite.

## 16. Completion Criteria

The migration is complete when:

- all first-party browser source is strict TypeScript;
- vendor files remain isolated and declared through narrow type adapters;
- Rust wire types generate their browser contracts;
- no feature depends on accidental script ordering;
- no undocumented mutable `window` callbacks remain;
- `app.js` has been replaced by a small typed bootstrap and feature modules;
- frontend type checking, linting, unit tests, component tests, browser smoke
  tests, and generated-output checks run in CI;
- Cargo builds remain possible without Node.js or network access;
- production assets remain local, embedded, cacheable, and reproducible.
