escapeMapHtml was defined inside the map-core.js IIFE, making it
inaccessible to app.js and plugin files (aprs, ais, vdes, cw, hf-aprs)
that call it from global scope, causing ReferenceError at runtime.
Move the function definition to app.js (global scope), export it via
window.trx, and destructure it in map-core.js like other shared utils.
https://claude.ai/code/session_01RhL8cCcszaguKqoWn5XUxL
Signed-off-by: Claude <noreply@anthropic.com>
Extract map-core.js (3,483 lines) and screenshot.js (261 lines) from
the monolithic app.js, reducing it by ~30% (11,967 → 8,427 lines).
Modules communicate via a window.trx shared namespace with getter/setter-
backed state proxying. Map and statistics code lazy-loads on first tab
activation; screenshot code lazy-loads on first "S" keypress. All cross-
module calls use optional chaining for safe access before modules load.
Adds Rust infrastructure (include_str, gz_cache, Actix routes) for
serving the new JS assets.
https://claude.ai/code/session_01HgW8UpscRRA3CgSLqQDzdp
Signed-off-by: Claude <noreply@anthropic.com>
The auth-badge element was wrapped inside <template id="tmpl-about">,
making it invisible to document.getElementById() at page load.
updateAuthUI() accessed badge.style without a null check, throwing a
TypeError that halted app initialization before connect() was called.
Move auth-badge outside the template so it is always in the live DOM,
and add defensive null guards on badge/badgeRole access.
https://claude.ai/code/session_01Km7uxYUzehpYBdYqncnt4n
Signed-off-by: Claude <noreply@anthropic.com>
CSS: reduce backdrop-filter to modals only, add contain/content-visibility
for inactive tabs, optimize transitions to background-color, pre-compute
color-mix results, add container queries, split themes to lazy-loaded file.
JS: cache DOM refs in render path, add field-level diffing for SSE updates,
replace innerHTML with replaceChildren() in hot paths, add WebGL colour
cache invalidation on theme switch.
HTML: add defer to scripts, lazy-load plugin scripts on tab activation,
SVG sprite sheet for tab icons, template elements for deferred tab content,
improve aria-live/keyboard nav/colour contrast accessibility.
Server: upgrade Cache-Control to immutable, add Brotli compression alongside
gzip with Accept-Encoding negotiation.
Implements all items from docs/frontend_improvements.md except app.js ES
module split (P1, requires major refactor) and Web Worker migration (P3).
https://claude.ai/code/session_015rQNMGvusj5jY66MPUgYqt
Signed-off-by: Claude <noreply@anthropic.com>
The #sat-status element was stuck on "Waiting for satellite pass" because:
1. The client audio handler (audio_client.rs) did not include AUDIO_MSG_LRPT_IMAGE
in its message type match, so LRPT image messages from the server were silently
dropped and never reached the frontend.
2. No progress was reported during active LRPT decoding — the only status update
happened when a complete image was finalized, which could take the entire pass.
3. The sat-status text was never updated when the decoder was enabled/disabled,
leaving it permanently at the HTML default text.
Changes:
- Add DecodedMessage::LrptProgress variant for live MCU progress reporting
- Send LRPT progress updates from the decoder task when new MCUs are decoded
- Add AUDIO_MSG_LRPT_IMAGE and AUDIO_MSG_LRPT_PROGRESS to client audio handler
- Update sat-status text when decoder state changes (enabled/disabled)
- Handle lrpt_progress messages in the frontend to show "Receiving — N MCU rows"
https://claude.ai/code/session_017knbD7dr6hJGAWR6pModL7
Signed-off-by: Claude <noreply@anthropic.com>
Add download/remove buttons per file, filename filter, sort dropdown, and paginated file list. Restore header REC toggle button. Add GET /api/recorder/download/{filename} and DELETE /api/recorder/files/{filename} endpoints with path traversal protection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Record Opus audio streams to OGG files on the client. Includes manual start/stop via HTTP API, scheduler-driven auto-recording per schedule entry, and a header REC button in the web UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show S0–S9 as whole units and S9+xdB in 10dB steps instead of fractional S-unit values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fetch /decoders on page load and use the registry to drive all
decoder-related UI instead of hardcoded lists:
- bookmarks.js: bmReadDecoders/bmWriteDecoders and bookmark form
checkboxes generated from registry; bmApply() decoder toggle gate
uses registry active_modes instead of hardcoded DIG/FM check
- background-decode.js: delete SUPPORTED_DECODERS constant, derive
bookmarkDecoderKinds() from registry
- app.js: _decoderToggles and SSE status sync built from registry;
updateDecodeStatus() and setModeBoundDecodeStatus() driven by
registry mode_bound/toggle entries
- index.html: replace 8 hardcoded decoder checkboxes with dynamic
container populated from registry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add DECODER_REGISTRY in trx-protocol::decoders as the single source of
truth for all decoder metadata (activation mode, supported rig modes,
background-decode capability). Replace duplicated resolver functions in
background_decode.rs and sse.rs with shared resolve_bookmark_decoders().
Add GET /decoders endpoint to expose the registry to the frontend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
For SDR backends, DIG and PKT are removed from supported_modes and
replaced by USB and FM respectively. CAT backends (FT-817, FT-450D)
retain DIG/PKT as before.
Decoder mode allowances updated:
- APRS: FM | PKT (was PKT only)
- HF-APRS: USB | DIG (was DIG only)
- AIS: AIS | FM | PKT (was AIS only)
- VDES: VDES | FM (was VDES only)
- FT8/FT4/FT2/WSPR: USB | DIG (unchanged)
- CW: CW | CWR (unchanged)
- LRPT: FM (unchanged, mode-independent)
Frontend status text, bookmark decoder toggles, background-decode
fallbacks, and scheduler wiring updated to match.
https://claude.ai/code/session_01DCAaMH8RF5FNB2gRtVu4pY
Signed-off-by: Claude <noreply@anthropic.com>
The band plan strip was visually positioned between the waterfall and
waveform areas. Move it to the top of .signal-visual-block (above the
overview/waterfall) so it renders above the waterfall. Remove the
bp-webgl transparent overrides since the strip now shows colored
segments in its own position rather than overlaying the spectrum canvas.
https://claude.ai/code/session_01KoxcohG6hn5b7kSc3mC4dA
Signed-off-by: Claude <noreply@anthropic.com>
Relocate the band plan strip from the top of the spectrum canvas to the
bottom, directly above the waterfall canvas. Move the DOM element inside
.spectrum-wrap before the waterfall canvas so it flows naturally in the
correct position. Remove the reparenting logic since the element is now
always inside .spectrum-wrap.
https://claude.ai/code/session_01FUD2eKgeXMFGhhYTzmA4Z6
Signed-off-by: Claude <noreply@anthropic.com>
The spectrum floor/gamma IIFE (line 11507) was missing its closing
`})();`, causing all bandplan strip variables and functions to be
trapped inside the IIFE scope. This made `bandplanRegion`,
`updateBandplanStrip`, and `_bandplanServerDefaultApplied` invisible
to the rest of the file, throwing ReferenceErrors that crashed
`render()` before the frequency display update could run — leaving
the frequency input stuck at its initial "--" placeholder.
https://claude.ai/code/session_01RgKhusmnk7AHEJqn1KHffU
Signed-off-by: Claude <noreply@anthropic.com>
The "Decodes by type" statistics panel only showed AIS because
statsRecordDecode was only called from dispatchDecodeMessage, which
was bypassed by two code paths:
1. dispatchDecodeBatch: uniform-type batches dispatched to specialized
batch handlers (onServerFt8Batch, etc.) returned early without
recording stats.
2. restoreDecodeHistoryGroup: history messages restored on page load
were never recorded in the statistics log.
Fix both paths by recording stats up-front in dispatchDecodeBatch
before dispatching to batch handlers, and in restoreDecodeHistoryGroup
before restoring to plugin views. Add a skipStats parameter to
dispatchDecodeMessage to prevent double-counting when the fallback
per-message loop runs inside dispatchDecodeBatch. Also accept an
optional timestamp in statsRecordDecode so history entries use their
original ts_ms rather than Date.now().
https://claude.ai/code/session_01Ss2AD2bQgXu1ir1Z1WE3VY
Signed-off-by: Claude <noreply@anthropic.com>
Extract the three summary sections (longest decode paths, strongest/weakest
signals) from the Map tab into a new dedicated Statistics tab. Add new
analytics: decode counters, unique stations/grids, decode rate, decode-by-type
breakdown, band activity, per-receiver comparison, and DX distance histogram.
The Statistics panel has its own receiver and history filters independent of
the map view.
https://claude.ai/code/session_01R9T4Byg7uw6qpkTsyVJd9k
Signed-off-by: Claude <noreply@anthropic.com>
Add bandplan_enabled (default: true) and bandplan_region (default:
"iaru_r1") fields to [frontends.http] config section, allowing the
operator to control the initial bandplan display setting from the
server config rather than requiring each browser session to configure
it manually. The server-provided default is applied on first connect
only when the user has no existing localStorage override.
https://claude.ai/code/session_01H7427hzbJepJzkoUJzoDmH
Signed-off-by: Claude <noreply@anthropic.com>
Previously R would retune even when the frequency was already aligned
to the jog step boundary. Now it shows "Already on step" and sends no
command. Also remove the stale "retune" label from the shortcut help.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move bandplan segment rendering from DOM elements to WebGL, drawing
coloured rectangles at the bottom of the spectrum canvas (above the
waterfall). All segments are batched into a single drawTriangles() call
for efficiency. The DOM strip is reparented into .spectrum-wrap and
restyled as a transparent text-label overlay (bp-webgl class). Non-SDR
rigs without a spectrum canvas retain the original DOM-coloured fallback.
https://claude.ai/code/session_01XTizHhXbXSAPQVAf1j9CSF
Signed-off-by: Claude <noreply@anthropic.com>
Move the bandplan strip out of the SDR-only spectrum panel into the
always-visible signal-visual-block. Add bandplanComputeRange() that
derives a frequency range from the current tuned frequency and band
edges when no spectrum data is available (non-SDR rigs). Trigger
bandplan updates on frequency changes and from the overview draw loop.
https://claude.ai/code/session_01AyBktp6b8qFjchyyqwL7dv
Signed-off-by: Claude <noreply@anthropic.com>
Add a bandplan display strip that shows IARU frequency allocations
(CW, Phone, Digital, FM, Beacon, Satellite) above the spectrum plot.
Includes IARU Region 1/2/3 data for all HF/VHF/UHF bands, a settings
submenu for region selection and label toggle, and color-coded segments
that pan/zoom with the spectrum view.
https://claude.ai/code/session_01AyBktp6b8qFjchyyqwL7dv
Signed-off-by: Claude <noreply@anthropic.com>
Combine round (R) and retune (T) into a single R hotkey that rounds to
the nearest jog step boundary, or retunes if already rounded. Update F
hotkey description to "Pick frequency" in the F1 help overlay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Server-side:
- Cache index_html() with OnceLock (avoids 3 string replacements per request)
- Pre-compress all static assets (JS/CSS/HTML) with gzip at startup, serve
cached bytes with ETag + Cache-Control headers for browser caching
- Add If-None-Match / 304 Not Modified support for conditional GETs
- Serialize SSE state+meta in single serde pass via SnapshotWithMeta,
eliminating the serialize → parse → flatten → re-serialize round-trip
- Add Cache-Control: immutable for favicon/logo (never change)
Client-side:
- Replace atob() + charCodeAt loop with direct base64 lookup-table decoder
that writes to a reusable Int8Array (avoids UTF-16 string allocation)
- Spectrum bins now flow as Int8Array throughout the pipeline, reducing
waterfall row memory from ~8 bytes/element to 1 byte/element
- Add isBinsArray() helper to support both Array and TypedArray in all
spectrum/waterfall guard checks
https://claude.ai/code/session_01J3VCWZeEPsyFJiHjJRBREo
Signed-off-by: Claude <noreply@anthropic.com>
The render() function runs on every SSE event (5-20×/sec) and was
unconditionally writing to decoder toggle buttons and About-tab
decoder status elements — 8 getElementById calls + 32 DOM property
writes per frame — even when values hadn't changed. This caused
unnecessary style recalculation overhead on every SSE frame,
contributing to spectrum stuttering.
Changes:
- Cache all 7 decoder toggle button elements at module init instead
of calling getElementById on every render() call
- Track last-written enabled state per button; skip DOM writes when
the value is unchanged (steady-state cost: 0 DOM writes per frame)
- Same pattern for 8 About-tab decoder status elements
- Gate updateSatLiveState className/textContent writes on value change
Net effect: eliminates ~50 unnecessary DOM operations per SSE frame
during normal operation (decoders rarely toggle).
https://claude.ai/code/session_01G6wuNCkckbHHsU7w5zCtW2
Signed-off-by: Claude <noreply@anthropic.com>
Six hot-path optimizations that reduce per-frame CPU cost:
1. Waterfall color LUT: Pre-compute a 256-entry RGBA lookup table
(bins are i8 = 256 possible values) instead of calling
waterfallColorRgba() per-pixel with HSL→RGB math + Math.pow().
Eliminates ~2000+ HSL conversions per frame across both waterfalls.
2. Noise floor O(N)→O(N log N): Replace .slice().sort() with an
in-place quickselect algorithm for 15th-percentile estimation.
For 1024 bins this is ~10× faster.
3. Reuse spectrum bin buffers: SSE handler and buildSpectrumRenderData
now reuse pre-allocated arrays instead of creating new Array(N)
and .map() allocations every frame. Reduces GC pressure.
4. Cache canvas dimensions: drawSpectrum and drawSpectrumWaterfall
read cached CSS dimensions instead of querying clientWidth/
clientHeight every frame (which forces layout recalculation).
Dimensions refreshed on resize and layout changes.
5. Cache DOM references: getElementById calls for zoom indicator and
minimap elements moved to module-level constants instead of
querying the DOM on every drawSpectrum call.
6. Efficient array trimming: Peak hold pruning uses in-place splice
from front instead of .filter() (which allocates a new array).
Waterfall row trimming uses splice instead of repeated .shift().
https://claude.ai/code/session_01G6wuNCkckbHHsU7w5zCtW2
Signed-off-by: Claude <noreply@anthropic.com>
Three issues in the satellite predictions view caused page-wide
rendering performance degradation:
1. Unbounded DOM nodes: All satellite passes (200+ satellites × multiple
passes = 500-1000 rows with 5 spans each) were rendered at once,
creating thousands of DOM nodes that slowed style recalculation and
layout across the entire page. Now caps at 50 visible rows with a
"Show more" button.
2. No DOM cleanup on view switch: Prediction rows persisted in the DOM
when navigating away from the predictions view or the SAT tab,
bloating the page DOM indefinitely. Now clears prediction DOM when
leaving the predictions view or switching decoder tabs.
3. Countdown timer never paused: The 1-second setInterval with
querySelectorAll kept running even when the predictions view was
hidden, wasting CPU on invisible DOM queries. Now only runs when
predictions view is active, caches element references instead of
querying the DOM each tick, and auto-pauses when the view is hidden.
Also caches prediction DOM element references at module init instead
of calling getElementById on every render invocation.
https://claude.ai/code/session_01G6wuNCkckbHHsU7w5zCtW2
Signed-off-by: Claude <noreply@anthropic.com>
applyRigList() was called on every SSE state update (since `remotes`
is always present in the payload), and it unconditionally called
bmFetch() which fires 2x GET /bookmarks (list + overlay). At the
default poll rate this generated ~20 bookmark fetches/second — visible
as constant GET /bookmarks traffic on each spectrum render cycle.
Now track the previous rig list + active rig as a key and only
re-fetch bookmarks (and re-init scheduler/background-decode) when
the rig list actually changes.
https://claude.ai/code/session_017g7VNMb6CChaiWrfzVBhbR
Signed-off-by: Claude <noreply@anthropic.com>
- Add NOAA-15/18/19 and Meteor-M N2-3/N2-4 to predictions list
- Rename PREDICTION_SATS (was HAM_SATS) to include weather + ham sats
- Rename all wxsat identifiers to sat throughout JS/HTML/CSS/Rust:
wxsat.js → sat.js, WXSAT_JS → SAT_JS, /wxsat.js route → /sat.js,
all #wxsat-* element IDs, .wxsat-* CSS classes, window.addWxsat* →
window.addSat*, window.onServerWxsatImage → window.onServerSatImage,
etc. (backend protocol strings unchanged)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Rename "Weather Satellites" sub-tab to "SAT"
- Add "Predictions" view: next 24 h flyby table for 13 ham sats
(ISS, AO-91, AO-92, SO-50, AO-73, JO-97, PO-101, LilacSat-2,
CAS-4B, EO-88, RS-44, SALSAT, GREENCUBE)
- trx-core/geo: add PassPrediction, HAM_SATS, compute_upcoming_passes(),
find_passes_for_sat(), compute_az_el() helpers; spawn_tle_refresh_task
now also fetches CelesTrak amateur group on startup and every 24 h
- trx-frontend-http: add GET /sat_passes endpoint
- app.js: locator tooltips now accumulate all receivers per station
via remotes Set; _detailPassesRigFilter checks the Set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SGP4-based geo-referencing for NOAA APT and Meteor LRPT decoded
satellite images, enabling them to be displayed as semi-transparent
overlays on the Leaflet map module with ground track polylines.
Changes:
- Add sgp4 crate dependency to trx-core for orbital propagation
- New trx-core/src/geo.rs module with TLE-based pass geo-referencing,
ECI-to-geodetic conversion, and station-location fallback estimation
- Extend WxsatImage and LrptImage structs with geo_bounds and
ground_track optional fields (backward compatible via serde defaults)
- Compute geo-bounds in finalize_wxsat_pass and finalize_lrpt_pass
using satellite identity, pass timestamps, and station coordinates
- Add 'wxsat' source filter to the map module (off by default)
- Add L.imageOverlay rendering with popup and ground track polyline
- Add "Show on Map" buttons in wxsat plugin live/history views
https://claude.ai/code/session_01DUCfb9CjGoViwBrznpfWyt
Signed-off-by: Claude <noreply@anthropic.com>
Replace flat image list with two switchable views:
- Live: decoder state cards (Idle/Listening), descriptions, latest image
- History: filterable table with columns for time, type, satellite,
channels, lines, and download link. Supports text filter, type filter
(All/APT/LRPT), and sort order (newest/oldest).
https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
Restructure trx-wxsat into noaa/ (APT) and lrpt/ (Meteor-M LRPT) submodules
with shared crate base. Add QPSK demodulator, CCSDS CADU framer, MCU channel
assembler for LRPT. Wire LRPT through full stack (core types, protocol, server
decoder task, client). Add Weather Satellites sub-tab in Digital Modes with
toggle buttons for NOAA APT and Meteor LRPT, descriptions, and image history.
https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
Statistics panels (longest paths, strongest/weakest signals) now respect
all active map filters — source type, rig selector, band, search, and
history. Locator tooltips display which rig received each decoded frame.
https://claude.ai/code/session_01LT7zBnb2kQiYpeTuWNXHsT
Signed-off-by: Claude <noreply@anthropic.com>
Remap retune from R to T, and add a new R hotkey that rounds the
current frequency forward to the next jog-step boundary. Both the
new hotkey and the remapped retune are documented in the F1 help
overlay.
https://claude.ai/code/session_017neG2jL9uXFSRpmhyS1EqG
Signed-off-by: Claude <noreply@anthropic.com>
Remove rig_id filtering from dispatchDecodeMessage and dispatchDecodeBatch
so that decode data from all rigs (including remote/non-primary) flows into
the map. Also remove the rig_filter query param from decode history fetch
so all history is loaded. The existing map rig filter dropdown handles
visibility filtering via marker.__trxRigIds.
https://claude.ai/code/session_01GGvdXKdEbRBnJa2BjAQuVB
Signed-off-by: Claude <noreply@anthropic.com>
Change RigRxStatus.sig from i32 to f64 and add get_signal_strength_db
to RigCat trait so SDR backends can bypass the coarse 0..15 quantisation.
Compensate for decimation processing gain so the meter matches the
spectrum peak. Display with one decimal place in all units.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase sig-strength-display min-width to 7.5rem so the field no longer
resizes when the value switches between two-digit and three-digit numbers.
Reposition the fast BW overlay immediately when bandwidth changes arrive
via SSE, and force-display on bookmark apply so freq+bw render atomically
instead of the BW bars wiggling from a stale intermediate state.
https://claude.ai/code/session_01R2XBFEBL8CrsTx5inu25MA
Signed-off-by: Claude <noreply@anthropic.com>
SSE status updates called applyLocalTunedFrequency with forceDisplay=true,
clearing the freqDirty flag on every update and overwriting user input mid-
typing. Remove forceDisplay from SSE path so the dirty flag is respected.
Skip applyLocalTunedFrequency entirely when frequency hasn't changed to
avoid redundant spectrum redraws and overlay repositioning on every SSE
frame. Only trigger scheduleSpectrumDraw when frequency actually changes.
Add blur and Escape handlers on frequency inputs to cleanly exit editing
mode when the user abandons input.
https://claude.ai/code/session_01H2VMATj29FPgR64t9YMdSR
Signed-off-by: Claude <noreply@anthropic.com>
Add a new "Sig Strength" display field in the freq row that shows
the measured signal strength. Clicking the field cycles through
three units: dBFS (default), dBf, and dBm. The selected unit is
persisted in localStorage.
https://claude.ai/code/session_01EvRV8UgsVtbrcH4t2hmFBF
Signed-off-by: Claude <noreply@anthropic.com>
Estimate Co-Channel Interference (CCI) from pilot tone quadrature
leakage and coherence degradation. Estimate Adjacent Channel
Interference (ACI) from CMA equalizer tap deviation from identity.
Both metrics (0-100 scale) are surfaced through RigFilterState and
displayed as colour-coded bars in the WFM control panel.
The RDS decoder quality parameter is now adaptively penalised when
CCI/ACI levels are elevated, reducing block-error rate under
interference conditions.
https://claude.ai/code/session_016EKzep42RCvE4GxvvRaCwu
Signed-off-by: Claude <noreply@anthropic.com>