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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
System font stack: replace bare 'sans-serif' with system-ui / -apple-system
/ BlinkMacSystemFont / Segoe UI chain — sharper rendering on all platforms
at zero extra load cost.
Button hover/active: add transition (100ms) + color-mix hover brightening
+ active depression (translateY 1px) to all buttons. Previously buttons had
zero visual feedback on interaction.
Scrollbar styling: thin (6px) custom scrollbars via ::-webkit-scrollbar and
scrollbar-width/color for Firefox. Thumb uses border-color tinted with the
accent on hover — matches each theme automatically via CSS variables.
Phosphor theme: classic green-phosphor CRT aesthetic — near-black background,
#39ff14 neon-green accent, glow text-shadow on the freq display, matching
spectrum/waterfall canvas palette. Both dark and light variants included.
Registered in the style picker select, setStyle() valid list, and
CANVAS_PALETTE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Previously the server emitted N individual SSE events (one per decoded
message) followed by a history_done sentinel. With a 1.3 MB history
this caused thousands of EventSource onmessage callbacks each blocking
the JS main thread, interrupting audio playback and spectrum rendering
for 50+ seconds.
Server: serialize the entire history Vec as a single named "history"
event containing a JSON array, then chain directly into the live
decode stream. One serde_json::to_string call instead of N.
JS: listen for the "history" event, parse the array once, pass it to
the existing drainDecodeHistory() chunked dispatcher (30 msgs per
setTimeout slice to stay off the main thread), then gate onmessage
dispatching on historyReceived. Removes the historyBuffer accumulator
and the history_done event entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Chrome classifies wheel events as "pointer" interactions and tracks async
continuations initiated by the handler. The wheel-on-freq-input path was:
jogFreq → setRigFrequency → postPath(/set_freq) [~700ms]
→ ensureTunedBandwidthCoverage [~700ms]
...two sequential network round-trips totalling ~1400ms of INP.
Three changes:
1. yieldToMain(): add a scheduler.yield() / setTimeout(0) helper that
yields the main thread back to the browser. Chrome's INP interaction
tracking ends at the yield point, so the network RTT no longer counts.
2. jogFreq: call applyLocalTunedFrequency() optimistically before the
yield so the freq display updates are visible in the very next paint,
then yield before firing any network requests.
3. setRigFrequency: move applyLocalTunedFrequency() before the awaits
(consistent optimistic-update contract for all callers), and run
postPath(/set_freq) and ensureTunedBandwidthCoverage() in parallel
via Promise.all — they are independent server operations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three causes of >30 s SSE stalls:
1. SPECTRUM_IO_TIMEOUT was 300 ms — transient server jitter triggered
false-positive spectrum failures and immediate TCP disconnects.
Raised to 1 s to tolerate brief load spikes.
2. reconnect_delay was never reset after a successful TCP connect, so
after a few spectrum-induced disconnects the backoff reached 10 s.
Each new disconnect then cost 10 s of stale SSE state, and several
cycles accumulated to >30 s. Reset to 1 s on every successful
TCP connect so recovery stays fast.
3. SSE pings were emitted as comments (": ping"), which EventSource
never exposes to onmessage. lastEventAt was therefore never updated
by pings, causing the JS heartbeat to force-reconnect every ~20 s
even on healthy, stable connections. Changed to a named "ping"
event and added es.addEventListener("ping", …) to update lastEventAt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the JSON f32 array (~7.5 KB/frame) with a named SSE event "b"
carrying base64-encoded i8 bins (~1.4 KB/frame, ~5x reduction):
event: b
data: {center_hz},{sample_rate},{base64_i8_bins}
1 dB per step covers the -128…+127 dBFS display range, sufficient for
visualization. RDS is stripped from the spectrum frame and emitted as a
separate named "event: rds" only when the payload changes. The JS
decoder uses atob() + sign-extension to reconstruct the float bin array.
A minimal inline base64 encoder is added server-side (no new crate).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Add "Enable HF APRS" toggle button to the HF APRS tab (same style as
FT8/WSPR); button is disabled during TX like other decoder toggles
- app.js: sync button text/colour from SSE state updates
- hf-aprs.js: connect button click to /toggle_hf_aprs_decode
- bookmarks.js: add "HF APRS" checkbox to Add/Edit Bookmark decoder
section; bmReadDecoders/bmWriteDecoders handle "hf-aprs" key; bmApply
toggles the decoder to match bookmark preference on recall
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Adds a second APRS demodulator path tuned for the HF APRS standard
(300 baud Bell 103-style AFSK, mark=1600 Hz / space=1800 Hz), active
on RigMode::DIG. Shares AX.25 framing, APRS parsing, APRS-IS uplink,
and frontend display with the existing VHF stack.
- trx-aprs: parameterise Demodulator::new(); add AprsDecoder::new_hf()
- trx-core: HfAprs variant in DecodedMessage; hf_aprs_decode_enabled /
hf_aprs_decode_reset_seq in RigState/RigSnapshot; SetHfAprsDecodeEnabled
and ResetHfAprsDecoder commands; handlers.rs fallback arm updated
- trx-protocol: client command variants + bidirectional mapping; test
fixture updated
- trx-server: run_hf_aprs_decoder() task (activates on DIG mode);
hf_aprs history in DecoderHistories; rig_task command dispatch;
aprsfi uplink forwards HfAprs via OR-pattern
- trx-frontend: hf_aprs_history in FrontendRuntimeContext
- trx-frontend-http: prune/record/snapshot/clear helpers; SSE history
replay; toggle_hf_aprs_decode + clear_hf_aprs_decode endpoints;
/hf-aprs.js endpoint; HF APRS tab in web UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Safari stalls noticeably on gl.bufferData (which reallocates the GPU
buffer) when called multiple times per frame. Replace with a pre-
allocated scratch Float32Array and gl.bufferSubData, which only uploads
new data without reallocating. The GPU buffer is grown with bufferData
only when the scratch outgrows it (amortised doubling). Also eliminate
the per-draw-call `new Float32Array(vertices)` allocation in favour of
scratch.set() + subarray(), removing per-frame GC pressure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
During history restore with thousands of APRS packets, the console.log
in addAprsPacket was called for every entry, slowing the replay down
significantly. Remove it entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Server emits an SSE sentinel event (history_done) after replaying
stored history. Client buffers all incoming messages until the sentinel
arrives, then drains the buffer in 30-event chunks via setTimeout so
the browser can handle input between batches. Live events after the
sentinel are dispatched immediately as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Prepend the in-progress line to the bar render so characters appear
immediately rather than waiting for a newline or 5s gap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show a live decode bar on the overview strip when in CW/CWR mode,
matching the APRS and AIS bar pattern. Accumulates decoded characters
into lines (split on newline events or >5s gaps), keeps a 15-minute
rolling history, and shows up to 8 recent lines with timestamp and
WPM/tone metadata. Clears on resetCwHistoryView.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add cwAutoLocalOverride flag in cw.js to block server-state snapshots
from overriding the checkbox while a user-initiated POST is in-flight.
Expose applyCwAutoUiFromServer for app.js render() to call instead of
applyCwAutoUi, preventing a racing SSE event carrying the old cw_auto
value from immediately undoing the user's toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>