Commit Graph
50 Commits
Author SHA1 Message Date
sjg 1f256cbb68 [refactor](trx-frontend-http): extract the browser test fixture
browser-smoke.mjs carried its static server inline, which made it the
only browser test that could exist: a second one would have had to copy
180 lines of routes to change a single capability flag.  The server
moves to tests/web-fixture.mjs behind startWebFixture(), with the rig's
spectrum support, bookmarks and band plan as options.

Serving a rig with a spectrum matters because that is where the layout
actually lives — the panel, the strips above it and the waterfall are
all gated on filter_controls, and the existing fixture reports a
CAT-only rig, so none of it has ever been rendered under test.

No change to what the smoke test checks.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 21:26:06 +02:00
sjg 92fbdb692c [feat](trx-frontend-http): lay the scheduler controls out in one row
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 8m12s
CI / frontend (push) Failing after 29s
CI / reuse (push) Successful in 3s
The controls were a column — release, then the step buttons, then the
status line, then the entry on air last — which read bottom-up and left
the entry that is actually transmitting furthest from the buttons that
change it.  They now run left to right: step through the entries, hand
the rig back, then the current entry behind a separator.

The separator is a pseudo-element on the current-entry block rather than
an element of its own, because that block is display-toggled whenever
fewer than two entries are active; a standalone rule would be left
hanging with nothing after it.

No ids move, so the enable/disable logic in the scheduler plugin and the
release polling in vchan bind exactly as before.  The smoke test asserts
the row's order, which is also what keeps the separator in place.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:58:31 +02:00
sjg b2fbcb318d [fix](trx-frontend-http): stop Tools lighting up on every refresh
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 8m21s
CI / frontend (push) Failing after 30s
CI / reuse (push) Successful in 3s
navigateToTab marked the Tools button by asking whether the destination
tab was displayed, which is the right question at the wrong moment: the
first route navigation runs while the card is still behind the loading
state, where every tab computes to display:none.  Refreshing or deep
linking to any page therefore lit Tools alongside the real destination,
and nothing re-evaluated it once the page appeared.

Membership of the Tools menu answers the same question without needing
anything laid out, and still reads the grouping ui-core installs rather
than a second copy of it.

The smoke fixture now serves the SPA shell for route paths the way the
server's per-tab index handlers do, so a deep link no longer 404s and
the case is testable at all; two of them are asserted.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:51:04 +02:00
sjg 44721b579c [fix](trx-frontend-http): fill the map column down to the footer
The windowed map was capped at 75% of the viewport height and at a
width-derived aspect ratio, which left a dead band under it: 69px at
1600x950, and on a 420px-wide phone a 270px map on an 800px screen.
Neither cap was doing useful work now that the stage spans the full
width, so the map fills the column down to the footer instead.

Growing into the footer needs a bound: once the column is tall enough to
push the footer below the fold, using its position would push it further
on every pass, so the bottom edge is clamped to the viewport.  Growth
then consumes the column's spare height and settles in one pass.

Also drops three mapIsFullscreen() branches in the windowed path that
could never be taken — the fullscreen case returns above them.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:50:48 +02:00
sjg 889d00007d [feat](trx-frontend-http): box the selected tab instead of underlining it
CI / lint (push) Successful in 2m23s
CI / test (push) Successful in 8m19s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 3s
The desktop strip marked the current page with a 2px underline while the
mobile bottom nav already boxed it, so one navigation model looked like
two.  The box now sits on both: a transparent 1px border on the base
reserves it, so switching pages moves no neighbours, and hover fills a
fainter version of the same shape.  Tools carries it too — that button
is marked active for the destinations the strip hides.

Dropping the mobile rule's border-bottom:none, which only existed to
cancel the old desktop underline, closes the bottom edge its active box
had been missing.  The smoke test checks all four edges.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:37:31 +02:00
sjg 6f53592041 [feat](trx-frontend-http): rework the page footer
The footer floated in space below the content with no rule to close the
page, its two clusters sat on a text baseline that left the source pill
hanging, and the status hint was a plain line of text a size larger than
the attribution beside it.

Now a hairline closes the page the way .tab-bar opens it, the clusters
centre on one line, and the attribution drops the opacity it stacked on
top of --text-muted, which had put it below a readable contrast ratio.

The status hint becomes a pill with a state dot: green when ready, amber
while a command is in flight, red on connection loss.  The colour comes
from a data-state attribute, so every hint now goes through setPowerHint
instead of assigning textContent directly.  --status-ok carries the
indicator green; .about-status-on picks it up too, which darkens it on
light themes where the old value was barely legible.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:37:12 +02:00
sjg 709939f60b [feat](trx-frontend-http): span the map across the full viewport width
The map stage broke out of the centred .card column: negative inline
margins cancel the card's centring offset and its side padding, so the
stage reaches both viewport edges at every width without hardcoding
either value.  Its rounded corners and left/right borders go with it —
edge to edge, the panel reads as a band rather than a floating card.

The browser smoke test now measures the stage against the viewport, and
checks that the full-bleed width does not push the page sideways.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 20:36:50 +02:00
sjgandClaude Opus 5 5b7dd493d4 [feat](trx-frontend-http): draw APRS symbols from the sprite sheets
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m12s
CI / frontend (pull_request) Successful in 3m2s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 7m28s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 3s
Resolve a table/code pair to a sprite cell in aprs-shared, and use it
from both the packet lists and the map markers, which had each been
printing the raw symbol character in a bordered box.

A table identifier of / or \ selects the primary or alternate sheet
directly.  Anything else is an overlay character, which the APRS spec
draws on top of the alternate symbol -- so those stack the overlay sheet
over the alternate one rather than picking a sheet.  Codes outside
0x21..0x7E have no cell and keep the old character box.

The sheet URLs stay in the stylesheet so a min-resolution query can swap
in the retina sheets; only the cell offset is computed and set inline.
Map markers share the helper through the plugin chunk, so the map stays
free of any remote symbol fetch.

Verified in a browser against the real stylesheet and sheets: /> is a
car, /_ a WX circle, /& an igate diamond, \n a red triangle, and the
overlays S> and 7# carry their character on the alternate symbol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018huL1ELyr86yVqfAabtioA
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-03 19:55:50 +02:00
sjgandClaude Opus 5 b252717f6b [test](trx-frontend-http): serve a realistic decoder registry to the smoke test
CI / lint (pull_request) Successful in 2m18s
CI / test (pull_request) Successful in 7m25s
CI / frontend (pull_request) Successful in 2m10s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m26s
CI / frontend (push) Successful in 2m12s
CI / reuse (push) Successful in 3s
The fixture answered /decoders with an empty list, which hid most of the
application from the only test that runs it in a browser.  The decoder
sub-tabs, their panels, the decode toggles and the bookmark decoder
checkboxes are all built from that registry, so the run exercised three
of thirteen sub-tabs and none of the decoder UI.  Finding this needed
route interception, because nothing in the suite could see it.

Serve eleven decoders covering the modes the real registry spans.  The
run now builds 13 sub-tabs and 11 bookmark decoder checkboxes — the same
checkboxes whose construction a recent fix changed without any test
reaching them — and still reports no runtime errors.

It also makes an existing fault observable: at 1100px the decoder
sub-tab bar hides 195px of itself with no scrollbar or fade, the same
silent truncation the top strip had.  No assertion for it here, since
that would fail until the truncation is fixed.

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-03 17:46:52 +02:00
sjgandClaude Opus 5 b409c57296 [test](trx-frontend-http): assert header geometry in the browser smoke test
CI / test (push) Successful in 7m27s
CI / frontend (push) Successful in 3m1s
CI / reuse (push) Successful in 3s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 7m24s
CI / frontend (pull_request) Successful in 2m11s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m15s
Several layout faults shipped while every gate passed, because nothing
looked at geometry: a header whose height tracked the viewport, controls
at four different heights, a tab strip that ran under the controls, and a
dropdown that opened underneath the page.

Assert the invariants behind those at four widths — the header stays one
row, the tabs do not reach the controls, the controls share a height, the
page does not scroll sideways — and that the menu renders with real
dimensions and wins a hit test at its own centre.

The overlap check measures the tabs rather than the strip: with the strip
allowed to overflow, its box shrinks while its content paints across the
controls, so the container's own rect never registers the collision.

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-03 00:18:16 +02:00
sjgandClaude Opus 5 27f2558193 [feat](trx-frontend-http): one navigation model at every width
CI / test (pull_request) Successful in 8m13s
CI / frontend (pull_request) Successful in 3m0s
CI / reuse (pull_request) Successful in 3s
CI / test (push) Successful in 7m25s
CI / lint (pull_request) Successful in 2m18s
CI / lint (push) Successful in 2m17s
CI / frontend (push) Successful in 2m9s
CI / reuse (push) Successful in 3s
Eight destinations sat flat in the tab strip with equal weight, competing
with the controls for the same row and then scrolling out of reach with
only a fade to say so.  They are not equal: Radio is where an operator
spends nearly all their time, Bookmarks, Digital modes and Map are
operating surfaces, and Statistics, Recorder, Settings and About are
occasional.

The mobile layout already grouped them exactly that way, behind its More
menu, so the application carried two navigation models.  Adopt the mobile
grouping at every width instead of adding a third: four operating tabs
plus More.  The strip no longer scrolls at any width, and the menu keeps
its bottom-sheet placement on mobile while anchoring under its button
elsewhere.

Drop the labels between 701 and 1180px so the tabs degrade to their icons
— which every tab already carries — before the strip could ever need to
hide a destination.

Rename Main to Radio: it is the receiver, not a generic first page, and
the name now says what the destination is rather than where it sits.

Freeing that width also let the style picker and theme toggle return to
the bar inline, leaving only the layout picker in the overflow menu.

Navigating to About in the browser smoke test now goes through More, as a
person would.

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 19:12:14 +02:00
sjgandClaude Opus 5 b31790ff48 [fix](trx-frontend-http): keep layout sections togglable
CI / lint (pull_request) Successful in 2m21s
CI / frontend (pull_request) Successful in 3m3s
CI / reuse (pull_request) Successful in 3s
CI / test (pull_request) Successful in 8m32s
CI / lint (push) Successful in 2m30s
CI / test (push) Successful in 8m36s
CI / frontend (push) Successful in 2m13s
CI / reuse (push) Successful in 4s
A layout seeds the collapsible sections; it should not hold them there.
applyLayout writes the disclosure state of the advanced, audio and
scheduler sections, and it runs far more often than a layout change:
render() calls applyRigList() for every SSE frame carrying `remotes`,
which calls setActiveRig() unconditionally, which re-applies the layout.

An operator who expanded a section that the selected layout collapses by
default therefore had it shut again within about a second, which read as
the section being locked by the layout — most visibly the scheduler under
Compact.

Write the section state only when the layout actually changes, or the
first time each section appears in the DOM, since the advanced controls
are constructed after the first applyLayout call.  Switching layout still
reseeds every section, so choosing a layout keeps meaning "give me these
defaults".

Verified in Chromium: with Compact selected, activating the scheduler
summary opens the section and it survives both a rig-state refresh and a
repeated applyLayout, while selecting Full still reseeds it.

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 17:46:56 +02:00
sjgandClaude Opus 5 0d4c657b97 [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
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
sjgandClaude Opus 5 23dbcac5b6 [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
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>
2026-08-02 10:15:56 +02:00
sjg a1a8d1d1d3 test: cover lazy map and rig startup flows 2026-08-01 22:34:22 +02:00
sjg ddb33b6ff3 feat: generate frontend status metadata contract 2026-08-01 22:27:35 +02:00
sjg f87289b129 fix: validate typed frontend startup in Chromium 2026-08-01 22:25:10 +02:00
sjg d929ad3bde build: consolidate typed frontend module graph 2026-08-01 21:54:00 +02:00
sjg 8aeca613aa test: add frontend browser startup smoke coverage 2026-08-01 20:19:27 +02:00
sjg 5136704826 refactor: extract typed spectrum math 2026-08-01 18:19:02 +02:00
sjg e300cc343d refactor: extract typed auto bandwidth estimator 2026-08-01 17:59:04 +02:00
sjg 1fb196a64e refactor: extract typed navigation routes 2026-08-01 17:18:41 +02:00
sjg ab3fe4bbb7 refactor: extract typed frontend formatters 2026-08-01 16:42:54 +02:00
sjg 26a167cb82 refactor: replace feature globals with typed services 2026-08-01 16:05:40 +02:00
sjg 26be675242 build: enable frontend module code splitting 2026-08-01 14:40:59 +02:00
sjg 82121491c5 refactor: convert map feature to strict TypeScript 2026-08-01 13:39:41 +02:00
sjg 0338c8b8d2 refactor: convert scheduler to typed module 2026-08-01 13:23:30 +02:00
sjg c7994347e9 refactor: convert bookmarks to typed module 2026-08-01 13:13:52 +02:00
sjg 9fef0ebc7b refactor: route all decoders through plugin runtime 2026-08-01 13:06:38 +02:00
sjg 508cf2ba87 refactor: add typed decoder plugin runtime 2026-08-01 13:02:54 +02:00
sjg 6a83e2e90a refactor: convert virtual channels to TypeScript 2026-08-01 12:57:09 +02:00
sjg fc2f55bab9 refactor: convert satellite scheduler to TypeScript 2026-08-01 12:54:03 +02:00
sjg 4fb65971e4 refactor: convert satellite view to TypeScript 2026-08-01 12:51:22 +02:00
sjg 021d31d780 refactor: share typed APRS plugin contracts 2026-08-01 12:48:33 +02:00
sjg 074c67c7b9 refactor: convert APRS plugin to TypeScript 2026-08-01 12:43:40 +02:00
sjg d93f784aad refactor: convert AIS plugin to TypeScript 2026-08-01 12:39:20 +02:00
sjg ec59908e0d refactor: convert background decode controls to TypeScript 2026-08-01 12:36:27 +02:00
sjg 7251ec276d refactor: convert WEFAX plugin to TypeScript 2026-08-01 12:33:38 +02:00
sjg 0ab0f80986 refactor: convert VDES plugin to TypeScript 2026-08-01 12:30:17 +02:00
sjg 2cbcc23fec refactor: move plugin loading into typed registry 2026-08-01 12:26:29 +02:00
sjg 968fb3ea2d refactor: type the local Leaflet AIS adapter 2026-08-01 12:23:37 +02:00
sjg 5ba3ecf59b refactor: make FT8 family a typed module 2026-08-01 12:21:13 +02:00
sjg 03bdc10b02 refactor: convert CW plugin to TypeScript 2026-08-01 12:18:51 +02:00
sjg d96624be4f refactor: convert WSPR plugin to TypeScript 2026-08-01 12:15:19 +02:00
sjg a5dccd5489 refactor: consolidate FT2 and FT4 plugins in TypeScript 2026-08-01 12:12:27 +02:00
sjg 42e5dc8604 docs: record frontend migration baseline 2026-08-01 12:09:38 +02:00
sjg 9ac6d7f82c refactor: convert WebGL renderer to TypeScript 2026-08-01 12:07:02 +02:00
sjg a58553ba66 refactor: convert decode history worker to TypeScript 2026-08-01 12:03:19 +02:00
sjg 7442437757 refactor: convert shared UI core to TypeScript 2026-08-01 12:01:07 +02:00
sjg 888f793eb8 build: add frontend TypeScript toolchain 2026-08-01 11:52:47 +02:00