Commit Graph
100 Commits
Author SHA1 Message Date
sjg dca01ead90 [fix](trx-frontend-http): reset scheduler step timing
Track the last applied scheduler entry so previous/next\ncycles correctly across active entries and resets the\ncountdown after manual entry changes.\n\nVerification: cargo test -p trx-frontend-http scheduler\nVerification: node --check assets/web/plugins/scheduler.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 13:17:45 +01:00
sjg badf7c0d0f [fix](trx-frontend-http): improve scheduler entry controls
Add previous/next scheduler entry controls for overlapping\ntime-span slots and fix interleave timing calculations so\nthe active slot and countdown align with the overlap window.\n\nVerification: cargo test -p trx-frontend-http scheduler\nVerification: node --check assets/web/plugins/scheduler.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 13:11:30 +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 07c5129668 [feat](trx-reporting): add software comment to APRS beacon
Append the APRS-IS position beacon comment as\n`trx-rs v<version> by SP2SJG` so IGate beacons identify\nthe running software version.\n\nUpdate the APRS beacon formatter tests to assert the exact\npayload including the generated version string.\n\nCo-Authored-By: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:37:38 +01:00
sjgandClaude Sonnet 4.6 9292bf3830 [fix](trx-ft8): drop FT8 messages that fail to unpack
Skip decoded candidates where ftx_message_decode() returns a non-OK
status instead of forwarding a synthetic error string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:33:02 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 a3c098b68f [fix](trx-ft8): correct FT8 SNR to WSJT-X 2500 Hz convention
The score from ft8_lib is an averaged uint8 difference between Costas
sync tones and their neighbours (each unit = 0.5 dB). The previous
score * 0.5 gave the signal-above-adjacent-noise in dB relative to a
single 3.125 Hz waterfall bin, yielding values of +5 to +50 dB —
all wrong.

Subtract 10*log10(2500/3.125) ≈ 29 dB to normalise to the 2500 Hz
reference bandwidth used by WSJT-X and expected by PSKReporter:

  snr = score * 0.5 - 29.0

This maps score 10 (minimum decodable) → -24 dB and score 60 → +1 dB,
matching typical WSJT-X SNR report ranges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:13:03 +01:00
sjgandClaude Sonnet 4.6 3c31e16833 [fix](trx-reporting): split beacon_symbol into table and code fields
Replace the two-character beacon_symbol string with separate
beacon_symbol_table (char) and beacon_symbol_code (char) fields to
avoid TOML backslash escaping issues with the alternate symbol table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:03:56 +01:00
sjgandClaude Sonnet 4.6 40e99d3ea9 [feat](trx-reporting): add APRS-IS position beaconing
Add periodic IGate position beacon support to the APRS-IS uplink.
New AprsFiConfig fields: beacon (bool), beacon_interval_secs (default
1200), beacon_symbol (default "/-"), latitude/longitude overrides.

A beacon is sent immediately on connect then every beacon_interval_secs.
Coordinates fall back from [aprsfi] to [general].latitude/longitude.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:49:29 +01:00
sjgandClaude Sonnet 4.6 872e086763 [feat](trx-reporting): add callsign field to AprsFiConfig
Allow specifying the IGate callsign directly in [aprsfi] instead of
relying on [general].callsign. The aprsfi-specific callsign takes
precedence; [general].callsign is used as fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:35:32 +01:00
sjgandClaude Sonnet 4.6 fed8948a61 [feat](trx-rs): extract trx-reporting crate for uplink tasks
Move aprsfi and pskreporter modules from trx-server into a new
standalone trx-reporting library crate. Config types (AprsFiConfig,
PskReporterConfig) move to trx-reporting and are re-exported from
trx-server::config for backwards compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:28:47 +01:00
sjgandClaude Sonnet 4.6 ecb058a9d2 [fix](trx-server): comply with APRS-IS IGate spec
- Append mandatory q-construct (,qAR,<callsign>) to all forwarded
  TNC2 packets via updated format_tnc2(pkt, igate_call)
- Add TCPIP/TCPXX loop-prevention check before forwarding
- Drain server-sent data in select! loop to prevent TCP backpressure
- Enable TCP_NODELAY for low-latency packet forwarding
- Guard against history replays: skip packets older than 2 minutes
- Use "trx-rs" in login string and keepalive comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:24:03 +01:00
sjgandClaude Sonnet 4.6 1dec2c2663 [chore](trx-rs): centralize version in workspace.package
Add [workspace.package] version = "0.1.0" to the root Cargo.toml and
switch all 21 member crates to version.workspace = true so the entire
workspace is versioned from a single place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:13:06 +01:00
sjgandClaude Sonnet 4.6 f7d3a935ee [fix](trx-server): use "trx-rs" as PSKReporter software identifier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:04:46 +01:00
sjgandClaude Sonnet 4.6 587ece9903 [fix](trx-backend-soapysdr): fix audio distortion from incorrect force_mono_pcm
Two bugs introduced by 60697bb:

1. dsp.rs passed `channel_idx == 0` as force_mono_pcm, which forced the
   primary pipeline channel to output mono samples. The Opus encoder was
   configured for stereo, so it received half the expected frame data,
   causing distortion for all connected audio clients.
   Fixed by passing `false` — hidden virtual channels already set
   force_mono_pcm=true via set_force_mono_pcm() in vchan_impl.rs.

2. main.rs short-circuited channel conversion when no audio clients were
   connected, sending raw frames to pcm_tx (decoders). When clients then
   connected, decoders switched to receiving stereo-interleaved frames,
   making decoder input format dependent on client presence.
   Fixed by always performing the channel conversion before sending to
   pcm_tx; the no-client skip now only bypasses Opus encode + rx_audio_tx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 08:53:08 +01:00
sjgandClaude Sonnet 4.6 945912adc1 [fix](trx-server): guard APRS-IS uplink against history replays
Skip APRS packets whose ts_ms is older than 120 seconds. Live RF-decoded
packets arrive within milliseconds; history replay items can be up to 24
hours old and must not be re-uploaded to APRS-IS as live traffic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 08:45:24 +01:00
sjgandClaude Sonnet 4.6 5ed3e29d90 [fix](trx-server): fix PSKReporter IPFIX packet format and batching
Rewrite the PSKReporter uplink to match the protocol spec exactly:

- Fix template FlowSetIDs: receiver uses 0x0003 (Options Template Set),
  sender uses 0x0002 (Template Set); previously both used 0x9992/0x9993
- Add missing enterprise numbers (0x0000768F = 30351) to all enterprise
  field specifiers in both template blocks
- Fix sender template field IDs: use correct attributes (senderCallsign
  30351.1, frequency 30351.5, sNR 30351.6, iMD 30351.7, mode 30351.10,
  informationSource 30351.11, senderLocator 30351.3, flowStartSeconds 150)
- Fix sender data field order to match the template declaration
- Add iMD byte (0) required by the 8-field template
- Add 4-byte null padding on receiver and sender data records
- Batch spots into one UDP packet per 5-minute window (spec requirement)
- Deduplicate by callsign within each window (keep most-recent spot)
- Send template descriptors only in first 3 packets then once per hour
- Increment sequence number by report count, not packet count
- Guard against history replays: drop any spot older than the flush
  window (live FT8/WSPR is seconds old; history can be 24 h old)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 08:33:36 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex 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
sjgandOpenAI Codex cb824d20ec [fix](trx-frontend-http): separate map corner controls
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 15:48:28 +01:00
sjgandOpenAI Codex 968dc84997 [fix](trx-frontend-http): honor scheduler explicit center freq
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 15:13:04 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex a82a7ab668 [feat](trx-frontend-http): overlay map controls
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 14:18:18 +01:00
sjgandOpenAI Codex 60697bb26a [perf](trx-server): reduce headless scheduler audio cpu
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 14:18:03 +01:00
sjgandOpenAI Codex 7fde0f7730 [chore](trx-server): remove temporary hang tracing
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:50:41 +01:00
sjgandOpenAI Codex 47f57aa10d [fix](trx-server): drop state watch borrows before decode
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:37:42 +01:00
sjgandOpenAI Codex bc153b84ee [debug](trx-server): trace rig task progress
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:32:38 +01:00
sjgandOpenAI Codex 12f1d81af8 [debug](trx-server): trace backend control flow
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:19:21 +01:00
sjgandOpenAI Codex 1be02ec8ad [debug](trx-server): trace listener and rig task activity
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 11:57:34 +01:00
sjgandOpenAI Codex e5bf7346c8 [debug](trx-backend-soapysdr): trace SDR retune application
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 11:31:27 +01:00
sjgandOpenAI Codex fc954fd27c [fix](trx-frontend-http): stop headless background spectrum polling
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 11:28:41 +01:00
sjgandOpenAI Codex 8be8e798c6 [fix](trx-frontend-http): drop orphaned vchans on disconnect
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 08:14:34 +01:00
sjgandOpenAI Codex 8043a5001c [fix](trx-server): move remaining decoders off async workers
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 08:09:16 +01:00
sjgandOpenAI Codex 73173d29ff [fix](trx-backend-soapysdr): keep vchans across scheduler retunes
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 07:38:56 +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 66987a4070 [fix](trx-client): preserve vchan commands under scheduler churn
Use an unbounded virtual-channel command queue so background decode and scheduler transitions do not silently drop subscribe or remove commands.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 07:10:40 +01:00
sjg c178c56f2e [fix](trx-server): clean up hidden vchans on disconnect
Remove hidden background decode channels when the owning audio client disconnects to avoid stale DSP and decoder buildup.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 07:01:09 +01:00
sjg 69fd24577c [fix](trx-server): move heavy decoders off async workers
Run FT8 and WSPR decode steps in blocking sections so the server listener stays responsive under decode load.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 06:56:54 +01:00
sjg 0179c13000 [fix](trx-server): avoid spectrum hang on overflow
Avoid SoapySDR overflow restart wedge and coalesce concurrent GetSpectrum requests.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 06:45:10 +01:00
sjgandOpenAI Codex 4cac91e36a [fix](trx-frontend-http): show scheduler interleave timing
Replace the misleading scheduler task countdown with the actual time-span interleave switch timing in the main controls row.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 01:08:31 +01:00
sjgandOpenAI Codex be4844c710 [fix](trx-client): speed up remote spectrum polling
Raise remote spectrum polling from 100 ms to 50 ms while keeping the relaxed timeout and subscriber gating.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 01:03:38 +01:00
sjgandOpenAI Codex 4f4cd4647d [fix](trx-client): reduce remote spectrum timeout churn
Relax the remote spectrum timeout, poll at the backend update cadence, and stop polling when no spectrum subscribers are connected.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 00:59:59 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex 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
sjgandOpenAI Codex f7cbc0cb02 [fix](trx-frontend-http): move scheduler handoff out of channels row
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 00:28:25 +01:00
sjgandOpenAI Codex 5c28ed1269 [feat](trx-frontend-http): add scheduler control handoff
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 00:22:18 +01:00
sjgandOpenAI Codex f8fd4572c7 [feat](trx-frontend-http): paginate bookmarks tab
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 23:57:02 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex bdd3d29374 [fix](trx-frontend-http): show aprs and ais bookmarks in background decode
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 23:03:57 +01:00
sjgandOpenAI Codex 95fccd3da6 [feat](trx-frontend-http): expand background decode selection
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:57:25 +01:00
sjgandOpenAI Codex 34d0ec8ca8 [feat](trx-server): add aprs and ais background decoders
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:57:18 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex 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
sjgandOpenAI Codex 21a534bdb6 [feat](trx-client): support background decode subscriptions
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:42:43 +01:00
sjgandOpenAI Codex 763d4c00b0 [feat](trx-server): support hidden background decode channels
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:42:37 +01:00
sjgandOpenAI Codex 6c9ff33d68 [feat](trx-frontend): add background decode audio commands
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:42:33 +01:00
sjgandOpenAI Codex b83558b1a2 [feat](trx-core): add hidden background channel API
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:42:21 +01:00
sjgandOpenAI Codex 2d014ac45b [docs](trx-rs): refresh top-level docs and config example
Rewrite the README, remove AI-generated planning docs, and regenerate the combined example config.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 22:09:34 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex 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
sjgandOpenAI Codex 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
sjgandClaude Sonnet 4.6 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
1fe7dc88c6 [feat](trx-frontend-http): add frequency layers display for virtual channels
Render virtual channels as absolutely-positioned layer strips inside a
shared relative container (#vchan-freq-layers). Layers are sorted by
frequency ascending so higher-frequency channels receive a higher z-index
and sit on top by default. Hovering any layer temporarily assigns it the
maximum z-index to bring it to the front; leaving restores the original
stacking order. Each layer is offset by 11 px vertically so all channels
remain visible as a staggered card stack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-12 20:04:27 +01:00
sjg 93ff35a824 Add per-channel RDS overlays for WFM vchans 2026-03-11 22:39:02 +01:00
sjgandOpenAI Codex 21972c27d2 [test](trx-client): fix fixtures for updated rig snapshot
Update test fixtures to include hf_aprs_decode_enabled and use the current spectrum watch sender type in remote client tests.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 21:41:39 +01:00
sjgandOpenAI Codex 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
sjgandOpenAI Codex 717228a635 [feat](trx-server): handle virtual channel bandwidth updates
Handle AUDIO_MSG_VCHAN_BW in the audio server path and apply per-channel filter bandwidth through the SoapySDR virtual channel manager.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 21:41:26 +01:00
sjgandOpenAI Codex 502e97049a [feat](trx-core): add virtual channel bandwidth control
Add virtual-channel bandwidth control to the shared core API and audio protocol constants for client/server coordination.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 21:41:17 +01:00
sjgandClaude Sonnet 4.6 894739bbab [chore](trx-rs): resolve all compiler and clippy warnings
- trx-server/rig_handle: remove dead vchan_manager field (was set but
  never read after the virtual-channel refactor)
- trx-server/listener: remove now-missing vchan_manager initializer
- trx-server/main: remove vchan_manager_for_handle intermediates that
  only fed the dropped field
- trx-server/audio: suppress too_many_arguments on run_audio_listener
- trx-frontend-http/server: suppress too_many_arguments on build_server
- trx-core/vchan: update module doc comment to not reference the
  removed RigHandle::vchan_manager field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 21:01:38 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 60267d450b [feat](trx-rs): persistent multi-channel virtual channels with OOB eviction
Allow users to allocate multiple virtual channels independently of
browser tab count. Channels survive SDR center-frequency retuning as
long as they stay within the capture bandwidth; channels that fall
outside the SDR span are automatically destroyed.

Changes:
- trx-core: add AUDIO_MSG_VCHAN_DESTROYED (0x12) wire constant;
  add default subscribe_destroyed() to VirtualChannelManager trait
- trx-backend-soapysdr: update_center_hz() detects OOB channels,
  removes them, fires destroyed_tx broadcast; add destroyed_sender()
  and subscribe_destroyed() override
- trx-server/audio: recv_destroyed() helper avoids select! busy-loop
  for non-SDR backends; send AUDIO_MSG_VCHAN_DESTROYED to client when
  a channel is evicted server-side
- trx-client/audio_client: persist active_subs across TCP reconnects,
  re-subscribe on reconnect; handle AUDIO_MSG_VCHAN_DESTROYED by
  pruning vchan_audio map and forwarding UUID via vchan_destroyed_tx
- trx-frontend/lib: add vchan_destroyed broadcast field to
  FrontendRuntimeContext
- trx-client/main: wire vchan_destroyed_tx into audio client and
  frontend runtime context
- trx-frontend-http/vchan: remove per-session one-channel limit in
  allocate(); replace auto-evict in release_session_on_rig() with
  subscriber-count-only update; add remove_by_uuid() for server-
  triggered OOB destruction (skips redundant VChanAudioCmd::Remove)
- trx-frontend-http/server: spawn background task that forwards
  vchan_destroyed broadcast to ClientChannelManager.remove_by_uuid()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 20:50:49 +01:00
sjgandClaude Sonnet 4.6 4e93dcc82a [fix](trx-frontend-http): fix virtual channel audio streaming
Three bugs prevented vchan audio from working reliably:

1. vchan.js: `vchanReconnectAudio` returned before updating
   `_audioChannelOverride` when audio was inactive. Switching to
   a virtual channel with audio off then starting audio manually
   would connect to the primary channel instead. Move the override
   update before the rxActive guard so it always reflects the
   active channel.

2. audio.rs: `audio_ws` returned 404 immediately if the channel
   was not yet in `vchan_audio`. The entry is populated when
   `AUDIO_MSG_VCHAN_ALLOCATED` arrives from the audio TCP client,
   which can lag the HTTP allocation by up to ~100 ms. Replace the
   instant 404 with a 2-second polling loop (50 ms intervals) so
   the WebSocket upgrade waits for the channel to be ready.

3. vchan.rs: `release_session_on_rig` evicted zero-subscriber
   channels silently — no `VChanAudioCmd::Remove` was sent.
   Collect evicted channel IDs before retain() and send Remove
   commands so the server-side DSP pipeline and Opus encoder are
   torn down properly on session disconnect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 20:24:48 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 af45c32222 [feat](trx-frontend-http): vchan freq display sync, BW accent, scheduler multi-channel
Virtual channel display:
- vchan.js: wrap refreshFreqDisplay() so the main freq field always shows
  the active virtual channel's frequency instead of channel 0's; expose
  vchanSyncAccentUI() to add vchan-ch-active CSS class (colored border) to
  #freq and #spectrum-bw-input when on a non-primary channel
- style.css: --vchan-color (#38bdf8 sky-blue), .vchan-ch-active box-shadow,
  vchan-picker active button left-border accent

Scheduler multi-channel slots:
- scheduler.rs: add center_hz (Option<u64>) and bookmark_ids (Vec<String>)
  to ScheduleEntry; SchedulerStatus gains last_center_hz and
  last_bookmark_ids; background task sends SetCenterFreq before SetFreq
  when center_hz is set and records extra bookmark_ids in status
- scheduler.js: center-freq input and extra-channel bookmark picker (tag
  list with + / × buttons) in the add-entry form; extra channels shown in
  the entries table
- index.html: center freq field + extra bookmark picker widgets; table
  gains Center freq and Extra channels columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:22:36 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 832e42ca3b [fix](trx-backend-soapysdr): suppress unused import/field warnings in vchan_impl
- VirtualSquelchConfig is only used in tests; gate import with #[cfg(test)]
- fixed_slot_count is reserved for future use; mark #[allow(dead_code)]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 07:01:37 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 dda5ec17bb [feat](trx-backend): VirtualChannelManager trait + SdrVirtualChannelManager impl
Add VirtualChannelManager trait in trx-core::vchan with types VChannelInfo,
VChanError, and SharedVChanManager alias. Re-export from trx-backend::vchan.

Implement SdrVirtualChannelManager in trx-backend-soapysdr:
- Wraps Arc<SdrPipeline> + shared AtomicI64 center_hz
- add_channel / remove_channel / set_channel_freq / set_channel_mode
- Slot-stability: on remove, shifts pipeline_slot for surviving channels
- update_center_hz: recomputes IF offsets for all virtual channels on retune
- update_primary_meta: keeps channel-0 freq/mode in sync for API consumers

Wire into SoapySdrRig (holds Arc<SdrVirtualChannelManager>, exposes
channel_manager()), SdrPipeline (shared_center_hz AtomicI64), and RigHandle
(vchan_manager: Option<SharedVChanManager>). main.rs extracts the manager
before boxing the SDR rig and stores it in the handle.

Add max_virtual_channels to SdrConfig (default 4, TOML-configurable).
Add 5 unit tests: add, remove, permanent guard, cap, out-of-bandwidth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 00:48:31 +01:00
sjgandClaude Sonnet 4.6 05169912b1 [feat](trx-backend-soapysdr): dynamic virtual channel DSP slots
Replace the fixed channel_dsps Vec (thread-owned) with an
Arc<RwLock<Vec<...>>> shared between the IQ read thread and the async
side, enabling live add/remove of virtual DSP channels.

Cache audio construction params in SdrPipeline so add_virtual_channel()
can build ChannelDsp instances without being re-passed them. Add:
  - SdrPipeline::add_virtual_channel() / remove_virtual_channel()
  - SoapySdrRig::virtual_channel_add/remove/set_freq/set_mode()
  - SoapySdrRig::center_hz() / half_span_hz() accessors

The IQ read loop holds a brief read lock (~2 ms per block) while
processing all channels; write lock for add/remove waits at most one
block. All 27 existing tests continue to pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 00:26:23 +01:00
sjgandClaude Sonnet 4.6 2f115fbec3 [docs](trx-rs): add virtual channels design document
Describes the per-rig dynamic virtual channel architecture for SDR rigs:
session binding via SSE session_id, channel lifecycle (ref-counted,
auto-freed on last subscriber disconnect), center-freq conflict rules,
per-channel audio WebSocket and decode SSE, frontend picker UI, and
phased implementation plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-11 00:17:52 +01:00
sjgandClaude Sonnet 4.6 d53d60629e [feat](trx-frontend-http): treat 00:00–00:00 time span as all-day entry
start == end previously matched nothing (empty range).  Now treated as a
24-hour window, making it easy to define a catch-all bookmark without
manually entering 00:00–23:59.

UI shows "All day / —" in the entries table and tooltip hints on both time
inputs explain the convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:43:53 +01:00
sjgandClaude Sonnet 4.6 e4cfd35282 [feat](trx-frontend-http): per-entry interleave time in TimeSpan scheduler
Each ScheduleEntry can now carry its own interleave_min, overriding the
config-level default for that slot in the cycle.  The cycle length is the
sum of all active entries' effective durations (weighted), so entries with
longer individual interleave times occupy proportionally more time.

UI: "Interleave (min, optional)" input in the add-entry form; value shown
in the entries table (displays "—" when using the config default).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:35:52 +01:00
sjgandClaude Sonnet 4.6 998c6ad0e6 [style](trx-frontend-http): align scheduler buttons with site-wide button styles
Remove custom padding, border-radius, color, cursor, and hover rules from
.sch-save-btn, .sch-reset-btn, and .sch-remove-btn — the global button rule
already handles all of that consistently across every theme.

.sch-save-btn retains only the accent-green background/border-color to mark
it as the primary action; the global hover/active/disabled transitions still
apply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:32:16 +01:00
sjgandClaude Sonnet 4.6 f1d412a566 [style](trx-frontend-http): fix clippy::manual_range_contains in scheduler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:28:20 +01:00
sjgandClaude Sonnet 4.6 877573c905 [fix](trx-frontend-http): populate scheduler bookmark picker after async load
The TimeSpan bookmark <select> was populated in wireSchedulerEvents() which
runs before the apiGetBookmarks() fetch completes, leaving it empty.
Moved population to populateTsBookmarkSelect() called from loadScheduler()'s
.then() callback so bookmarkList is already filled.

Also pre-fill grayline lat/lon from serverLat/serverLon when the field has
no saved value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:28:00 +01:00
sjgandClaude Sonnet 4.6 4f9f93c9c1 [feat](trx-frontend-http): add interleave time for overlapping TimeSpan entries
When multiple time-span entries are active simultaneously, the scheduler
now cycles through them by slot: slot = floor(utc_min / interleave_min) % count.
The interleave_min field is optional (null disables, first match wins).

UI: "Interleave time (min)" number input in the TimeSpan section with a
hint explaining the behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 23:25:12 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 46c0f8d0bb [fix](trx-frontend-http): deduplicate AIS history snapshot by MMSI
AIS vessels transmit every 2-30 s; without deduplication the 24-hour ring
buffer can hold tens of thousands of entries, making the /decode/history
response huge and causing O(n^2) DOM thrashing on the client side.

- Add AIS_HISTORY_MAX = 10 000 to cap the ring buffer memory footprint.
- snapshot_ais_history() now returns the latest message per MMSI (one entry
  per vessel), sorted ascending by ts_ms so the client replays in order.

This matches APRS history behaviour: APRS stations transmit infrequently so
their history is naturally compact; AIS history is now equally compact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 20:17:01 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 a62997d6ce [fix](trx-frontend-http): propagate color/outline in TrackSymbol setAisState
setAisState only updated heading/course/speed, silently dropping color and
outline fields. Extend it to also accept and apply those fields so theme
color refreshes take effect without recreating the marker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-10 19:32:14 +01:00
sjgandClaude Sonnet 4.6 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
sjgandClaude Sonnet 4.6 ce1ca48384 [fix](trx-frontend-http): remove animation from spectrum center-shift arrows
The global button transition and the :active scale(0.97) transform were
interfering with the translateY(-50%) centering, making the buttons jump
on press. Added transition:none and reduced :active to translateY(-50%)
only (no scale).

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