[feat](trx-client): add configurable decode history retention

Add a default decode history retention setting in minutes with per-rig overrides, resolve the active rig retention in the HTTP frontend runtime, and apply that retention consistently across backend decode history buffers and frontend decoder views. This removes fixed APRS, HF APRS, AIS, VDES, FT8, and WSPR browser-side history caps in favor of time-based pruning, and includes the pending longest-QSO card style reset.

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

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 17:18:09 +01:00
parent 86768c8e7f
commit a924902074
12 changed files with 306 additions and 51 deletions
+61 -1
View File
@@ -247,6 +247,10 @@ pub struct HttpFrontendConfig {
pub spectrum_usable_span_ratio: f32, pub spectrum_usable_span_ratio: f32,
/// Whether to expose the RF Gain control in the web UI. /// Whether to expose the RF Gain control in the web UI.
pub show_sdr_gain_control: bool, pub show_sdr_gain_control: bool,
/// Default decode history retention in minutes for the active rig.
pub decode_history_retention_min: u64,
/// Optional per-rig decode history retention overrides in minutes.
pub decode_history_retention_min_by_rig: HashMap<String, u64>,
/// Authentication settings /// Authentication settings
pub auth: HttpAuthConfig, pub auth: HttpAuthConfig,
} }
@@ -262,6 +266,8 @@ impl Default for HttpFrontendConfig {
spectrum_coverage_margin_hz: 50_000, spectrum_coverage_margin_hz: 50_000,
spectrum_usable_span_ratio: 0.92, spectrum_usable_span_ratio: 0.92,
show_sdr_gain_control: true, show_sdr_gain_control: true,
decode_history_retention_min: 24 * 60,
decode_history_retention_min_by_rig: HashMap::new(),
auth: HttpAuthConfig::default(), auth: HttpAuthConfig::default(),
} }
} }
@@ -388,6 +394,25 @@ impl ClientConfig {
"[frontends.http].spectrum_usable_span_ratio must be > 0.0 and <= 1.0".to_string(), "[frontends.http].spectrum_usable_span_ratio must be > 0.0 and <= 1.0".to_string(),
); );
} }
if self.frontends.http.decode_history_retention_min == 0 {
return Err(
"[frontends.http].decode_history_retention_min must be > 0".to_string(),
);
}
for (rig_id, minutes) in &self.frontends.http.decode_history_retention_min_by_rig {
if rig_id.trim().is_empty() {
return Err(
"[frontends.http].decode_history_retention_min_by_rig keys must not be empty"
.to_string(),
);
}
if *minutes == 0 {
return Err(format!(
"[frontends.http].decode_history_retention_min_by_rig[\"{}\"] must be > 0",
rig_id
));
}
}
if self.frontends.rigctl.enabled && self.frontends.rigctl.rig_ports.is_empty() { if self.frontends.rigctl.enabled && self.frontends.rigctl.rig_ports.is_empty() {
return Err( return Err(
"[frontends.rigctl].rig_ports must contain at least one rig when enabled" "[frontends.rigctl].rig_ports must contain at least one rig when enabled"
@@ -487,6 +512,8 @@ impl ClientConfig {
spectrum_coverage_margin_hz: 50_000, spectrum_coverage_margin_hz: 50_000,
spectrum_usable_span_ratio: 0.92, spectrum_usable_span_ratio: 0.92,
show_sdr_gain_control: true, show_sdr_gain_control: true,
decode_history_retention_min: 24 * 60,
decode_history_retention_min_by_rig: HashMap::new(),
auth: HttpAuthConfig { auth: HttpAuthConfig {
enabled: false, enabled: false,
rx_passphrase: Some("rx-passphrase-example".to_string()), rx_passphrase: Some("rx-passphrase-example".to_string()),
@@ -588,13 +615,24 @@ mod tests {
assert_eq!(config.frontends.http.initial_map_zoom, 10); assert_eq!(config.frontends.http.initial_map_zoom, 10);
assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 50_000); assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 50_000);
assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 0.92); assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 0.92);
assert_eq!(config.frontends.http.decode_history_retention_min, 1440);
assert!(
config
.frontends
.http
.decode_history_retention_min_by_rig
.is_empty()
);
assert_eq!(config.frontends.rigctl.port, 4532); assert_eq!(config.frontends.rigctl.port, 4532);
assert!(config.frontends.http_json.enabled); assert!(config.frontends.http_json.enabled);
assert_eq!(config.frontends.http_json.port, 0); assert_eq!(config.frontends.http_json.port, 0);
assert!(config.remote.url.is_none()); assert!(config.remote.url.is_none());
assert!(config.general.website_url.is_none()); assert!(config.general.website_url.is_none());
assert!(config.general.website_name.is_none()); assert!(config.general.website_name.is_none());
assert!(config.general.ais_vessel_url_base.is_none()); assert_eq!(
config.general.ais_vessel_url_base,
Some("https://www.vesselfinder.com/?mmsi=".to_string())
);
assert_eq!(config.remote.poll_interval_ms, 750); assert_eq!(config.remote.poll_interval_ms, 750);
assert!(config.frontends.audio.enabled); assert!(config.frontends.audio.enabled);
assert_eq!(config.frontends.audio.server_port, 4531); assert_eq!(config.frontends.audio.server_port, 4531);
@@ -626,6 +664,11 @@ port = 8080
initial_map_zoom = 12 initial_map_zoom = 12
spectrum_coverage_margin_hz = 40000 spectrum_coverage_margin_hz = 40000
spectrum_usable_span_ratio = 0.9 spectrum_usable_span_ratio = 0.9
decode_history_retention_min = 720
[frontends.http.decode_history_retention_min_by_rig]
vhf = 180
uhf = 60
"#; "#;
@@ -648,6 +691,23 @@ spectrum_usable_span_ratio = 0.9
assert_eq!(config.frontends.http.initial_map_zoom, 12); assert_eq!(config.frontends.http.initial_map_zoom, 12);
assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 40_000); assert_eq!(config.frontends.http.spectrum_coverage_margin_hz, 40_000);
assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 0.9); assert_eq!(config.frontends.http.spectrum_usable_span_ratio, 0.9);
assert_eq!(config.frontends.http.decode_history_retention_min, 720);
assert_eq!(
config
.frontends
.http
.decode_history_retention_min_by_rig
.get("vhf"),
Some(&180)
);
assert_eq!(
config
.frontends
.http
.decode_history_retention_min_by_rig
.get("uhf"),
Some(&60)
);
} }
#[test] #[test]
+4
View File
@@ -183,6 +183,10 @@ async fn async_init() -> DynResult<AppState> {
cfg.frontends.http.spectrum_coverage_margin_hz; cfg.frontends.http.spectrum_coverage_margin_hz;
frontend_runtime.http_spectrum_usable_span_ratio = frontend_runtime.http_spectrum_usable_span_ratio =
cfg.frontends.http.spectrum_usable_span_ratio; cfg.frontends.http.spectrum_usable_span_ratio;
frontend_runtime.http_decode_history_retention_min =
cfg.frontends.http.decode_history_retention_min;
frontend_runtime.http_decode_history_retention_min_by_rig =
cfg.frontends.http.decode_history_retention_min_by_rig.clone();
// Resolve remote URL: CLI > config [remote] section > error // Resolve remote URL: CLI > config [remote] section > error
let remote_url = cli let remote_url = cli
+6
View File
@@ -236,6 +236,10 @@ pub struct FrontendRuntimeContext {
pub http_spectrum_coverage_margin_hz: u32, pub http_spectrum_coverage_margin_hz: u32,
/// Fraction of the sampled spectrum span treated as usable by the web UI. /// Fraction of the sampled spectrum span treated as usable by the web UI.
pub http_spectrum_usable_span_ratio: f32, pub http_spectrum_usable_span_ratio: f32,
/// Default decode history retention in minutes.
pub http_decode_history_retention_min: u64,
/// Per-rig decode history retention overrides in minutes.
pub http_decode_history_retention_min_by_rig: HashMap<String, u64>,
/// Currently selected remote rig id (used by remote client routing). /// Currently selected remote rig id (used by remote client routing).
pub remote_active_rig_id: Arc<Mutex<Option<String>>>, pub remote_active_rig_id: Arc<Mutex<Option<String>>>,
/// Cached remote rig list from GetRigs polling. /// Cached remote rig list from GetRigs polling.
@@ -296,6 +300,8 @@ impl FrontendRuntimeContext {
http_initial_map_zoom: 10, http_initial_map_zoom: 10,
http_spectrum_coverage_margin_hz: 50_000, http_spectrum_coverage_margin_hz: 50_000,
http_spectrum_usable_span_ratio: 0.92, http_spectrum_usable_span_ratio: 0.92,
http_decode_history_retention_min: 24 * 60,
http_decode_history_retention_min_by_rig: HashMap::new(),
remote_active_rig_id: Arc::new(Mutex::new(None)), remote_active_rig_id: Arc::new(Mutex::new(None)),
remote_rigs: Arc::new(Mutex::new(Vec::new())), remote_rigs: Arc::new(Mutex::new(Vec::new())),
owner_callsign: None, owner_callsign: None,
@@ -347,10 +347,27 @@ const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"
const headerStylePickSelect = document.getElementById("header-style-pick-select"); const headerStylePickSelect = document.getElementById("header-style-pick-select");
const rdsPsOverlay = document.getElementById("rds-ps-overlay"); const rdsPsOverlay = document.getElementById("rds-ps-overlay");
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000)); let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
let decodeHistoryRetentionMin = 24 * 60;
let primaryRds = null; let primaryRds = null;
let vchanRdsById = new Map(); let vchanRdsById = new Map();
let rdsOverlayEntries = []; let rdsOverlayEntries = [];
function currentDecodeHistoryRetentionMs() {
const minutes = Math.max(1, Math.round(Number(decodeHistoryRetentionMin) || (24 * 60)));
return minutes * 60 * 1000;
}
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() {
if (typeof window.pruneAprsHistoryView === "function") window.pruneAprsHistoryView();
if (typeof window.pruneHfAprsHistoryView === "function") window.pruneHfAprsHistoryView();
if (typeof window.pruneAisHistoryView === "function") window.pruneAisHistoryView();
if (typeof window.pruneVdesHistoryView === "function") window.pruneVdesHistoryView();
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
};
function syncTopBarAccess() { function syncTopBarAccess() {
const loggedOut = authEnabled && !authRole; const loggedOut = authEnabled && !authRole;
const tabBar = document.getElementById("tab-bar"); const tabBar = document.getElementById("tab-bar");
@@ -2441,6 +2458,19 @@ function render(update) {
) { ) {
spectrumUsableSpanRatio = Math.max(0.01, Math.min(1.0, Number(update.spectrum_usable_span_ratio))); spectrumUsableSpanRatio = Math.max(0.01, Math.min(1.0, Number(update.spectrum_usable_span_ratio)));
} }
if (
typeof update.decode_history_retention_min === "number" &&
Number.isFinite(update.decode_history_retention_min) &&
update.decode_history_retention_min > 0
) {
const nextRetentionMin = Math.max(1, Math.round(Number(update.decode_history_retention_min)));
if (nextRetentionMin !== decodeHistoryRetentionMin) {
decodeHistoryRetentionMin = nextRetentionMin;
if (typeof window.applyDecodeHistoryRetention === "function") {
window.applyDecodeHistoryRetention();
}
}
}
scheduleSpectrumLayout(); scheduleSpectrumLayout();
updateTitle(); updateTitle();
updateFooterBuildInfo(); updateFooterBuildInfo();
@@ -8,7 +8,6 @@ const aisBarOverlay = document.getElementById("ais-bar-overlay");
const aisChannelSummaryEl = document.getElementById("ais-channel-summary"); const aisChannelSummaryEl = document.getElementById("ais-channel-summary");
const aisVesselCountEl = document.getElementById("ais-vessel-count"); const aisVesselCountEl = document.getElementById("ais-vessel-count");
const aisLatestSeenEl = document.getElementById("ais-latest-seen"); const aisLatestSeenEl = document.getElementById("ais-latest-seen");
const AIS_MAX_MESSAGES = 200;
const AIS_BAR_WINDOW_MS = 15 * 60 * 1000; const AIS_BAR_WINDOW_MS = 15 * 60 * 1000;
const AIS_DEFAULT_A_HZ = 161_975_000; const AIS_DEFAULT_A_HZ = 161_975_000;
const AIS_CHANNEL_SPACING_HZ = 50_000; const AIS_CHANNEL_SPACING_HZ = 50_000;
@@ -17,6 +16,17 @@ let aisMessageHistory = [];
let aisPaused = false; let aisPaused = false;
let aisBufferedWhilePaused = 0; let aisBufferedWhilePaused = 0;
function currentAisHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneAisMessageHistory() {
const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
aisMessageHistory = aisMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
}
function scheduleAisUi(key, job) { function scheduleAisUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job); window.trxScheduleUiFrameJob(key, job);
@@ -295,6 +305,7 @@ window.resetAisHistoryView = function() {
}; };
function renderAisHistory() { function renderAisHistory() {
pruneAisMessageHistory();
if (!aisMessagesEl || aisPaused) { if (!aisMessagesEl || aisPaused) {
updateAisSummary(); updateAisSummary();
return; return;
@@ -317,7 +328,7 @@ function addAisMessage(msg) {
}); });
aisMessageHistory.unshift(msg); aisMessageHistory.unshift(msg);
if (aisMessageHistory.length > AIS_MAX_MESSAGES) aisMessageHistory.length = AIS_MAX_MESSAGES; pruneAisMessageHistory();
scheduleAisBarUpdate(); scheduleAisBarUpdate();
if (aisPaused) { if (aisPaused) {
@@ -332,6 +343,12 @@ function addAisMessage(msg) {
} }
} }
window.pruneAisHistoryView = function() {
pruneAisMessageHistory();
updateAisBar();
renderAisHistory();
};
if (aisClearBtn) { if (aisClearBtn) {
aisClearBtn.addEventListener("click", async () => { aisClearBtn.addEventListener("click", async () => {
try { try {
@@ -10,7 +10,6 @@ const aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
const aprsTotalCountEl = document.getElementById("aprs-total-count"); const aprsTotalCountEl = document.getElementById("aprs-total-count");
const aprsVisibleCountEl = document.getElementById("aprs-visible-count"); const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
const aprsLatestSeenEl = document.getElementById("aprs-latest-seen"); const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
const APRS_MAX_PACKETS = 100;
const APRS_BAR_WINDOW_MS = 15 * 60 * 1000; const APRS_BAR_WINDOW_MS = 15 * 60 * 1000;
let aprsFilterText = ""; let aprsFilterText = "";
let aprsPacketHistory = []; let aprsPacketHistory = [];
@@ -21,6 +20,17 @@ let aprsHideCrc = false;
let aprsCollapseDup = false; let aprsCollapseDup = false;
let aprsTypeFilter = "all"; let aprsTypeFilter = "all";
function currentAprsHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneAprsPacketHistory() {
const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
aprsPacketHistory = aprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs);
}
function scheduleAprsUi(key, job) { function scheduleAprsUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job); window.trxScheduleUiFrameJob(key, job);
@@ -304,6 +314,7 @@ function renderAprsRow(pkt, isFresh) {
} }
function renderAprsHistory() { function renderAprsHistory() {
pruneAprsPacketHistory();
if (!aprsPacketsEl || aprsPaused) { if (!aprsPacketsEl || aprsPaused) {
updateAprsSummary(); updateAprsSummary();
updateAprsChipState(); updateAprsChipState();
@@ -359,13 +370,19 @@ window.resetAprsHistoryView = function() {
if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs"); if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs");
}; };
window.pruneAprsHistoryView = function() {
pruneAprsPacketHistory();
updateAprsBar();
renderAprsHistory();
};
function addAprsPacket(pkt) { function addAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now(); const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs; pkt._tsMs = tsMs;
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
aprsPacketHistory.unshift(pkt); aprsPacketHistory.unshift(pkt);
if (aprsPacketHistory.length > APRS_MAX_PACKETS) aprsPacketHistory.length = APRS_MAX_PACKETS; pruneAprsPacketHistory();
if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) { if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode, pkt); window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode, pkt);
@@ -5,7 +5,6 @@ const ft8MessagesEl = document.getElementById("ft8-messages");
const ft8FilterInput = document.getElementById("ft8-filter"); const ft8FilterInput = document.getElementById("ft8-filter");
const ft8PauseBtn = document.getElementById("ft8-pause-btn"); const ft8PauseBtn = document.getElementById("ft8-pause-btn");
const ft8BarOverlay = document.getElementById("ft8-bar-overlay"); const ft8BarOverlay = document.getElementById("ft8-bar-overlay");
const FT8_MAX_MESSAGES = 200;
const FT8_BAR_WINDOW_MS = 15 * 60 * 1000; const FT8_BAR_WINDOW_MS = 15 * 60 * 1000;
const FT8_PERIOD_SECONDS = 15; const FT8_PERIOD_SECONDS = 15;
let ft8FilterText = ""; let ft8FilterText = "";
@@ -13,6 +12,17 @@ let ft8MessageHistory = [];
let ft8Paused = false; let ft8Paused = false;
let ft8BufferedWhilePaused = 0; let ft8BufferedWhilePaused = 0;
function currentFt8HistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneFt8MessageHistory() {
const cutoffMs = Date.now() - currentFt8HistoryRetentionMs();
ft8MessageHistory = ft8MessageHistory.filter((msg) => Number(msg?._tsMs ?? msg?.ts_ms) >= cutoffMs);
}
function scheduleFt8Ui(key, job) { function scheduleFt8Ui(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job); window.trxScheduleUiFrameJob(key, job);
@@ -78,6 +88,7 @@ function updateFt8PauseUi() {
} }
function renderFt8History() { function renderFt8History() {
pruneFt8MessageHistory();
if (!ft8MessagesEl || ft8Paused) { if (!ft8MessagesEl || ft8Paused) {
updateFt8PauseUi(); updateFt8PauseUi();
return; return;
@@ -91,8 +102,9 @@ function renderFt8History() {
} }
function addFt8Message(msg) { function addFt8Message(msg) {
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
ft8MessageHistory.unshift(msg); ft8MessageHistory.unshift(msg);
if (ft8MessageHistory.length > FT8_MAX_MESSAGES) ft8MessageHistory.length = FT8_MAX_MESSAGES; pruneFt8MessageHistory();
scheduleFt8BarUpdate(); scheduleFt8BarUpdate();
if (ft8Paused) { if (ft8Paused) {
ft8BufferedWhilePaused += 1; ft8BufferedWhilePaused += 1;
@@ -102,6 +114,12 @@ function addFt8Message(msg) {
scheduleFt8HistoryRender(); scheduleFt8HistoryRender();
} }
window.pruneFt8HistoryView = function() {
pruneFt8MessageHistory();
updateFt8Bar();
renderFt8History();
};
function ft8BarRfText(msg) { function ft8BarRfText(msg) {
const displayFreqHz = normalizeFt8DisplayFreqHz(msg.freq_hz); const displayFreqHz = normalizeFt8DisplayFreqHz(msg.freq_hz);
if (!Number.isFinite(displayFreqHz)) return null; if (!Number.isFinite(displayFreqHz)) return null;
@@ -9,7 +9,6 @@ const hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn")
const hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count"); const hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count");
const hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count"); const hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count");
const hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen"); const hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen");
const HF_APRS_MAX_PACKETS = 100;
let hfAprsFilterText = ""; let hfAprsFilterText = "";
let hfAprsPacketHistory = []; let hfAprsPacketHistory = [];
let hfAprsPaused = false; let hfAprsPaused = false;
@@ -19,6 +18,17 @@ let hfAprsHideCrc = false;
let hfAprsCollapseDup = false; let hfAprsCollapseDup = false;
let hfAprsTypeFilter = "all"; let hfAprsTypeFilter = "all";
function currentHfAprsHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneHfAprsPacketHistory() {
const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs();
hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs);
}
function scheduleHfAprsHistoryRender() { function scheduleHfAprsHistoryRender() {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory()); window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory());
@@ -296,6 +306,7 @@ function renderHfAprsRow(pkt, isFresh) {
} }
function renderHfAprsHistory() { function renderHfAprsHistory() {
pruneHfAprsPacketHistory();
if (!hfAprsPacketsEl || hfAprsPaused) { if (!hfAprsPacketsEl || hfAprsPaused) {
updateHfAprsSummary(); updateHfAprsSummary();
updateHfAprsChipState(); updateHfAprsChipState();
@@ -318,13 +329,18 @@ window.resetHfAprsHistoryView = function() {
renderHfAprsHistory(); renderHfAprsHistory();
}; };
window.pruneHfAprsHistoryView = function() {
pruneHfAprsPacketHistory();
renderHfAprsHistory();
};
function addHfAprsPacket(pkt) { function addHfAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now(); const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs; pkt._tsMs = tsMs;
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
hfAprsPacketHistory.unshift(pkt); hfAprsPacketHistory.unshift(pkt);
if (hfAprsPacketHistory.length > HF_APRS_MAX_PACKETS) hfAprsPacketHistory.length = HF_APRS_MAX_PACKETS; pruneHfAprsPacketHistory();
if (hfAprsPaused) { if (hfAprsPaused) {
hfAprsBufferedWhilePaused += 1; hfAprsBufferedWhilePaused += 1;
@@ -8,13 +8,23 @@ const vdesBarOverlay = document.getElementById("vdes-bar-overlay");
const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary"); const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
const vdesFrameCountEl = document.getElementById("vdes-frame-count"); const vdesFrameCountEl = document.getElementById("vdes-frame-count");
const vdesLatestSeenEl = document.getElementById("vdes-latest-seen"); const vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
const VDES_MAX_MESSAGES = 200;
const VDES_BAR_WINDOW_MS = 15 * 60 * 1000; const VDES_BAR_WINDOW_MS = 15 * 60 * 1000;
let vdesFilterText = ""; let vdesFilterText = "";
let vdesMessageHistory = []; let vdesMessageHistory = [];
let vdesPaused = false; let vdesPaused = false;
let vdesBufferedWhilePaused = 0; let vdesBufferedWhilePaused = 0;
function currentVdesHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneVdesMessageHistory() {
const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
vdesMessageHistory = vdesMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
}
function scheduleVdesUi(key, job) { function scheduleVdesUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job); window.trxScheduleUiFrameJob(key, job);
@@ -60,6 +70,7 @@ function vdesHexPreview(rawBytes) {
} }
function updateVdesSummary() { function updateVdesSummary() {
pruneVdesMessageHistory();
if (vdesChannelSummaryEl) { if (vdesChannelSummaryEl) {
vdesChannelSummaryEl.textContent = currentVdesCenterText(); vdesChannelSummaryEl.textContent = currentVdesCenterText();
} }
@@ -228,6 +239,7 @@ window.resetVdesHistoryView = function() {
}; };
function renderVdesHistory() { function renderVdesHistory() {
pruneVdesMessageHistory();
if (!vdesMessagesEl || vdesPaused) { if (!vdesMessagesEl || vdesPaused) {
updateVdesSummary(); updateVdesSummary();
return; return;
@@ -250,7 +262,7 @@ function addVdesMessage(msg) {
}); });
vdesMessageHistory.unshift(msg); vdesMessageHistory.unshift(msg);
if (vdesMessageHistory.length > VDES_MAX_MESSAGES) vdesMessageHistory.length = VDES_MAX_MESSAGES; pruneVdesMessageHistory();
scheduleVdesBarUpdate(); scheduleVdesBarUpdate();
if (vdesPaused) { if (vdesPaused) {
@@ -323,4 +335,10 @@ window.onServerVdes = function(msg) {
} }
}; };
window.pruneVdesHistoryView = function() {
pruneVdesMessageHistory();
updateVdesBar();
renderVdesHistory();
};
updateVdesSummary(); updateVdesSummary();
@@ -4,13 +4,23 @@ const wsprPeriodEl = document.getElementById("wspr-period");
const wsprMessagesEl = document.getElementById("wspr-messages"); const wsprMessagesEl = document.getElementById("wspr-messages");
const wsprFilterInput = document.getElementById("wspr-filter"); const wsprFilterInput = document.getElementById("wspr-filter");
const wsprPauseBtn = document.getElementById("wspr-pause-btn"); const wsprPauseBtn = document.getElementById("wspr-pause-btn");
const WSPR_MAX_MESSAGES = 200;
const WSPR_PERIOD_SECONDS = 120; const WSPR_PERIOD_SECONDS = 120;
let wsprFilterText = ""; let wsprFilterText = "";
let wsprMessageHistory = []; let wsprMessageHistory = [];
let wsprPaused = false; let wsprPaused = false;
let wsprBufferedWhilePaused = 0; let wsprBufferedWhilePaused = 0;
function currentWsprHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function"
? window.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneWsprMessageHistory() {
const cutoffMs = Date.now() - currentWsprHistoryRetentionMs();
wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg?._tsMs ?? msg?.ts_ms) >= cutoffMs);
}
function scheduleWsprHistoryRender() { function scheduleWsprHistoryRender() {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("wspr-history", () => renderWsprHistory()); window.trxScheduleUiFrameJob("wspr-history", () => renderWsprHistory());
@@ -59,6 +69,7 @@ function updateWsprPauseUi() {
} }
function renderWsprHistory() { function renderWsprHistory() {
pruneWsprMessageHistory();
if (!wsprMessagesEl || wsprPaused) { if (!wsprMessagesEl || wsprPaused) {
updateWsprPauseUi(); updateWsprPauseUi();
return; return;
@@ -72,8 +83,9 @@ function renderWsprHistory() {
} }
function addWsprMessage(msg) { function addWsprMessage(msg) {
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
wsprMessageHistory.unshift(msg); wsprMessageHistory.unshift(msg);
if (wsprMessageHistory.length > WSPR_MAX_MESSAGES) wsprMessageHistory.length = WSPR_MAX_MESSAGES; pruneWsprMessageHistory();
if (wsprPaused) { if (wsprPaused) {
wsprBufferedWhilePaused += 1; wsprBufferedWhilePaused += 1;
updateWsprPauseUi(); updateWsprPauseUi();
@@ -82,6 +94,11 @@ function addWsprMessage(msg) {
scheduleWsprHistoryRender(); scheduleWsprHistoryRender();
} }
window.pruneWsprHistoryView = function() {
pruneWsprMessageHistory();
renderWsprHistory();
};
function escapeWsprHtml(input) { function escapeWsprHtml(input) {
return input return input
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@@ -82,6 +82,7 @@ struct FrontendMeta {
initial_map_zoom: u8, initial_map_zoom: u8,
spectrum_coverage_margin_hz: u32, spectrum_coverage_margin_hz: u32,
spectrum_usable_span_ratio: f32, spectrum_usable_span_ratio: f32,
decode_history_retention_min: u64,
} }
#[get("/status")] #[get("/status")]
@@ -127,6 +128,10 @@ fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String {
extra.insert("initial_map_zoom".into(), serde_json::json!(meta.initial_map_zoom)); extra.insert("initial_map_zoom".into(), serde_json::json!(meta.initial_map_zoom));
extra.insert("spectrum_coverage_margin_hz".into(), serde_json::json!(meta.spectrum_coverage_margin_hz)); extra.insert("spectrum_coverage_margin_hz".into(), serde_json::json!(meta.spectrum_coverage_margin_hz));
extra.insert("spectrum_usable_span_ratio".into(), serde_json::json!(meta.spectrum_usable_span_ratio)); extra.insert("spectrum_usable_span_ratio".into(), serde_json::json!(meta.spectrum_usable_span_ratio));
extra.insert(
"decode_history_retention_min".into(),
serde_json::json!(meta.decode_history_retention_min),
);
// Serialize the extra map, strip its outer braces, and splice in. // Serialize the extra map, strip its outer braces, and splice in.
let extra_json = match serde_json::to_string(&extra) { let extra_json = match serde_json::to_string(&extra) {
@@ -156,6 +161,7 @@ fn frontend_meta_from_context(
initial_map_zoom: initial_map_zoom_from_context(context), initial_map_zoom: initial_map_zoom_from_context(context),
spectrum_coverage_margin_hz: spectrum_coverage_margin_hz_from_context(context), spectrum_coverage_margin_hz: spectrum_coverage_margin_hz_from_context(context),
spectrum_usable_span_ratio: spectrum_usable_span_ratio_from_context(context), spectrum_usable_span_ratio: spectrum_usable_span_ratio_from_context(context),
decode_history_retention_min: decode_history_retention_min_from_context(context),
} }
} }
@@ -217,6 +223,24 @@ fn spectrum_usable_span_ratio_from_context(context: &FrontendRuntimeContext) ->
context.http_spectrum_usable_span_ratio context.http_spectrum_usable_span_ratio
} }
fn decode_history_retention_min_from_context(context: &FrontendRuntimeContext) -> u64 {
let default_minutes = context.http_decode_history_retention_min.max(1);
let Some(active_rig_id) = context
.remote_active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
else {
return default_minutes;
};
context
.http_decode_history_retention_min_by_rig
.get(&active_rig_id)
.copied()
.filter(|minutes| *minutes > 0)
.unwrap_or(default_minutes)
}
#[get("/events")] #[get("/events")]
pub async fn events( pub async fn events(
state: web::Data<watch::Receiver<RigState>>, state: web::Data<watch::Receiver<RigState>>,
@@ -27,13 +27,6 @@ use trx_core::decode::{
}; };
use trx_frontend::FrontendRuntimeContext; use trx_frontend::FrontendRuntimeContext;
const HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60);
/// Maximum number of raw AIS messages kept in the ring buffer.
/// AIS vessels can transmit every 2 s, so without a cap the buffer grows
/// unboundedly. 10 000 entries covers ~100 active vessels at 2-second intervals
/// for ~3 minutes — enough for a realistic snapshot while bounding memory use.
const AIS_HISTORY_MAX: usize = 10_000;
fn current_timestamp_ms() -> i64 { fn current_timestamp_ms() -> i64 {
let millis = SystemTime::now() let millis = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -42,36 +35,68 @@ fn current_timestamp_ms() -> i64 {
i64::try_from(millis).unwrap_or(i64::MAX) i64::try_from(millis).unwrap_or(i64::MAX)
} }
fn prune_aprs_history(history: &mut VecDeque<(Instant, AprsPacket)>) { fn decode_history_retention(context: &FrontendRuntimeContext) -> Duration {
let default_minutes = context.http_decode_history_retention_min.max(1);
let minutes = context
.remote_active_rig_id
.lock()
.ok()
.and_then(|v| v.clone())
.and_then(|rig_id| {
context
.http_decode_history_retention_min_by_rig
.get(&rig_id)
.copied()
})
.filter(|minutes| *minutes > 0)
.unwrap_or(default_minutes);
Duration::from_secs(minutes.saturating_mul(60))
}
fn decode_history_cutoff(context: &FrontendRuntimeContext) -> Instant {
Instant::now() - decode_history_retention(context)
}
fn prune_aprs_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, AprsPacket)>) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
} }
} }
fn prune_hf_aprs_history(history: &mut VecDeque<(Instant, AprsPacket)>) { fn prune_hf_aprs_history(
context: &FrontendRuntimeContext,
history: &mut VecDeque<(Instant, AprsPacket)>,
) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
} }
} }
fn prune_ais_history(history: &mut VecDeque<(Instant, AisMessage)>) { fn prune_ais_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, AisMessage)>) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
} }
} }
fn prune_vdes_history(history: &mut VecDeque<(Instant, VdesMessage)>) { fn prune_vdes_history(
context: &FrontendRuntimeContext,
history: &mut VecDeque<(Instant, VdesMessage)>,
) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
@@ -87,10 +112,7 @@ fn record_ais(context: &FrontendRuntimeContext, mut msg: AisMessage) {
.lock() .lock()
.expect("ais history mutex poisoned"); .expect("ais history mutex poisoned");
history.push_back((Instant::now(), msg)); history.push_back((Instant::now(), msg));
prune_ais_history(&mut history); prune_ais_history(context, &mut history);
if history.len() > AIS_HISTORY_MAX {
history.pop_front();
}
} }
fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) { fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) {
@@ -102,30 +124,36 @@ fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) {
.lock() .lock()
.expect("vdes history mutex poisoned"); .expect("vdes history mutex poisoned");
history.push_back((Instant::now(), msg)); history.push_back((Instant::now(), msg));
prune_vdes_history(&mut history); prune_vdes_history(context, &mut history);
} }
fn prune_cw_history(history: &mut VecDeque<(Instant, CwEvent)>) { fn prune_cw_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, CwEvent)>) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
} }
} }
fn prune_ft8_history(history: &mut VecDeque<(Instant, Ft8Message)>) { fn prune_ft8_history(context: &FrontendRuntimeContext, history: &mut VecDeque<(Instant, Ft8Message)>) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
} }
} }
fn prune_wspr_history(history: &mut VecDeque<(Instant, WsprMessage)>) { fn prune_wspr_history(
context: &FrontendRuntimeContext,
history: &mut VecDeque<(Instant, WsprMessage)>,
) {
let cutoff = decode_history_cutoff(context);
while let Some((ts, _)) = history.front() { while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION { if *ts >= cutoff {
break; break;
} }
history.pop_front(); history.pop_front();
@@ -141,7 +169,7 @@ fn record_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
.lock() .lock()
.expect("aprs history mutex poisoned"); .expect("aprs history mutex poisoned");
history.push_back((Instant::now(), pkt)); history.push_back((Instant::now(), pkt));
prune_aprs_history(&mut history); prune_aprs_history(context, &mut history);
} }
fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) { fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
@@ -153,7 +181,7 @@ fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
.lock() .lock()
.expect("hf_aprs history mutex poisoned"); .expect("hf_aprs history mutex poisoned");
history.push_back((Instant::now(), pkt)); history.push_back((Instant::now(), pkt));
prune_hf_aprs_history(&mut history); prune_hf_aprs_history(context, &mut history);
} }
fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) { fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
@@ -162,7 +190,7 @@ fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
.lock() .lock()
.expect("cw history mutex poisoned"); .expect("cw history mutex poisoned");
history.push_back((Instant::now(), event)); history.push_back((Instant::now(), event));
prune_cw_history(&mut history); prune_cw_history(context, &mut history);
} }
fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) { fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
@@ -171,7 +199,7 @@ fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
.lock() .lock()
.expect("ft8 history mutex poisoned"); .expect("ft8 history mutex poisoned");
history.push_back((Instant::now(), msg)); history.push_back((Instant::now(), msg));
prune_ft8_history(&mut history); prune_ft8_history(context, &mut history);
} }
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) { fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
@@ -180,7 +208,7 @@ fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
.lock() .lock()
.expect("wspr history mutex poisoned"); .expect("wspr history mutex poisoned");
history.push_back((Instant::now(), msg)); history.push_back((Instant::now(), msg));
prune_wspr_history(&mut history); prune_wspr_history(context, &mut history);
} }
pub fn snapshot_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPacket> { pub fn snapshot_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPacket> {
@@ -188,7 +216,7 @@ pub fn snapshot_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPacket
.aprs_history .aprs_history
.lock() .lock()
.expect("aprs history mutex poisoned"); .expect("aprs history mutex poisoned");
prune_aprs_history(&mut history); prune_aprs_history(context, &mut history);
history.iter().map(|(_, pkt)| pkt.clone()).collect() history.iter().map(|(_, pkt)| pkt.clone()).collect()
} }
@@ -197,7 +225,7 @@ pub fn snapshot_hf_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPac
.hf_aprs_history .hf_aprs_history
.lock() .lock()
.expect("hf_aprs history mutex poisoned"); .expect("hf_aprs history mutex poisoned");
prune_hf_aprs_history(&mut history); prune_hf_aprs_history(context, &mut history);
history.iter().map(|(_, pkt)| pkt.clone()).collect() history.iter().map(|(_, pkt)| pkt.clone()).collect()
} }
@@ -213,7 +241,7 @@ pub fn snapshot_ais_history(context: &FrontendRuntimeContext) -> Vec<AisMessage>
.ais_history .ais_history
.lock() .lock()
.expect("ais history mutex poisoned"); .expect("ais history mutex poisoned");
prune_ais_history(&mut history); prune_ais_history(context, &mut history);
// Iterate oldest-first; later entries overwrite earlier ones so the // Iterate oldest-first; later entries overwrite earlier ones so the
// HashMap always holds the newest message per MMSI. // HashMap always holds the newest message per MMSI.
let mut latest: HashMap<u32, AisMessage> = HashMap::new(); let mut latest: HashMap<u32, AisMessage> = HashMap::new();
@@ -230,7 +258,7 @@ pub fn snapshot_vdes_history(context: &FrontendRuntimeContext) -> Vec<VdesMessag
.vdes_history .vdes_history
.lock() .lock()
.expect("vdes history mutex poisoned"); .expect("vdes history mutex poisoned");
prune_vdes_history(&mut history); prune_vdes_history(context, &mut history);
history.iter().map(|(_, msg)| msg.clone()).collect() history.iter().map(|(_, msg)| msg.clone()).collect()
} }
@@ -239,7 +267,7 @@ pub fn snapshot_cw_history(context: &FrontendRuntimeContext) -> Vec<CwEvent> {
.cw_history .cw_history
.lock() .lock()
.expect("cw history mutex poisoned"); .expect("cw history mutex poisoned");
prune_cw_history(&mut history); prune_cw_history(context, &mut history);
history.iter().map(|(_, evt)| evt.clone()).collect() history.iter().map(|(_, evt)| evt.clone()).collect()
} }
@@ -248,7 +276,7 @@ pub fn snapshot_ft8_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message>
.ft8_history .ft8_history
.lock() .lock()
.expect("ft8 history mutex poisoned"); .expect("ft8 history mutex poisoned");
prune_ft8_history(&mut history); prune_ft8_history(context, &mut history);
history.iter().map(|(_, msg)| msg.clone()).collect() history.iter().map(|(_, msg)| msg.clone()).collect()
} }
@@ -257,7 +285,7 @@ pub fn snapshot_wspr_history(context: &FrontendRuntimeContext) -> Vec<WsprMessag
.wspr_history .wspr_history
.lock() .lock()
.expect("wspr history mutex poisoned"); .expect("wspr history mutex poisoned");
prune_wspr_history(&mut history); prune_wspr_history(context, &mut history);
history.iter().map(|(_, msg)| msg.clone()).collect() history.iter().map(|(_, msg)| msg.clone()).collect()
} }