[fix](trx-frontend-http): replay what arrived before a lazy view loaded
CI / lint (pull_request) Successful in 2m23s
CI / test (pull_request) Successful in 8m36s
CI / frontend (pull_request) Failing after 1m23s
CI / reuse (pull_request) Successful in 5s

Opening the Map or Statistics page showed only what had been decoded since
the moment it was opened, and a reload — landing straight on the tab, so its
module loads at startup ahead of the history — was the only way to see the
rest.  Two things were being thrown away.

The decode log the Statistics page counts lives in the map module, which is
lazy.  Recording into a module that is not loaded yet is a no-op, and unlike
the map markers nothing replayed the log when it finally arrived, so every
decode heard before the first visit was simply never counted.  Hold those
records in the client and hand them over when the module attaches.

The map's own replay covered APRS, AIS and VDES, whose plugins implement
syncMap, but not the grid squares: the FTx family and WSPR plotted locators
as decodes arrived and had no replay at all, so everything they heard before
the map loaded was lost, and the unique-grid counter with it.  Both plot
through a helper now, which their syncMap replays oldest first.  A replayed
WSPR spot carries the frequency it was heard on rather than one worked out
against wherever the dial has moved to since.

Pinned in decode-flow, whose history fixture gains FT8 and WSPR spots: after
a first visit the statistics count every stored decode and every grid square,
which before this change were 0 and 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
sjg
2026-08-07 08:56:06 +02:00
co-authored by Claude Opus 5
parent bf3bcc8a84
commit ae7df31d91
10 changed files with 175 additions and 50 deletions
@@ -5889,6 +5889,7 @@ function navigateToTab(name, options = {}) {
if (leavingSatellites) window.clearSatPredictionDom?.();
void loadPluginsForTab(name).then(() => {
if (name === "satellites") window.refreshSatPredictions?.();
flushPendingDecodeStats();
}).catch((error) => {
console.error(error);
});
@@ -7619,11 +7620,35 @@ var IMAGE_DECODE_KINDS = /* @__PURE__ */ new Set([
"sstv",
"sstv_progress"
]);
var pendingDecodeStats = [];
var PENDING_DECODE_STATS_MAX = 5e4;
function recordDecodeStat(kind, rig, tsMs) {
const stats = window.trx.modules.map;
if (stats) {
stats.statsRecordDecode(kind, rig, tsMs);
return;
}
pendingDecodeStats.push({ kind, rig, tsMs });
if (pendingDecodeStats.length > PENDING_DECODE_STATS_MAX) {
pendingDecodeStats.splice(0, pendingDecodeStats.length - PENDING_DECODE_STATS_MAX);
}
}
function flushPendingDecodeStats() {
const stats = window.trx.modules.map;
if (!stats || pendingDecodeStats.length === 0) return;
for (const entry of pendingDecodeStats.splice(0)) {
stats.statsRecordDecode(entry.kind, entry.rig, entry.tsMs);
}
stats.scheduleStatsRender();
}
function scheduleStatsRenderIfLoaded() {
window.trx.modules.map?.scheduleStatsRender();
}
function dispatchDecodeMessage(msg, skipStats = false) {
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
if (!skipStats && msg.type && !IMAGE_DECODE_KINDS.has(msg.type)) {
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.modules.map?.scheduleStatsRender();
recordDecodeStat(msg.type, msg.rig_id || msg.remote || null);
scheduleStatsRenderIfLoaded();
}
}
var DECODE_HISTORY_WORKER_GROUP_LIMIT = 512;
@@ -7669,9 +7694,9 @@ function restoreDecodeHistoryGroup(kind, messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (!IMAGE_DECODE_KINDS.has(kind)) {
for (const msg of messages) {
window.trx.modules.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || void 0);
recordDecodeStat(kind, msg.rig_id || msg.remote || null, msg.ts_ms || void 0);
}
window.trx.modules.map?.scheduleStatsRender();
scheduleStatsRenderIfLoaded();
}
window.trxPluginRuntime.restore(kind, messages);
}
@@ -168,19 +168,22 @@ function initializeFtxDecoder(config) {
}
messagesElement.replaceChildren(fragment);
};
const normalize = (message) => {
const plotLocator = (message) => {
const raw = message.message ?? "";
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
if (grids.length === 0) return;
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
const frequency = displayFrequency(message.freq_hz);
if (grids.length > 0) {
bridge.mapAddLocator?.(raw, grids, id, station, {
...message,
freq_hz: frequency ?? message.freq_hz,
locator_details: locatorDetails
});
}
bridge.mapAddLocator?.(raw, grids, id, station, {
...message,
freq_hz: frequency ?? message.freq_hz,
locator_details: locatorDetails
});
};
const normalize = (message) => {
const frequency = displayFrequency(message.freq_hz);
plotLocator(message);
return {
// The rig that heard it, kept so the mini view can tell a decode of the
// rig on screen from one a background rig made on another band.
@@ -240,6 +243,10 @@ function initializeFtxDecoder(config) {
rerender: () => {
bridge.updateFt8Bar?.();
render();
},
// Oldest first, so the map builds the grids up in the order they were heard.
syncMap: () => {
for (const message of [...history].reverse()) plotLocator(message);
}
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
@@ -1,6 +1,6 @@
import {
initializeFtxDecoder
} from "./chunk-PAKFJPA2.js";
} from "./chunk-PJ5Q7CWJ.js";
import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
@@ -1,6 +1,6 @@
import {
initializeFtxDecoder
} from "./chunk-PAKFJPA2.js";
} from "./chunk-PJ5Q7CWJ.js";
import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
@@ -2,7 +2,7 @@ import {
initializeFt8FamilyBar,
initializeFtxDecoder,
installFtxCompatibilityHelpers
} from "./chunk-PAKFJPA2.js";
} from "./chunk-PJ5Q7CWJ.js";
import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
@@ -95,6 +95,7 @@ function normalizeServerWsprMessage(msg) {
rfHz,
history: {
rig_id: msg.rig_id ?? null,
_rfHz: rfHz,
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
@@ -110,12 +111,7 @@ function onServerWsprBatch(messages) {
for (const msg of messages) {
const next = normalizeServerWsprMessage(msg);
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
...msg,
...next.rfHz === null ? {} : { freq_hz: next.rfHz }
});
}
plotWsprLocator(msg);
next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
normalized.push(next.history);
}
@@ -255,15 +251,19 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
}
})();
});
function plotWsprLocator(msg) {
const next = normalizeServerWsprMessage(msg);
if (next.grids.length === 0 || !wsprWindow.mapAddLocator) return;
const rfHz = finiteNumber(msg._rfHz) ?? next.rfHz;
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
...msg,
...rfHz === null ? {} : { freq_hz: rfHz }
});
}
function onServerWspr(msg) {
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
...msg,
...next.rfHz === null ? {} : { freq_hz: next.rfHz }
});
}
plotWsprLocator(msg);
addWsprMessage(next.history);
}
wsprWindow.trxPluginRuntime.registerDecoder({
@@ -273,5 +273,9 @@ wsprWindow.trxPluginRuntime.registerDecoder({
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView,
rerender: renderWsprHistory
rerender: renderWsprHistory,
// Oldest first, so the map builds the grids up in the order they were heard.
syncMap: () => {
for (const message of [...wsprMessageHistory].reverse()) plotWsprLocator(message);
}
});