Commit Graph

380 Commits

Author SHA1 Message Date
sjg 9b8ea37a6e [feat](trx-frontend-http): add FT4 as map source
- Add ft4 to DEFAULT_MAP_SOURCE_FILTER (visible by default)
- Add ft4 hue to locatorThemeHues (peakHue + 30° offset from FT8)
- Propagate ft4 through locatorSourceLabel, locatorFilterColor,
  locatorHueForEntry, isLocatorOverlay, applyMapFilter,
  ensureDecodeLocatorMarker, pruneLocatorEntry, rebuildDecodeContactPaths,
  clearMapMarkersByType, markerSearchText, navigateToMapLocator, and
  the source items chip row
- ft8MapAddLocator now maps type="ft4" to markerType="ft4" so FT4
  locators get their own distinct marker colour and filter chip
- ft4.js: pass "ft4" (not "ft8") to ft8MapAddLocator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 18:57:58 +01:00
sjg 8eae376c56 [feat](trx-rs): add FT4 decoder support
Reuse the existing ft8_lib C library (FTX_PROTOCOL_FT4) and FT8
decoder infrastructure to add FT4 decoding across the full stack.

Changes:
- trx-ft8: add protocol param to ft8_decoder_create; add Ft8Decoder::new_ft4()
- trx-core: DecodedMessage::Ft4 variant, AUDIO_MSG_FT4_DECODE (0x14),
  ft4_decode_enabled/ft4_decode_reset_seq state, SetFt4DecodeEnabled/
  ResetFt4Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft4, run_ft4_decoder (7.5s slots via
  now*2/15), run_background_ft4_decoder, history push/replay, decoder
  task spawn
- trx-frontend-http: ft4_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft4.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft4 field
- web: ft4.js plugin (7.5s period timer, reuses FT8 CSS/map infra),
  FT4 subtab in index.html, app.js dispatch (onServerFt4/Batch,
  restoreFt4History), decode-history-worker HISTORY_GROUP_KEYS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 18:50:08 +01:00
sjg a1b9b7f762 [fix](trx-frontend): keep QSO summary visible
Decouple the longest-QSO summary from the contact path render toggle.
Keep the summary filtered by current map visibility while the toggle only
controls whether path overlays are drawn.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 18:20:15 +01:00
sjg 73b1d5618d [fix](trx-frontend-http): group decode history by decoder
Serve grouped decode history payloads and restore each decoder through
explicit history restore hooks instead of replaying a mixed message stream.

This reduces replay overhead further by removing type regrouping and keeping
history restoration on decoder-specific bulk paths.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 18:11:09 +01:00
sjg cba3751f0e [fix](trx-frontend-http): move history decode off main thread
Serve a dedicated decode-history worker and move compressed history fetch
and CBOR parsing into that worker.

The main thread now drains ready-made decode batches within a frame budget,
which further reduces UI disruption during large history restores.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 18:04:11 +01:00
sjg abae110ecd [fix](trx-frontend-http): batch decode history replay
Replay decode history in decoder-specific batches instead of feeding every
message through the single-message path.

This reduces per-message array churn and UI scheduling during large history
loads while keeping the existing live decode behavior unchanged.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 17:56:28 +01:00
sjg 4114e0b9fa [fix](trx-frontend-http): speed up decode history replay
Serve decode history as gzipped CBOR and decode it in the frontend.
Defer map materialization until replay completes to avoid replay-time stutter,
and include the pending longest-QSO style adjustment.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 17:47:49 +01:00
sjg a924902074 [feat](trx-client): add configurable decode history retention
Add a default decode history retention setting in minutes with per-rig overrides, resolve the active rig retention in the HTTP frontend runtime, and apply that retention consistently across backend decode history buffers and frontend decoder views. This removes fixed APRS, HF APRS, AIS, VDES, FT8, and WSPR browser-side history caps in favor of time-based pruning, and includes the pending longest-QSO card style reset.

Verification: cargo test -p trx-client config
Verification: cargo test -p trx-frontend-http --no-run
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js
Verification: git diff --check -- src/trx-client/src/config.rs src/trx-client/src/main.rs src/trx-client/trx-frontend/src/lib.rs src/trx-client/trx-frontend/trx-frontend-http/src/api.rs src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 17:18:09 +01:00
sjg 86768c8e7f [fix](trx-frontend-http): focus longest qso map paths
Let users click longest-QSO cards to isolate a single contact path on the map and click again to restore all visible contact paths. Also remove the extra inner panel styling from decode map tooltips so the popup renders as a single container.

Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 17:01:42 +01:00
sjg d5c3283b37 [fix](trx-frontend-http): restore map history window state
Keep map history data cached when the history window is reduced so older APRS, AIS, VDES, FT8, and WSPR items can be shown again when the user expands the window, and add a global decode-history replay overlay with progress updates across the UI. Also update the longest QSO summary to render bidirectional contacts with <-> labels.

Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 16:58:05 +01:00
sjg e2517ec184 [feat](trx-frontend-http): add longest map qso summary
Add a map summary section below the map that lists the five longest directed FT8 and WSPR contacts in the current view, including distance, band, age, and locator details.

Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 16:46:06 +01:00
sjg 3b52fdf232 [fix](trx-frontend-http): add map history limit filter
Add a map filter-panel history picker with 15 minute through 24 hour retention options and prune dynamic APRS, AIS, VDES, FT8, and WSPR overlays to the selected age window.

Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 16:41:14 +01:00
sjg c50f716390 [fix](trx-frontend-http): follow active channel in page title
Use the currently tuned virtual channel for the website title\ninstead of always showing channel 0 metadata.\n\nVerification: node --check assets/web/app.js\nVerification: node --check assets/web/plugins/vchan.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 13:21:04 +01:00
sjg c4d1313625 [fix](trx-frontend-http): smooth decode history loading
Reduce main-thread stalls while decode history replays.\n\nCoalesce decoder list redraws and map maintenance so spectrum\nand controls stay responsive during history import.\n\nVerification: node --check on modified frontend JS files.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:56:02 +01:00
sjg 28d5494c3b [fix](trx-frontend-http): correct map auth and overlay behavior
Treat the SPA entry routes as public so direct requests to /map,\n/decoders, /settings, and /about return the app shell and let\nthe frontend show the login screen instead of a 403.\n\nMove the map filter overlay to the bottom-right corner and color\ndecode contact paths by their decoded band so they match the band\nlegend and locator overlays.\n\nVerified with cargo test -p trx-frontend-http.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:46:08 +01:00
sjg b9744ef8fd [feat](trx-frontend-http): color radio paths by band or mode
Radio paths and decode contact paths now use the same color as the
marker they belong to, respecting the active filter mode:
- Band mode: color follows the band (golden-angle HSL hue)
- Mode/source mode: color follows the source type (FT8/WSPR/bookmark)

APRS, AIS, and VDES paths use their fixed source colors unchanged.
Decode contact paths sync color when the filter mode is switched.

CSS stroke/stroke-opacity removed from path classes so Leaflet's
color option takes effect; dasharray and flow animation are retained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:31:56 +01:00
sjg 0b0e86f496 [fix](trx-frontend-http): sync map legend with filter mode
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 19:38:34 +01:00
sjg fb83e3cade [feat](trx-frontend-http): default map filtering to band
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 16:31:40 +01:00
sjg e4487d9037 [feat](trx-frontend-http): add map filter panel toggle
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 14:29:29 +01:00
sjg c014f5cdc2 [fix](trx-frontend-http): refresh map sources on live APRS
Rebuild the visible-source chips when live APRS, AIS, or VDES markers are first added so the map filter list updates without a page refresh.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 07:24:09 +01:00
sjg b679ff0282 [feat](trx-frontend-http): refine map and scheduler controls
Add separate map path toggles, move scheduler handoff into the channels row, and show a live countdown to the next scheduler cycle.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 00:52:00 +01:00
sjg 4cca188d9f [feat](trx-frontend-http): improve scheduler and decode map controls
Remove settings rig pickers, restore the last scheduler cycle on release, fix FT8 locator role parsing, and add toggleable decode contact paths on the map.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 00:45:12 +01:00
sjg 963d527dfe [feat](trx-frontend-http): add direct tab routes
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 23:54:25 +01:00
sjg a91a1868d8 [fix](trx-frontend-http): refresh background decode settings UI
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:48:24 +01:00
sjg 2462f1dd47 [feat](trx-frontend-http): add background decode settings
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:42:50 +01:00
sjg fc24dc37ed [feat](trx-rs): add settings tab and virtual audio plan
Move Scheduler under a new Settings tab in the HTTP frontend.
Add the virtual-channel audio implementation plan document.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 21:55:54 +01:00
sjg 6d18f5e1d4 [fix](trx-frontend): enable BW drag handles on vchans
Use the active channel frequency for spectrum bandwidth edge hit-testing.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 20:49:43 +01:00
sjg e8326b4822 [fix](trx-frontend): align spectrum RDS overlays vertically
Keep virtual-channel RDS overlays on a shared vertical axis.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 20:44:21 +01:00
sjg add0a93424 [feat](trx-frontend-http): frequency-ordered z-index and hover-to-front for RDS overlay layers
Each RDS PS overlay item (position: absolute within the shared #rds-ps-overlay
container) now receives a z-index derived from its channel frequency: items are
sorted by freq_hz ascending so higher-frequency layers sit on top of
lower-frequency ones by default.

Hovering any layer temporarily assigns it the maximum z-index (entry count + 10)
to bring it to the front; mouseleave restores the frequency-derived default
stored in data-default-z.

Also reverts the incorrectly applied vchan picker layer changes from the
previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 20:12:23 +01:00
sjg 93ff35a824 Add per-channel RDS overlays for WFM vchans 2026-03-11 22:39:02 +01:00
sjg daa0631b35 [feat](trx-client): support virtual channel bandwidth control
Add client-side command plumbing, HTTP endpoint handling, and frontend interception so bandwidth changes are applied per active virtual channel and survive reconnects.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 21:41:32 +01:00
sjg 19ab3b3931 [feat](trx-rs): remove MARINE mode (superseded by virtual channels)
MARINE was a composite mode that ran both AIS and VDES decoders
simultaneously. It is now fully replaced by allocating two virtual
channels — one tuned to the AIS frequencies and one to VDES — each
decoded independently.

- trx-core/state: remove RigMode::MARINE variant
- trx-protocol/codec: remove MARINE parse/serialize
- trx-backend-ft817: remove MARINE from unsupported-mode guard
- trx-backend-ft450d: remove MARINE from FM CAT code mapping
- trx-backend-soapysdr: remove MARINE from bandwidth table, supported
  modes list, AIS channel activity check, parse_rig_mode, vchan_impl
  bandwidth table, demod selection, dsp/channel bandwidth / sample-rate
  / IQ-tap guards
- trx-server/audio: remove MARINE from AIS and VDES decoder activation
- trx-server/rig_task: remove MARINE from audio-streaming mode list
- trx-server/main: remove MARINE from bandwidth table, mode parser,
  VDES channel subscription match
- app.js: remove isMarineMode(), MARINE entry in MODE_BW_SPECS, MARINE
  bandwidth specs block in visibleBandwidthSpecs(), MARINE from
  decoder status mode lists, MARINE BW-edge drag guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 20:58:20 +01:00
sjg 6131d7a1d6 [feat](trx-rs): per-virtual-channel audio streaming
Add end-to-end audio routing for virtual DSP channels:

Server (trx-server):
- New wire protocol: AUDIO_MSG_RX_FRAME_CH (0x0b), VCHAN_ALLOCATED (0x0c),
  VCHAN_SUB (0x0d), VCHAN_UNSUB (0x0e), VCHAN_FREQ (0x0f), VCHAN_MODE (0x10),
  VCHAN_REMOVE (0x11) frame types in trx-core audio.rs
- Add frame helpers: write_vchan_uuid_msg, write_vchan_audio_frame,
  parse_vchan_audio_frame, parse_vchan_uuid_msg
- Add ensure_channel_pcm() to VirtualChannelManager trait; implement in
  SdrVirtualChannelManager with create-or-subscribe semantics using client UUID
- Extend audio.rs handle_audio_client: VChanCmd dispatcher, per-channel Opus
  encoder tasks, VCHAN_SUB/UNSUB/FREQ/MODE/REMOVE reader loop handlers
- Thread vchan_manager through run_audio_listener / spawn_rig_audio_stack

Client (trx-client):
- Add VChanAudioCmd enum to trx-frontend; add vchan_audio and vchan_audio_cmd
  fields to FrontendRuntimeContext
- Extend audio_client: demux AUDIO_MSG_RX_FRAME_CH to per-channel broadcasters,
  handle VCHAN_ALLOCATED; forward VChanAudioCmd over TCP write loop
- Wire vchan_cmd_tx/rx channel in main.rs; pass vchan_audio map to audio_client
- ClientChannelManager.set_audio_cmd() / send_audio_cmd(): dispatch
  Subscribe/Remove/SetFreq/SetMode on allocate/delete/freq/mode operations
- Wire audio_cmd sender in server.rs serve() after creating vchan_mgr

HTTP frontend:
- /audio?channel_id=<uuid>: route WebSocket to per-channel Opus broadcaster
- vchan.js: vchanReconnectAudio() stops/restarts RX audio on channel switch;
  _audioChannelOverride in app.js selects primary vs virtual WS endpoint
- app.js: _audioChannelOverride variable; startRxAudio appends channel param

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 08:06:18 +01:00
sjg 28036ab589 [feat](trx-frontend-http): sync mode picker to active virtual channel
- Add vchanSyncModeDisplay() in vchan.js; called from vchanSyncAccentUI()
  and vchanSubscribe() so the mode picker always reflects the active
  virtual channel's mode on switch and on channel-list refresh
- Guard the rig-state mode picker update in render() so it is skipped
  when vchanIsOnVirtual() is true, preventing primary-channel mode from
  overwriting the virtual channel selection

Note: per-channel audio and decoder output require server-side protocol
changes (separate Opus streams per virtual channel) and are not yet
implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:37:52 +01:00
sjg 3d284abab6 [fix](trx-frontend-http): center BW overlay on active virtual channel freq
When subscribed to a non-primary virtual channel the bandwidth overlay
was still anchored to lastFreqHz (channel 0).  Resolve the effective
center from the active vchan when vchanIsOnVirtual() is true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:30:24 +01:00
sjg 4bb7248257 [feat](trx-frontend-http): draw virtual channel markers on spectrum + OOB error
- Draw sky-blue dashed/solid lines on spectrum overlay for each vchan
- Active virtual channel gets a solid line; inactive ones are dashed
- Validate freq against SDR capture window in vchanSetChannelFreq and
  show a showHint error when tuning out of bandwidth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:27:51 +01:00
sjg cef1741e40 [feat](trx-frontend-http): intercept freq/mode changes for virtual channels
When on a non-primary (virtual) channel, redirect freq and mode changes
to the channel metadata API instead of the server:

- vchan.js: add vchanIsOnVirtual(), vchanSetChannelFreq/Mode(); expose
  window.vchanInterceptMode() hook; wrap window.setRigFrequency so all
  callers (jog, freq input, bookmarks, spectrum click) are automatically
  redirected without modification
- app.js: check vchanInterceptMode() in applyModeFromPicker() before
  posting /set_mode
- bookmarks.js: check vchanInterceptMode() for mode in bmApply();
  setRigFrequency() redirect is automatic via the vchan.js wrapper;
  bandwidth and decoder toggles still apply regardless of channel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:08:36 +01:00
sjg e5aa74a1b6 [feat](trx-frontend-http): virtual channel manager and picker UI
Add client-side virtual channel support (Phase 1 — metadata only):

- vchan.rs: ClientChannelManager keyed by rig_id; tracks per-session
  channel subscriptions and broadcasts list changes via change_tx
- server.rs: instantiate Arc<ClientChannelManager> and expose as app_data
- api.rs: wire ClientChannelManager into /events SSE (session UUID,
  init_rig, update_primary, channel change stream, session cleanup on
  disconnect); add channel CRUD routes:
    GET/POST /channels/{rig_id}
    DELETE   /channels/{rig_id}/{channel_id}
    POST     /channels/{rig_id}/{channel_id}/subscribe
    PUT      /channels/{rig_id}/{channel_id}/freq|mode
- auth.rs: classify /channels/ prefix as Read access
- plugins/vchan.js: channel picker with +/× buttons, subscribe on click,
  SDR-only (shown when filter_controls capability is set)
- app.js: handle SSE `session` and `channels` events, call
  vchanApplyCapabilities from applyCapabilities
- index.html: #vchan-row div + <script src="/vchan.js">
- style.css: .vchan-picker pill styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:00:22 +01:00
sjg c9d204ad1c [fix](trx-frontend-http): populate scheduler rig list after SSE delivers rig data
initScheduler() runs before the first SSE event, so lastRigIds is empty.
Now applyRigList() calls reloadSchedulerRigSelect() whenever the rig list
updates, and renderSchedulerRigSelect() loads the config for the first rig
if currentRigId was previously unset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:22:49 +01:00
sjg 6874055b1c [feat](trx-frontend-http): add Background Decoding Scheduler
Implements a scheduler that retunes the rig automatically when no SSE
clients are connected.  Two modes are supported:

- Grayline: tunes to per-period bookmarks (dawn/day/dusk/night) based on
  an inline NOAA solar algorithm given station lat/lon.
- Time Span: tunes to bookmarks within user-defined UTC windows; midnight-
  spanning intervals supported.

Backend:
- SchedulerStore (PickleDB, sch:{rig_id} keys) in scheduler.rs
- spawn_scheduler_task polls every 30 s, checks context.sse_clients == 0,
  sends SetFreq + SetMode via RigRequest with rig_id_override
- HTTP API: GET/PUT/DELETE /scheduler/{rig_id}, GET …/status
- sse_clients Arc<AtomicUsize> added to FrontendRuntimeContext and shared
  with the SSE counter in build_server (single source of truth)
- /scheduler/ added to Read auth routes (write requires Control)

Frontend:
- Scheduler tab (clock icon, 6th position) with Grayline/TimeSpan UI
- scheduler.js plugin: loads config + bookmarks, live status polling
  every 15 s, write controls hidden for Rx-role users
- CSS .sch-* component styles added to style.css
- SCHEDULER.md design document at repo root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:20:42 +01:00
sjg e1fe1980ea [fix](trx-frontend-http): fix decode history invisible on first load
Two root causes:

1. /decode/history was classified as Control in the auth router (not listed
   in Read routes), so it returned 401/403 when auth is enabled and the
   user had no session or rx-only role. Add it to the Read route list.

2. connectDecode() was called from window.load unconditionally, before the
   auth flow completed. On first load with auth enabled the session cookie
   doesn't exist yet, so the history fetch fails silently. Move the call to
   be alongside connect() in initializeApp(), login, and guest handlers so
   it always runs with valid auth context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:52:24 +01:00
sjg d862d953e1 [fix](trx-frontend-http): flush live decode buffer only after history drain completes
drainDecodeHistory() chunks work via setTimeout but flushLiveBuffer() was
called synchronously right after starting the drain, so live messages could
interleave with in-progress history chunks. Pass flushLiveBuffer as an
onDone callback so live messages are only dispatched once all history chunks
have been processed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:40:08 +01:00
sjg 81515d921e [fix](trx-frontend-http): AIS map markers use scheme lead color (--accent-green)
Use --accent-green (the primary/lead accent color) for AIS vessel markers and
tracks instead of a hardcoded or red-based color, so they match the active
buttons and other prominent UI elements for every color scheme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:37:15 +01:00
sjg 6a755ef2b4 [fix](trx-frontend-http): use dedicated --ais-accent color for map markers
Replace --accent-red with a new --ais-accent CSS variable (default #00aacc
cyan-blue) so AIS vessel markers and track lines are visually distinct from
other UI elements regardless of theme. Light theme uses a slightly darker
#0088aa for readability on the map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:34:19 +01:00
sjg 4740b38ad4 [fix](trx-frontend-http): AIS map markers and tracks follow theme accent color
Read --accent-red CSS variable at draw time so markers, track lines, and
TrackSymbol icons automatically match the active color scheme. Add
refreshAisMarkerColors() called on theme toggle and style picker changes
to repaint existing markers without a page reload. Also buffer live SSE
decode messages until the /decode/history fetch settles to eliminate the
history-appears-after-reload race condition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:24:34 +01:00
sjg 00fc1dbdfb [fix](trx-frontend-http): increase map height in normal view
Raise the viewport cap from 60% to 75% of window height and relax the
aspect-ratio divisor from 1.9 to 1.55, giving the map more vertical
space without requiring fullscreen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:03:49 +01:00
sjg 9a398f3754 [fix](trx-frontend-http): map fullscreen fallback for mobile Safari
Mobile Safari (iOS) blocks requestFullscreen() on non-video elements,
so the Fullscreen button silently did nothing.

Add a CSS-based fake fullscreen path:
- mapEnterFakeFullscreen() adds .map-fake-fullscreen to #map-stage
  (position:fixed; inset:0; z-index:9000; height:100dvh) and
  map-fake-fullscreen-active to <body> (overflow:hidden).
- toggleMapFullscreen() tries native fullscreen first; catches the
  thrown NotAllowedError (or any other error) and falls back to the
  CSS path. Also handles the case where requestFullscreen is absent.
- mapIsFullscreen() checks for the CSS class in addition to the
  native fullscreen element references.
- mapExitFakeFullscreen() removes both classes on exit.
- Escape key exits CSS fake fullscreen (native handles its own Escape).
- sizeAprsMapToViewport() uses window.innerHeight for the fake path
  since clientHeight may not reflect fixed layout synchronously.
- sizeAprsMapToViewport() is called via requestAnimationFrame after
  toggling so layout is settled before the Leaflet invalidateSize().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:01:48 +01:00
sjg 2e8f025b19 [fix](trx-frontend-http): fix spectrum center-shift arrow behavior
Two bugs fixed:

1. Wrong vertical position of shift buttons and bookmark side panels.
   top: calc((--spectrum-plot-height - --overview-plot-height) / 2)
   evaluates to 0 when both vars are equal (default 160 px), so
   translateY(-50%) placed the buttons at the top edge of .spectrum-wrap
   instead of mid-canvas. Changed to calc(--spectrum-plot-height / 2).

2. Rapid clicks on the arrows did not accumulate: each call read
   lastSpectrumData.center_hz which is only updated when the server
   sends a new spectrum frame. Added spectrumCenterPendingHz to track
   the optimistic center immediately on click; reset when the server
   confirms a frame near the expected position.

Also hide .spectrum-bookmark-side on ≤640px (no horizontal room on
narrow phones); previously visible but clipped off-screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 18:57:59 +01:00
sjg 51df676e46 [feat](trx-frontend-http): mobile UI improvements
- Fix 5-tab bottom nav (grid was repeat(4) with 5 tabs; About overflowed)
- Add SVG icons to each tab; show icon+label on mobile bottom nav
- Swipe left/right to switch tabs (excludes jog wheel, spectrum canvas,
  map, scrollable containers and form inputs to avoid conflicts)
- Extract navigateToTab() helper used by both click and swipe handlers
- Collapse header subtitles at ≤640px to reclaim vertical space
- Bookmark table → 2-column card layout at ≤640px with ::before labels
- Audio volume labels switch to horizontal row layout at ≤520px;
  squelch slider now also spans full width
- Controls tray uses overflow-x: auto (not visible) at ≤760px so
  content wider than viewport scrolls rather than overflowing layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 18:46:44 +01:00
sjg 35983eb971 [feat](trx-frontend-http): split decode history into separate HTTP endpoint
Move history replay out of the /decode SSE stream into a new
GET /decode/history JSON endpoint. The JS client now opens /decode
immediately for live packets (no gating) and fetches history in
parallel via fetch(), draining it in the background with the existing
chunked drainDecodeHistory() helper.

This ensures real-time decode messages are never blocked by a large
history payload, and removes the historyReceived gate entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 18:33:40 +01:00