Add 8 enhancements to the spectrum display:
1. Noise floor reference line — dashed horizontal line at estimated
noise floor (15th-percentile heuristic)
2. Peak frequency labels — top 5 strongest peaks labeled with
frequency text on the spectrum canvas
3. Crosshair lines — vertical + horizontal guide lines follow
cursor on hover for precise frequency/dB reading
4. Zoom indicator + minimap — shows current zoom level (e.g. "4.0x")
and a minimap showing the visible window within the full span
5. dB range control — new Range input alongside Floor, with Auto
button updating both; allows direct control of vertical span
6. Keyboard shortcuts — Arrow Left/Right to pan, +/- to zoom,
0 to reset zoom; documented in hint bar
7. Full waterfall panel — WebGL waterfall canvas below the spectrum
plot, synchronized with zoom/pan, with scroll/click/drag support
8. Signal overlay extended — overlay height now includes waterfall
canvas for consistent BW/bookmark/freq marker coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Nudge state watch when server_connected goes false so SSE delivers the change. Frontend applies a desaturated frost + banner instead of a blocking overlay, keeping the last-known state visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The map module was tagging all decode markers (APRS, AIS, VDES,
FT8/FT4/FT2/WSPR locators) with the global rig picker's active rig
instead of the actual source rig. This made the map's own rig filter
dropdown ineffective in multi-rig setups.
- Add rig_id field to all decode message structs (AisMessage,
VdesMessage, AprsPacket, CwEvent, Ft8Message, WsprMessage)
- Set rig_id on messages in audio_client before broadcasting, using
the actual rig connection identifier
- Update history collector to prefer message rig_id over the global
active rig fallback
- Pass rig_id through plugin normalize functions (AIS, APRS, VDES,
HF-APRS) so it reaches the map add functions
- Update all map marker functions (aprsMapAddStation, aisMapAddVessel,
vdesMapAddPoint, mapAddLocator) to use the message's rig_id with
fallback to the global picker for backward compatibility
https://claude.ai/code/session_015gC7axHk2jmp7HbFPdbivN
Signed-off-by: Claude <noreply@anthropic.com>
Decoder bar overlays (AIS, VDES, FT8, APRS, RDS) use backdrop-filter
blur for a frosted-glass look in the browser, but this can't be
replicated on canvas — resulting in opaque blocks covering the spectrum
in screenshots. Cap their background alpha to 0.35 when rendering to
the snapshot canvas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Press F1 to toggle a help overlay listing available keyboard shortcuts.
Dismiss with F1, Escape, or clicking the backdrop. Refactored the
global keydown handler to route all shortcuts through one listener.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Radio paths now originate from the rig that decoded the message rather
than the currently selected rig. Bookmark locators no longer draw radio
paths. Rig switch no longer tears down decode pipeline since it is
rig-independent. Mobile spectrum controls use flex-wrap for better layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add latitude/longitude to /rigs API response. Map now displays receiver
markers for all configured rigs, de-duplicated by location. Decode history
is no longer filtered to the active rig so all remotes contribute to the map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
WebCodecs AudioDecoder does not support Opus on Safari. Fall back to
opus-decoder WASM library (loaded from CDN) for browsers without
WebCodecs Opus support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Scope picker filters the bookmarks table for editing. Spectrum and map
always show merged general + active rig bookmarks via bmOverlayList.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two-tier bookmark system: general bookmarks shared across all rigs plus
rig-specific bookmarks with scope picker in the Bookmarks tab. Scheduler
storage split into per-rig files with migration from legacy single file.
Decode history tagged with rig_id and filterable via ?remote= on
/decode/history endpoint. Decode SSE reconnects on rig switch to refresh
filtered history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Send the selected remote together with virtual-channel audio requests and use per-rig stream info when /audio is opened with channel_id.
This keeps browser channel audio aligned with the requested remote after the recent multi-rig client changes.
Add coverage for parsing channel_id together with remote.
Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Create and resume the RX AudioContext from the audio button click so Chromium does not leave playback suspended until a later interaction.
Reuse that context when stream metadata arrives instead of recreating it from the WebSocket message path.
Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rename HTTP query params, JSON fields, and scheduler payloads to
use remote names consistently while still accepting legacy `rig_id`
inputs through serde aliases.
Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Include active rig display name and nearest city in the browser tab
title: "freq - rig name - city, country - trx-rs v<version>".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add checkbox to enable/disable NB and number input for threshold (1-100).
Controls are hidden by default and shown when the server reports NB support
via SSE filter state updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The bookmark color palette is derived from CSS variables (--accent-yellow,
--accent-green, etc.) which change on both dark/light theme toggle AND
palette style change (arctic, lime, etc.). The previous fix only covered
setTheme; extract invalidateBookmarkColors() and call it from setStyle
as well so bookmark chips recolour on any visual change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
getComputedStyle may return stale CSS variable values if the browser
has not flushed the style recalculation after changing data-theme. Force
a recalc by reading a property value first. Also clear cached bookmark
DOM keys so the next draw pass rebuilds chips from scratch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Bumping bmRevision and scheduling a spectrum draw was not enough because
the spectrum draw path only runs when spectrum data is present. Instead,
directly update --bm-cat-bg/--bm-cat-fg on all existing bookmark chips
from the new theme palette so colors update immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
scheduleSpectrumDraw references a let-bound variable that hasn't been
initialized yet when setTheme runs at startup. Wrap in try/catch so the
early call is silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The HTTP round-trip for set_freq blocks on the server processing the
command (mpsc → TCP → rig hardware → response). With optimistic local
updates, CSS overlay, and SSE snap-back guard already in place, there is
no reason to await the network call. All callers (jog, freq input, spectrum
click, RDS AF tune) now return immediately after the optimistic update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When the user changes frequency, applyLocalTunedFrequency sets lastFreqHz
optimistically. But the SSE state stream could push back the old server
frequency before set_freq completes, causing the marker to snap back then
forward. Add a sequence-guarded _freqOptimisticHz that suppresses stale
SSE frequency updates while a set_freq is in flight.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace synchronous drawSignalOverlay() calls in freq/BW change handlers
with lightweight CSS div elements repositioned via transform: translateX().
This is GPU-composited with zero layout/paint cost, making frequency and
bandwidth changes appear instantaneous. The full WebGL overlay catches up
on the next requestAnimationFrame.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Call drawSignalOverlay() synchronously on frequency and bandwidth changes instead of deferring entirely to requestAnimationFrame. Also make bookmark apply fire-and-forget so the click handler returns immediately after optimistic UI updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Each browser tab can now subscribe to a specific rig's spectrum and
audio independently via ?rig_id= query params on /spectrum and /audio.
The remote client polls spectrum for all rigs with active subscribers
and routes responses to per-rig watch channels. Virtual channel
commands are routed through per-rig senders with global fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
select_rig no longer mutates remote_active_rig_id — only the per-session
mapping is updated. This eliminates cross-tab leaking entirely: each tab
carries its own rig_id via the session manager, /events?rig_id, and
/status?rig_id query params.
The global remote_active_rig_id now only serves as the startup default
for brand-new sessions that have no rig_id yet.
Also fix the About section to show the per-tab lastActiveRigId instead
of the server's global active_rig_id.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
pollFreshSnapshot() fetches GET /status on every SSE connect/reconnect,
but /status always returned the global selected rig's state, overwriting
the per-tab display with whichever rig was last switched to globally.
Now pollFreshSnapshot passes rig_id as a query param and the /status
endpoint uses the per-rig watch channel when provided, matching the
/events behavior for true per-tab rig isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
After select_rig stopped mutating the global remote_active_rig_id for
session-aware clients, SSE reconnects would fall back to the old global
default instead of the newly selected rig. Now connect() passes
lastActiveRigId as a rig_id query param to /events, and the server
prefers it over the global default when present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add per-rig watch::Sender<RigState> map to FrontendRuntimeContext,
populated by refresh_remote_snapshot for every rig returned by GetRigs.
The SSE /events endpoint now subscribes to the session's rig-specific
watch channel instead of the single global one, allowing different
browser tabs to independently view different rigs. The JS frontend
reconnects SSE on rig switch to pick up the new channel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SessionRigManager to track per-SSE-session rig_id so different
browser tabs can independently select rigs without interfering.
The /events SSE stream filters state updates by session rig (falling
back to the global active rig), and /select_rig accepts an optional
session_id to update the per-session mapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rig switch needs the server call so SSE/audio follow the selected rig. Play button now uses a fixed triangle icon sized to match header controls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The /select_rig endpoint sets global server state which affects all tabs. Since postPath() already sends rig_id with every command, the rig picker now just sets the local lastActiveRigId variable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
postPath() was duplicating rig_id on /select_rig calls, causing deserialization failure and silently dropping the rig switch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
All POST command endpoints now accept an optional ?rig_id= parameter so each browser tab can independently target a specific rig. The JS frontend tracks the active rig per-tab and auto-appends rig_id to every request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>