[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
@@ -347,10 +347,27 @@ const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"
const headerStylePickSelect = document.getElementById("header-style-pick-select");
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
let decodeHistoryRetentionMin = 24 * 60;
let primaryRds = null;
let vchanRdsById = new Map();
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() {
const loggedOut = authEnabled && !authRole;
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)));
}
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();
updateTitle();
updateFooterBuildInfo();
@@ -8,7 +8,6 @@ const aisBarOverlay = document.getElementById("ais-bar-overlay");
const aisChannelSummaryEl = document.getElementById("ais-channel-summary");
const aisVesselCountEl = document.getElementById("ais-vessel-count");
const aisLatestSeenEl = document.getElementById("ais-latest-seen");
const AIS_MAX_MESSAGES = 200;
const AIS_BAR_WINDOW_MS = 15 * 60 * 1000;
const AIS_DEFAULT_A_HZ = 161_975_000;
const AIS_CHANNEL_SPACING_HZ = 50_000;
@@ -17,6 +16,17 @@ let aisMessageHistory = [];
let aisPaused = false;
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) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
@@ -295,6 +305,7 @@ window.resetAisHistoryView = function() {
};
function renderAisHistory() {
pruneAisMessageHistory();
if (!aisMessagesEl || aisPaused) {
updateAisSummary();
return;
@@ -317,7 +328,7 @@ function addAisMessage(msg) {
});
aisMessageHistory.unshift(msg);
if (aisMessageHistory.length > AIS_MAX_MESSAGES) aisMessageHistory.length = AIS_MAX_MESSAGES;
pruneAisMessageHistory();
scheduleAisBarUpdate();
if (aisPaused) {
@@ -332,6 +343,12 @@ function addAisMessage(msg) {
}
}
window.pruneAisHistoryView = function() {
pruneAisMessageHistory();
updateAisBar();
renderAisHistory();
};
if (aisClearBtn) {
aisClearBtn.addEventListener("click", async () => {
try {
@@ -10,7 +10,6 @@ const aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
const aprsTotalCountEl = document.getElementById("aprs-total-count");
const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
const APRS_MAX_PACKETS = 100;
const APRS_BAR_WINDOW_MS = 15 * 60 * 1000;
let aprsFilterText = "";
let aprsPacketHistory = [];
@@ -21,6 +20,17 @@ let aprsHideCrc = false;
let aprsCollapseDup = false;
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) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
@@ -304,6 +314,7 @@ function renderAprsRow(pkt, isFresh) {
}
function renderAprsHistory() {
pruneAprsPacketHistory();
if (!aprsPacketsEl || aprsPaused) {
updateAprsSummary();
updateAprsChipState();
@@ -359,13 +370,19 @@ window.resetAprsHistoryView = function() {
if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs");
};
window.pruneAprsHistoryView = function() {
pruneAprsPacketHistory();
updateAprsBar();
renderAprsHistory();
};
function addAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs;
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
aprsPacketHistory.unshift(pkt);
if (aprsPacketHistory.length > APRS_MAX_PACKETS) aprsPacketHistory.length = APRS_MAX_PACKETS;
pruneAprsPacketHistory();
if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
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 ft8PauseBtn = document.getElementById("ft8-pause-btn");
const ft8BarOverlay = document.getElementById("ft8-bar-overlay");
const FT8_MAX_MESSAGES = 200;
const FT8_BAR_WINDOW_MS = 15 * 60 * 1000;
const FT8_PERIOD_SECONDS = 15;
let ft8FilterText = "";
@@ -13,6 +12,17 @@ let ft8MessageHistory = [];
let ft8Paused = false;
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) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
@@ -78,6 +88,7 @@ function updateFt8PauseUi() {
}
function renderFt8History() {
pruneFt8MessageHistory();
if (!ft8MessagesEl || ft8Paused) {
updateFt8PauseUi();
return;
@@ -91,8 +102,9 @@ function renderFt8History() {
}
function addFt8Message(msg) {
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
ft8MessageHistory.unshift(msg);
if (ft8MessageHistory.length > FT8_MAX_MESSAGES) ft8MessageHistory.length = FT8_MAX_MESSAGES;
pruneFt8MessageHistory();
scheduleFt8BarUpdate();
if (ft8Paused) {
ft8BufferedWhilePaused += 1;
@@ -102,6 +114,12 @@ function addFt8Message(msg) {
scheduleFt8HistoryRender();
}
window.pruneFt8HistoryView = function() {
pruneFt8MessageHistory();
updateFt8Bar();
renderFt8History();
};
function ft8BarRfText(msg) {
const displayFreqHz = normalizeFt8DisplayFreqHz(msg.freq_hz);
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 hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count");
const hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen");
const HF_APRS_MAX_PACKETS = 100;
let hfAprsFilterText = "";
let hfAprsPacketHistory = [];
let hfAprsPaused = false;
@@ -19,6 +18,17 @@ let hfAprsHideCrc = false;
let hfAprsCollapseDup = false;
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() {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("hf-aprs-history", () => renderHfAprsHistory());
@@ -296,6 +306,7 @@ function renderHfAprsRow(pkt, isFresh) {
}
function renderHfAprsHistory() {
pruneHfAprsPacketHistory();
if (!hfAprsPacketsEl || hfAprsPaused) {
updateHfAprsSummary();
updateHfAprsChipState();
@@ -318,13 +329,18 @@ window.resetHfAprsHistoryView = function() {
renderHfAprsHistory();
};
window.pruneHfAprsHistoryView = function() {
pruneHfAprsPacketHistory();
renderHfAprsHistory();
};
function addHfAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs;
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
hfAprsPacketHistory.unshift(pkt);
if (hfAprsPacketHistory.length > HF_APRS_MAX_PACKETS) hfAprsPacketHistory.length = HF_APRS_MAX_PACKETS;
pruneHfAprsPacketHistory();
if (hfAprsPaused) {
hfAprsBufferedWhilePaused += 1;
@@ -8,13 +8,23 @@ const vdesBarOverlay = document.getElementById("vdes-bar-overlay");
const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
const vdesFrameCountEl = document.getElementById("vdes-frame-count");
const vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
const VDES_MAX_MESSAGES = 200;
const VDES_BAR_WINDOW_MS = 15 * 60 * 1000;
let vdesFilterText = "";
let vdesMessageHistory = [];
let vdesPaused = false;
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) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
@@ -60,6 +70,7 @@ function vdesHexPreview(rawBytes) {
}
function updateVdesSummary() {
pruneVdesMessageHistory();
if (vdesChannelSummaryEl) {
vdesChannelSummaryEl.textContent = currentVdesCenterText();
}
@@ -228,6 +239,7 @@ window.resetVdesHistoryView = function() {
};
function renderVdesHistory() {
pruneVdesMessageHistory();
if (!vdesMessagesEl || vdesPaused) {
updateVdesSummary();
return;
@@ -250,7 +262,7 @@ function addVdesMessage(msg) {
});
vdesMessageHistory.unshift(msg);
if (vdesMessageHistory.length > VDES_MAX_MESSAGES) vdesMessageHistory.length = VDES_MAX_MESSAGES;
pruneVdesMessageHistory();
scheduleVdesBarUpdate();
if (vdesPaused) {
@@ -323,4 +335,10 @@ window.onServerVdes = function(msg) {
}
};
window.pruneVdesHistoryView = function() {
pruneVdesMessageHistory();
updateVdesBar();
renderVdesHistory();
};
updateVdesSummary();
@@ -4,13 +4,23 @@ const wsprPeriodEl = document.getElementById("wspr-period");
const wsprMessagesEl = document.getElementById("wspr-messages");
const wsprFilterInput = document.getElementById("wspr-filter");
const wsprPauseBtn = document.getElementById("wspr-pause-btn");
const WSPR_MAX_MESSAGES = 200;
const WSPR_PERIOD_SECONDS = 120;
let wsprFilterText = "";
let wsprMessageHistory = [];
let wsprPaused = false;
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() {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob("wspr-history", () => renderWsprHistory());
@@ -59,6 +69,7 @@ function updateWsprPauseUi() {
}
function renderWsprHistory() {
pruneWsprMessageHistory();
if (!wsprMessagesEl || wsprPaused) {
updateWsprPauseUi();
return;
@@ -72,8 +83,9 @@ function renderWsprHistory() {
}
function addWsprMessage(msg) {
msg._tsMs = Number.isFinite(msg?.ts_ms) ? Number(msg.ts_ms) : Date.now();
wsprMessageHistory.unshift(msg);
if (wsprMessageHistory.length > WSPR_MAX_MESSAGES) wsprMessageHistory.length = WSPR_MAX_MESSAGES;
pruneWsprMessageHistory();
if (wsprPaused) {
wsprBufferedWhilePaused += 1;
updateWsprPauseUi();
@@ -82,6 +94,11 @@ function addWsprMessage(msg) {
scheduleWsprHistoryRender();
}
window.pruneWsprHistoryView = function() {
pruneWsprMessageHistory();
renderWsprHistory();
};
function escapeWsprHtml(input) {
return input
.replaceAll("&", "&amp;")