diff --git a/docs/User-Manual.md b/docs/User-Manual.md
index 65de98ff..8230e3bb 100644
--- a/docs/User-Manual.md
+++ b/docs/User-Manual.md
@@ -454,6 +454,29 @@ listener and every rig's audio listener.
---
+## Multiple Rigs in the Web UI
+
+A client connected to several rigs decodes all of them at once, whichever one
+is on screen. The rig picker in the header decides which rig the UI is about,
+and each page answers that differently:
+
+| Page | Shows |
+|------|-------|
+| Radio | The selected rig: its spectrum, its audio, and the mini decode views over the waterfall. |
+| Digital Modes | The selected rig: every decoder panel, its counts and its status line. |
+| Map | The whole station — every rig's positions, with the map's own rig filter to narrow it. |
+| Statistics | The whole station, including the per-rig comparison. |
+
+Switching rigs repaints the radio and digital modes pages for the rig now
+selected. Nothing is lost by switching: the traffic other rigs heard is still
+held, and switching back brings it up again. The one exception is the CW pane,
+which is a single running stream of copied text rather than a list of frames,
+so it starts empty on the rig you switch to.
+
+Each browser tab keeps its own selection, so two tabs can watch two rigs.
+
+---
+
## Tune Links
Every page of the web UI carries what the radio is doing in its address, so the
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
index 093be46c..896a2191 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
@@ -1,6 +1,7 @@
import {
+ forActiveRig,
isActiveRigDecode
-} from "./chunk-BQQXSNLC.js";
+} from "./chunk-S57W63QN.js";
import {
hostCore,
hostState
@@ -143,18 +144,22 @@ function aisLatestByVessel(messages) {
}
return Array.from(byMmsi.values());
}
+function aisRigMessages() {
+ return forActiveRig(aisMessageHistory);
+}
function updateAisSummary() {
const plan = currentAisChannelPlan();
if (aisChannelSummaryEl) {
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
}
- const vessels = aisLatestByVessel(aisMessageHistory);
+ const rigMessages = aisRigMessages();
+ const vessels = aisLatestByVessel(rigMessages);
if (aisVesselCountEl) {
const count = vessels.length;
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
}
if (aisLatestSeenEl) {
- const latest = aisMessageHistory[0];
+ const latest = rigMessages[0];
if (!latest) {
aisLatestSeenEl.textContent = "No traffic yet";
} else {
@@ -272,7 +277,7 @@ function renderAisHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (const message of aisMessageHistory) {
+ for (const message of aisRigMessages()) {
fragment.appendChild(renderAisRow(message));
}
aisMessagesEl.replaceChildren(fragment);
@@ -304,10 +309,10 @@ function normalizeServerAisMessage(msg) {
}
function onServerAisBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (aisStatus) aisStatus.textContent = "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
+ if (aisStatus && isActiveRigDecode(next.rig_id)) aisStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -347,8 +352,9 @@ if (aisFilterInput) {
});
}
function onServerAis(msg) {
- if (aisStatus) aisStatus.textContent = "Receiving";
- addAisMessage(normalizeServerAisMessage(msg));
+ const message = normalizeServerAisMessage(msg);
+ if (aisStatus && isActiveRigDecode(message.rig_id)) aisStatus.textContent = "Receiving";
+ addAisMessage(message);
}
updateAisSummary();
window.trxPluginRuntime.registerDecoder({
@@ -358,6 +364,10 @@ window.trxPluginRuntime.registerDecoder({
restore: onServerAisBatch,
reset: resetAisHistoryView,
prune: pruneAisHistoryView,
+ rerender: () => {
+ updateAisBar();
+ renderAisHistory();
+ },
// Oldest first, so vessel tracks are rebuilt in the order they happened.
syncMap: () => {
for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
index a598cc0a..88b35e37 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
@@ -1065,6 +1065,9 @@ var runtime = {
plugin.prune();
return true;
},
+ rerenderAll() {
+ for (const plugin of decoders.values()) plugin.rerender?.();
+ },
syncMapAll() {
for (const plugin of decoders.values()) plugin.syncMap?.();
},
@@ -3577,6 +3580,7 @@ function resetDecoderStateOnRigSwitch() {
if (el) el.textContent = "--";
});
refreshDecodeBars();
+ window.trxPluginRuntime.rerenderAll();
}
function resetWfmStereoIndicator() {
if (!wfmStFlagEl) return;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
index 2eba9eba..a64edc72 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
@@ -6,8 +6,9 @@ import {
renderAprsPacketRow
} from "./chunk-OPEIVJGD.js";
import {
+ forActiveRig,
isActiveRigDecode
-} from "./chunk-BQQXSNLC.js";
+} from "./chunk-S57W63QN.js";
import {
hostCore,
hostState
@@ -85,20 +86,25 @@ function aprsFilterMatch(pkt) {
].filter(Boolean).join(" ").toUpperCase();
return haystack.includes(aprsFilterText);
}
+function aprsRigPackets() {
+ return forActiveRig(aprsPacketHistory);
+}
function aprsVisiblePackets() {
- const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
+ const rigPackets = aprsRigPackets();
+ const packets = aprsCollapseDup ? collapseAprsDuplicates(rigPackets) : rigPackets;
return packets.filter(aprsFilterMatch);
}
function updateAprsSummary() {
+ const rigPackets = aprsRigPackets();
const visible = aprsVisiblePackets();
if (aprsTotalCountEl) {
- aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
+ aprsTotalCountEl.textContent = `${rigPackets.length} total`;
}
if (aprsVisibleCountEl) {
aprsVisibleCountEl.textContent = `${visible.length} shown`;
}
if (aprsLatestSeenEl) {
- const latest = aprsPacketHistory[0];
+ const latest = rigPackets[0];
if (!latest) {
aprsLatestSeenEl.textContent = "No packets yet";
} else {
@@ -220,11 +226,11 @@ function normalizeServerAprsPacket(pkt) {
}
function onServerAprsBatch(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
- if (aprsStatus) aprsStatus.textContent = "Receiving";
const normalized = [];
let hasCrcOk = false;
for (const pkt of packets) {
const next = normalizeServerAprsPacket(pkt);
+ if (aprsStatus && isActiveRigDecode(next.rig_id)) aprsStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -282,8 +288,9 @@ if (aprsFilterInput) {
});
}
function onServerAprs(pkt) {
- if (aprsStatus) aprsStatus.textContent = "Receiving";
- addAprsPacket(normalizeServerAprsPacket(pkt));
+ const packet = normalizeServerAprsPacket(pkt);
+ if (aprsStatus && isActiveRigDecode(packet.rig_id)) aprsStatus.textContent = "Receiving";
+ addAprsPacket(packet);
}
renderAprsHistory();
window.trxPluginRuntime.registerDecoder({
@@ -293,6 +300,10 @@ window.trxPluginRuntime.registerDecoder({
restore: onServerAprsBatch,
reset: resetAprsHistoryView,
prune: pruneAprsHistoryView,
+ rerender: () => {
+ updateAprsBar();
+ renderAprsHistory();
+ },
// Oldest first, so station tracks are rebuilt in the order they happened.
syncMap: () => {
for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-K3D6FOP5.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-PAKFJPA2.js
similarity index 97%
rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-K3D6FOP5.js
rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-PAKFJPA2.js
index 19c67d0f..bf8b4031 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-K3D6FOP5.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-PAKFJPA2.js
@@ -1,6 +1,7 @@
import {
+ forActiveRig,
isActiveRigDecode
-} from "./chunk-BQQXSNLC.js";
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -156,9 +157,10 @@ function initializeFtxDecoder(config) {
const render = () => {
prune();
if (!messagesElement) return;
+ const rigMessages = forActiveRig(history);
const fragment = document.createDocumentFragment();
let count = 0;
- for (const message of history) {
+ for (const message of rigMessages) {
if (count >= 200) break;
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
fragment.appendChild(renderRow(message));
@@ -194,7 +196,9 @@ function initializeFtxDecoder(config) {
};
const receiveBatch = (messages) => {
if (messages.length === 0) return;
- if (status) status.textContent = "Receiving";
+ if (status && messages.some((message) => isActiveRigDecode(message.rig_id))) {
+ status.textContent = "Receiving";
+ }
history = messages.map(normalize).reverse().concat(history);
prune();
bridge.setFt8FamilyBarDecoder?.(id);
@@ -232,7 +236,11 @@ function initializeFtxDecoder(config) {
prune();
render();
},
- reset
+ reset,
+ rerender: () => {
+ bridge.updateFt8Bar?.();
+ render();
+ }
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-BQQXSNLC.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-S57W63QN.js
similarity index 65%
rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-BQQXSNLC.js
rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-S57W63QN.js
index 14d8f60f..8afb41a6 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-BQQXSNLC.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-S57W63QN.js
@@ -8,7 +8,11 @@ function isActiveRigDecode(rigId) {
if (!activeRigId || !rigId) return true;
return rigId === activeRigId;
}
+function forActiveRig(items) {
+ return items.filter((item) => isActiveRigDecode(item.rig_id));
+}
export {
- isActiveRigDecode
+ isActiveRigDecode,
+ forActiveRig
};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
index 1564ab69..1f91cc1c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
@@ -1,6 +1,6 @@
import {
isActiveRigDecode
-} from "./chunk-BQQXSNLC.js";
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -329,8 +329,9 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
})();
});
function onServerCw(evt) {
- if (cwStatusEl) cwStatusEl.textContent = "Receiving";
- if (evt.text && cwOutputEl) {
+ const forSelectedRig = isActiveRigDecode(evt.rig_id ?? null);
+ if (cwStatusEl && forSelectedRig) cwStatusEl.textContent = "Receiving";
+ if (evt.text && cwOutputEl && forSelectedRig) {
const now = Date.now();
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
const line = document.createElement("div");
@@ -370,10 +371,10 @@ function onServerCw(evt) {
}
updateCwBar();
}
- if (cwSignalIndicator) {
+ if (cwSignalIndicator && forSelectedRig) {
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
}
- if (!cwAutoInput || cwAutoInput.checked) {
+ if (forSelectedRig && (!cwAutoInput || cwAutoInput.checked)) {
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
cwWpmInput.value = String(clampCwWpm(evt.wpm));
}
@@ -398,7 +399,14 @@ cwWindow.trxPluginRuntime.registerDecoder({
id: "cw",
onMessage: onServerCw,
restore: restoreCwHistory,
- reset: resetCwHistoryView
+ reset: resetCwHistoryView,
+ // The copied text of a rig that is no longer on screen cannot be unpicked
+ // from the pane, so the switch starts the new rig's stream from empty.
+ rerender: () => {
+ if (cwOutputEl) cwOutputEl.innerHTML = "";
+ cwLastAppendTime = 0;
+ updateCwBar();
+ }
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
index db3311bb..7862c521 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
@@ -1,7 +1,7 @@
import {
initializeFtxDecoder
-} from "./chunk-K3D6FOP5.js";
-import "./chunk-BQQXSNLC.js";
+} from "./chunk-PAKFJPA2.js";
+import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft2.ts
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
index d4fdf8a8..386a5c2c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
@@ -1,7 +1,7 @@
import {
initializeFtxDecoder
-} from "./chunk-K3D6FOP5.js";
-import "./chunk-BQQXSNLC.js";
+} from "./chunk-PAKFJPA2.js";
+import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft4.ts
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
index 85570b26..d0b23a76 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
@@ -2,8 +2,8 @@ import {
initializeFt8FamilyBar,
initializeFtxDecoder,
installFtxCompatibilityHelpers
-} from "./chunk-K3D6FOP5.js";
-import "./chunk-BQQXSNLC.js";
+} from "./chunk-PAKFJPA2.js";
+import "./chunk-S57W63QN.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft8.ts
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
index d267fa66..aaff423e 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
@@ -5,6 +5,10 @@ import {
normalizeAprsPacket,
renderAprsPacketRow
} from "./chunk-OPEIVJGD.js";
+import {
+ forActiveRig,
+ isActiveRigDecode
+} from "./chunk-S57W63QN.js";
import {
hostCore,
hostState
@@ -67,21 +71,26 @@ function hfAprsFilterMatch(pkt) {
].filter(Boolean).join(" ").toUpperCase();
return haystack.includes(hfAprsFilterText);
}
+function hfAprsRigPackets() {
+ return forActiveRig(hfAprsPacketHistory);
+}
function hfAprsVisiblePackets() {
- const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
+ const rigPackets = hfAprsRigPackets();
+ const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(rigPackets) : rigPackets;
return packets.filter(hfAprsFilterMatch);
}
var collapseHfAprsDuplicates = collapseAprsDuplicates;
function updateHfAprsSummary() {
+ const rigPackets = hfAprsRigPackets();
const visible = hfAprsVisiblePackets();
if (hfAprsTotalCountEl) {
- hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
+ hfAprsTotalCountEl.textContent = `${rigPackets.length} total`;
}
if (hfAprsVisibleCountEl) {
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
}
if (hfAprsLatestSeenEl) {
- const latest = hfAprsPacketHistory[0];
+ const latest = rigPackets[0];
if (!latest) {
hfAprsLatestSeenEl.textContent = "No packets yet";
} else {
@@ -158,10 +167,10 @@ function normalizeServerHfAprsPacket(pkt) {
}
function onServerHfAprsBatch(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
const normalized = [];
for (const pkt of packets) {
const next = normalizeServerHfAprsPacket(pkt);
+ if (hfAprsStatus && isActiveRigDecode(next.rig_id)) hfAprsStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -227,8 +236,9 @@ if (hfAprsFilterInput) {
});
}
function onServerHfAprs(pkt) {
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
- addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
+ const packet = normalizeServerHfAprsPacket(pkt);
+ if (hfAprsStatus && isActiveRigDecode(packet.rig_id)) hfAprsStatus.textContent = "Receiving";
+ addHfAprsPacket(packet);
}
renderHfAprsHistory();
window.trxPluginRuntime.registerDecoder({
@@ -237,5 +247,6 @@ window.trxPluginRuntime.registerDecoder({
onBatch: onServerHfAprsBatch,
restore: onServerHfAprsBatch,
reset: resetHfAprsHistoryView,
- prune: pruneHfAprsHistoryView
+ prune: pruneHfAprsHistoryView,
+ rerender: renderHfAprsHistory
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sstv.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sstv.js
index e92e9e21..6d1161a8 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sstv.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sstv.js
@@ -1,3 +1,7 @@
+import {
+ forActiveRig,
+ isActiveRigDecode
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -111,6 +115,7 @@ function paintRow(line, rgb) {
liveRows = Math.max(liveRows, line + 1);
}
function onProgress(msg) {
+ if (!isActiveRigDecode(msg.rig_id)) return;
if (msg.state) {
if (sstvDom.status) sstvDom.status.textContent = msg.state;
beginPicture(msg.mode || "", msg.width || 0, msg.height || 0);
@@ -131,15 +136,20 @@ function onImage(msg) {
sstvHistory.push(image);
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
pruneHistory();
+ if (!isActiveRigDecode(image.rig_id)) return;
if (sstvDom.status) {
sstvDom.status.textContent = image.complete ? `Received ${image.mode ?? "picture"}` : `Partial ${image.mode ?? "picture"} — ${image.lines ?? 0} lines`;
}
scheduleUi("sstv-latest", renderLatestCard);
if (activeView === "history") scheduleUi("sstv-history", renderHistoryTable);
}
+function sstvRigImages() {
+ return forActiveRig(sstvHistory);
+}
function renderLatestCard() {
if (!sstvDom.liveLatest) return;
- const latest = sstvHistory[sstvHistory.length - 1];
+ const rigImages = sstvRigImages();
+ const latest = rigImages[rigImages.length - 1];
if (!latest) {
sstvDom.liveLatest.innerHTML = "";
return;
@@ -160,8 +170,9 @@ function renderLatestCard() {
`;
}
function filteredHistory() {
+ const rigImages = sstvRigImages();
const text = filterText.trim().toLowerCase();
- const matching = text ? sstvHistory.filter((image) => (image.mode ?? "").toLowerCase().includes(text)) : sstvHistory.slice();
+ const matching = text ? rigImages.filter((image) => (image.mode ?? "").toLowerCase().includes(text)) : rigImages;
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
return matching;
@@ -241,7 +252,11 @@ sstvWindow.trxPluginRuntime.registerDecoder({
onMessage: onImage,
restore: restoreHistory,
prune: renderHistoryTable,
- reset: resetHistoryView
+ reset: resetHistoryView,
+ rerender: () => {
+ renderLatestCard();
+ renderHistoryTable();
+ }
});
sstvWindow.trxPluginRuntime.registerDecoder({
id: "sstv_progress",
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
index 6bc119d2..65e8c373 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
@@ -1,6 +1,7 @@
import {
+ forActiveRig,
isActiveRigDecode
-} from "./chunk-BQQXSNLC.js";
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -63,17 +64,21 @@ function vdesHexPreview(rawBytes) {
if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase();
}
+function vdesRigMessages() {
+ return forActiveRig(vdesMessageHistory);
+}
function updateVdesSummary() {
pruneVdesMessageHistory();
if (vdesChannelSummaryEl) {
vdesChannelSummaryEl.textContent = currentVdesCenterText();
}
+ const rigMessages = vdesRigMessages();
if (vdesFrameCountEl) {
- const count = vdesMessageHistory.length;
+ const count = rigMessages.length;
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
}
if (vdesLatestSeenEl) {
- const latest = vdesMessageHistory[0];
+ const latest = rigMessages[0];
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
}
}
@@ -185,7 +190,7 @@ function renderVdesHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (const message of vdesMessageHistory) {
+ for (const message of vdesRigMessages()) {
fragment.appendChild(renderVdesRow(message));
}
vdesMessagesEl.replaceChildren(fragment);
@@ -212,10 +217,10 @@ function normalizeServerVdesMessage(msg) {
}
function onServerVdesBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (vdesStatus) vdesStatus.textContent = "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerVdesMessage(msg);
+ if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -254,8 +259,8 @@ function plotVdesMessage(msg) {
vdesWindow.vdesMapAddPoint(msg);
}
function onServerVdes(msg) {
- if (vdesStatus) vdesStatus.textContent = "Receiving";
const next = normalizeServerVdesMessage(msg);
+ if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
addVdesMessage(next);
plotVdesMessage(next);
}
@@ -272,6 +277,10 @@ window.trxPluginRuntime.registerDecoder({
restore: onServerVdesBatch,
reset: resetVdesHistoryView,
prune: pruneVdesHistoryView,
+ rerender: () => {
+ updateVdesBar();
+ renderVdesHistory();
+ },
// Oldest first, so tracks are rebuilt in the order they happened.
syncMap: () => {
for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
index 0b09f26b..e11af9ba 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
@@ -1,3 +1,7 @@
+import {
+ forActiveRig,
+ isActiveRigDecode
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -102,13 +106,17 @@ function paintLine(lineBytes) {
wefaxLiveCtx.putImageData(imgData, 0, y);
wefaxLiveLineCount++;
}
+function wefaxRigImages() {
+ return forActiveRig(wefaxImageHistory);
+}
function renderWefaxLatestCard() {
if (!wefaxDom.liveLatest) return;
- if (wefaxImageHistory.length === 0) {
+ const rigImages = wefaxRigImages();
+ if (rigImages.length === 0) {
wefaxDom.liveLatest.innerHTML = '
No images decoded yet. Enable the decoder and tune to a WEFAX station.
';
return;
}
- const img = wefaxImageHistory[0];
+ const img = rigImages[0];
if (!img) return;
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
@@ -129,7 +137,7 @@ function renderWefaxLatestCard() {
wefaxDom.liveLatest.innerHTML = html;
}
function getWefaxFilteredHistory() {
- let items = wefaxImageHistory;
+ let items = wefaxRigImages();
if (wefaxFilterText) {
items = items.filter(function(i) {
const haystack = [
@@ -173,7 +181,7 @@ function renderWefaxHistoryTable() {
}
wefaxDom.historyList.replaceChildren(fragment);
if (wefaxDom.historyCount) {
- const total = wefaxImageHistory.length;
+ const total = wefaxRigImages().length;
const shown = items.length;
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
}
@@ -208,6 +216,7 @@ function addWefaxImage(msg) {
}
}
function onServerWefaxProgress(msg) {
+ if (!isActiveRigDecode(msg.rig_id)) return;
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
wefaxDom.status.textContent = msg.state;
@@ -234,6 +243,7 @@ function onServerWefaxProgress(msg) {
}
function onServerWefax(msg) {
addWefaxImage(msg);
+ if (!isActiveRigDecode(msg.rig_id)) return;
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
if (wefaxDom.status) {
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
@@ -328,7 +338,11 @@ wefaxWindow.trxPluginRuntime.registerDecoder({
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
- reset: resetWefaxHistoryView
+ reset: resetWefaxHistoryView,
+ rerender: () => {
+ renderWefaxLatestCard();
+ renderWefaxHistoryTable();
+ }
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
index df393e5d..0d0a1f5b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
@@ -1,3 +1,7 @@
+import {
+ forActiveRig,
+ isActiveRigDecode
+} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
@@ -64,10 +68,10 @@ function renderWsprRow(msg) {
function renderWsprHistory() {
pruneWsprMessageHistory();
if (!wsprMessagesEl) return;
+ const rigMessages = forActiveRig(wsprMessageHistory);
const fragment = document.createDocumentFragment();
- for (let i = 0; i < wsprMessageHistory.length; i += 1) {
- const message = wsprMessageHistory[i];
- if (message) fragment.appendChild(renderWsprRow(message));
+ for (const message of rigMessages) {
+ fragment.appendChild(renderWsprRow(message));
}
wsprMessagesEl.replaceChildren(fragment);
}
@@ -90,6 +94,7 @@ function normalizeServerWsprMessage(msg) {
station,
rfHz,
history: {
+ rig_id: msg.rig_id ?? null,
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
@@ -101,10 +106,10 @@ function normalizeServerWsprMessage(msg) {
}
function onServerWsprBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized = [];
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,
@@ -251,7 +256,7 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
})();
});
function onServerWspr(msg) {
- if (wsprStatus) wsprStatus.textContent = "Receiving";
+ 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, {
@@ -267,5 +272,6 @@ wsprWindow.trxPluginRuntime.registerDecoder({
onBatch: onServerWsprBatch,
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
- reset: resetWsprHistoryView
+ reset: resetWsprHistoryView,
+ rerender: renderWsprHistory
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
index aaf7ce58..82dee01e 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts
@@ -2240,9 +2240,10 @@ function resetDecoderStateOnRigSwitch() {
});
// The decode stream is not rig-scoped, so the histories behind the mini views
- // survive the switch: drop the outgoing rig's frames from them now instead of
- // leaving them on screen until the next state update.
+ // and the decoder panels survive the switch: repaint both for the rig now
+ // selected instead of leaving the outgoing rig's traffic on screen.
refreshDecodeBars();
+ window.trxPluginRuntime.rerenderAll();
}
function resetWfmStereoIndicator() {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-runtime.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-runtime.ts
index 91a4804b..29045ae3 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-runtime.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-runtime.ts
@@ -81,6 +81,7 @@ const runtime: TrxPluginRuntime = {
plugin.prune();
return true;
},
+ rerenderAll() { for (const plugin of decoders.values()) plugin.rerender?.(); },
syncMapAll() { for (const plugin of decoders.values()) plugin.syncMap?.(); },
clearQueued() { queued.clear(); },
hasDecoder: (id) => decoders.has(id),
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/active-rig.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/active-rig.ts
index 499ae127..5fb35919 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/active-rig.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/active-rig.ts
@@ -4,13 +4,14 @@
import { hostState } from "./host.js";
-// The decode panels are aggregate views on purpose: they list what every rig
-// heard, and the history they restore from is not rig-scoped either. The mini
-// views over the waterfall are not aggregates — they read as a caption for the
-// spectrum underneath, so a frame a background rig copied on another band
-// belongs to a different picture entirely. Each decode names the rig that
-// heard it (the client stamps `rig_id` as the frame leaves the audio
-// connection), which is all the overlays need to keep to the rig on screen.
+// The decode stream is not rig-scoped: every rig a client is connected to sends
+// its decodes to every browser, each frame naming the rig that heard it (the
+// client stamps `rig_id` as it leaves the audio connection). The map wants all
+// of them — it has its own filter, and plotting only one rig would empty it —
+// but the radio page and the decoder panels describe one rig at a time: the one
+// the operator selected, whose spectrum is on screen and whose audio is
+// playing. A frame a background rig copied on another band belongs to neither
+// picture, so both filter on this.
export function isActiveRigDecode(rigId: string | null | undefined): boolean {
const activeRigId = hostState.lastActiveRigId;
// Before the rig list has arrived, and for a decode that reached the browser
@@ -18,3 +19,8 @@ export function isActiveRigDecode(rigId: string | null | undefined): boolean {
if (!activeRigId || !rigId) return true;
return rigId === activeRigId;
}
+
+/** The decodes of `items` that the selected rig heard, in their original order. */
+export function forActiveRig(items: T[]): T[] {
+ return items.filter((item) => isActiveRigDecode(item.rig_id));
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
index 182fbbe7..7edfc253 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
@@ -6,7 +6,7 @@ import { hostCore, hostState } from "./host.js";
export {};
-import { isActiveRigDecode } from "./active-rig.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract";
interface AisMessage {
@@ -191,20 +191,27 @@ function aisLatestByVessel(messages: AisMessage[]): AisMessage[] {
return Array.from(byMmsi.values());
}
+/** What the selected rig heard. The history holds every rig's traffic, for
+ * the map; the panel is about the rig the operator is working. */
+function aisRigMessages(): AisMessage[] {
+ return forActiveRig(aisMessageHistory);
+}
+
function updateAisSummary() {
const plan = currentAisChannelPlan();
if (aisChannelSummaryEl) {
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
}
- const vessels = aisLatestByVessel(aisMessageHistory);
+ const rigMessages = aisRigMessages();
+ const vessels = aisLatestByVessel(rigMessages);
if (aisVesselCountEl) {
const count = vessels.length;
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
}
if (aisLatestSeenEl) {
- const latest = aisMessageHistory[0];
+ const latest = rigMessages[0];
if (!latest) {
aisLatestSeenEl.textContent = "No traffic yet";
} else {
@@ -371,7 +378,7 @@ function renderAisHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (const message of aisMessageHistory) {
+ for (const message of aisRigMessages()) {
fragment.appendChild(renderAisRow(message));
}
aisMessagesEl.replaceChildren(fragment);
@@ -410,10 +417,11 @@ function normalizeServerAisMessage(msg: AisMessage): AisMessage {
function onServerAisBatch(messages: AisMessage[]): void {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (aisStatus) aisStatus.textContent = "Receiving";
const normalized: AisMessage[] = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ if (aisStatus && isActiveRigDecode(next.rig_id)) aisStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -455,8 +463,9 @@ if (aisFilterInput) {
}
function onServerAis(msg: AisMessage): void {
- if (aisStatus) aisStatus.textContent = "Receiving";
- addAisMessage(normalizeServerAisMessage(msg));
+ const message = normalizeServerAisMessage(msg);
+ if (aisStatus && isActiveRigDecode(message.rig_id)) aisStatus.textContent = "Receiving";
+ addAisMessage(message);
}
updateAisSummary();
@@ -467,6 +476,7 @@ updateAisSummary();
restore: onServerAisBatch,
reset: resetAisHistoryView,
prune: pruneAisHistoryView,
+ rerender: () => { updateAisBar(); renderAisHistory(); },
// Oldest first, so vessel tracks are rebuilt in the order they happened.
syncMap: () => { for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry); },
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
index 5e14d0a6..c476983a 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
@@ -4,7 +4,7 @@
import { hostCore, hostState } from "./host.js";
-import { isActiveRigDecode } from "./active-rig.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import {
aprsAgeText,
aprsPacketCategory,
@@ -107,21 +107,29 @@ function aprsFilterMatch(pkt: AprsPacket): boolean {
return haystack.includes(aprsFilterText);
}
+/** What the selected rig heard. The history holds every rig's traffic, for
+ * the map; the panel is about the rig the operator is working. */
+function aprsRigPackets(): AprsPacket[] {
+ return forActiveRig(aprsPacketHistory);
+}
+
function aprsVisiblePackets(): AprsPacket[] {
- const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
+ const rigPackets = aprsRigPackets();
+ const packets = aprsCollapseDup ? collapseAprsDuplicates(rigPackets) : rigPackets;
return packets.filter(aprsFilterMatch);
}
function updateAprsSummary() {
+ const rigPackets = aprsRigPackets();
const visible = aprsVisiblePackets();
if (aprsTotalCountEl) {
- aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
+ aprsTotalCountEl.textContent = `${rigPackets.length} total`;
}
if (aprsVisibleCountEl) {
aprsVisibleCountEl.textContent = `${visible.length} shown`;
}
if (aprsLatestSeenEl) {
- const latest = aprsPacketHistory[0];
+ const latest = rigPackets[0];
if (!latest) {
aprsLatestSeenEl.textContent = "No packets yet";
} else {
@@ -259,11 +267,12 @@ function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket {
function onServerAprsBatch(packets: AprsPacket[]): void {
if (!Array.isArray(packets) || packets.length === 0) return;
- if (aprsStatus) aprsStatus.textContent = "Receiving";
const normalized: AprsPacket[] = [];
let hasCrcOk = false;
for (const pkt of packets) {
const next = normalizeServerAprsPacket(pkt);
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ if (aprsStatus && isActiveRigDecode(next.rig_id)) aprsStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -327,8 +336,9 @@ if (aprsFilterInput) {
// --- Server-side APRS decode handler ---
function onServerAprs(pkt: AprsPacket): void {
- if (aprsStatus) aprsStatus.textContent = "Receiving";
- addAprsPacket(normalizeServerAprsPacket(pkt));
+ const packet = normalizeServerAprsPacket(pkt);
+ if (aprsStatus && isActiveRigDecode(packet.rig_id)) aprsStatus.textContent = "Receiving";
+ addAprsPacket(packet);
}
renderAprsHistory();
@@ -339,6 +349,7 @@ renderAprsHistory();
restore: onServerAprsBatch,
reset: resetAprsHistoryView,
prune: pruneAprsHistoryView,
+ rerender: () => { updateAprsBar(); renderAprsHistory(); },
// Oldest first, so station tracks are rebuilt in the order they happened.
syncMap: () => { for (const entry of [...aprsPacketHistory].reverse()) plotAprsPacket(entry); },
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/cw.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/cw.ts
index 3028dcc6..0a9c2442 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/cw.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/cw.ts
@@ -432,8 +432,13 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
// --- Server-side CW decode handler ---
function onServerCw(evt: CwEvent): void {
- if (cwStatusEl) cwStatusEl.textContent = "Receiving";
- if (evt.text && cwOutputEl) {
+ // The panel is a single stream of copied text: characters from a background
+ // rig do not belong in it at all, and interleaving them would make both
+ // rigs unreadable. The bar histories below keep every rig, each on its own
+ // line, and pick out the selected rig when they paint.
+ const forSelectedRig = isActiveRigDecode(evt.rig_id ?? null);
+ if (cwStatusEl && forSelectedRig) cwStatusEl.textContent = "Receiving";
+ if (evt.text && cwOutputEl && forSelectedRig) {
// Append decoded text to output
const now = Date.now();
if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 10000 || evt.text === "\n") {
@@ -475,10 +480,10 @@ function onServerCw(evt: CwEvent): void {
}
updateCwBar();
}
- if (cwSignalIndicator) {
+ if (cwSignalIndicator && forSelectedRig) {
cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
}
- if (!cwAutoInput || cwAutoInput.checked) {
+ if (forSelectedRig && (!cwAutoInput || cwAutoInput.checked)) {
if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
cwWpmInput.value = String(clampCwWpm(evt.wpm));
}
@@ -506,6 +511,13 @@ cwWindow.trxPluginRuntime.registerDecoder({
onMessage: onServerCw,
restore: restoreCwHistory,
reset: resetCwHistoryView,
+ // The copied text of a rig that is no longer on screen cannot be unpicked
+ // from the pane, so the switch starts the new rig's stream from empty.
+ rerender: () => {
+ if (cwOutputEl) cwOutputEl.innerHTML = "";
+ cwLastAppendTime = 0;
+ updateCwBar();
+ },
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ftx-family.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ftx-family.ts
index 8f4a77fe..64b7d475 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ftx-family.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ftx-family.ts
@@ -4,7 +4,7 @@
import { hostCore } from "./host.js";
-import { isActiveRigDecode } from "./active-rig.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export type FtxDecoderId = "ft2" | "ft4" | "ft8";
@@ -229,9 +229,12 @@ export function initializeFtxDecoder(config: FtxConfig): void {
const render = (): void => {
prune();
if (!messagesElement) return;
+ // The history holds every rig's decodes, for the map; the panel is about
+ // the rig the operator is working.
+ const rigMessages = forActiveRig(history);
const fragment = document.createDocumentFragment();
let count = 0;
- for (const message of history) {
+ for (const message of rigMessages) {
if (count >= 200) break;
if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
fragment.appendChild(renderRow(message));
@@ -267,7 +270,10 @@ export function initializeFtxDecoder(config: FtxConfig): void {
};
const receiveBatch = (messages: FtxMessage[]): void => {
if (messages.length === 0) return;
- if (status) status.textContent = "Receiving";
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ if (status && messages.some((message) => isActiveRigDecode(message.rig_id))) {
+ status.textContent = "Receiving";
+ }
history = messages.map(normalize).reverse().concat(history);
prune();
bridge.setFt8FamilyBarDecoder?.(id);
@@ -305,6 +311,7 @@ export function initializeFtxDecoder(config: FtxConfig): void {
restore: receiveBatch,
prune: () => { prune(); render(); },
reset,
+ rerender: () => { bridge.updateFt8Bar?.(); render(); },
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
index 3192fe2e..a7ef6118 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
@@ -4,6 +4,7 @@
import { hostCore, hostState } from "./host.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import {
aprsAgeText,
aprsPacketCategory,
@@ -90,23 +91,31 @@ function hfAprsFilterMatch(pkt: AprsPacket): boolean {
return haystack.includes(hfAprsFilterText);
}
+/** What the selected rig heard. The history holds every rig's traffic, for
+ * the map; the panel is about the rig the operator is working. */
+function hfAprsRigPackets(): AprsPacket[] {
+ return forActiveRig(hfAprsPacketHistory);
+}
+
function hfAprsVisiblePackets(): AprsPacket[] {
- const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
+ const rigPackets = hfAprsRigPackets();
+ const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(rigPackets) : rigPackets;
return packets.filter(hfAprsFilterMatch);
}
const collapseHfAprsDuplicates = collapseAprsDuplicates;
function updateHfAprsSummary() {
+ const rigPackets = hfAprsRigPackets();
const visible = hfAprsVisiblePackets();
if (hfAprsTotalCountEl) {
- hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
+ hfAprsTotalCountEl.textContent = `${rigPackets.length} total`;
}
if (hfAprsVisibleCountEl) {
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
}
if (hfAprsLatestSeenEl) {
- const latest = hfAprsPacketHistory[0];
+ const latest = rigPackets[0];
if (!latest) {
hfAprsLatestSeenEl.textContent = "No packets yet";
} else {
@@ -192,10 +201,11 @@ function normalizeServerHfAprsPacket(pkt: AprsPacket): AprsPacket {
function onServerHfAprsBatch(packets: AprsPacket[]): void {
if (!Array.isArray(packets) || packets.length === 0) return;
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
const normalized: AprsPacket[] = [];
for (const pkt of packets) {
const next = normalizeServerHfAprsPacket(pkt);
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ if (hfAprsStatus && isActiveRigDecode(next.rig_id)) hfAprsStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -266,8 +276,9 @@ if (hfAprsFilterInput) {
// --- Server-side HF APRS decode handler ---
function onServerHfAprs(pkt: AprsPacket): void {
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
- addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
+ const packet = normalizeServerHfAprsPacket(pkt);
+ if (hfAprsStatus && isActiveRigDecode(packet.rig_id)) hfAprsStatus.textContent = "Receiving";
+ addHfAprsPacket(packet);
}
renderHfAprsHistory();
@@ -278,4 +289,5 @@ renderHfAprsHistory();
restore: onServerHfAprsBatch,
reset: resetHfAprsHistoryView,
prune: pruneHfAprsHistoryView,
+ rerender: renderHfAprsHistory,
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/runtime-contract.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/runtime-contract.ts
index 077c0c7d..124681a0 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/runtime-contract.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/runtime-contract.ts
@@ -9,6 +9,10 @@ export interface DecoderPlugin {
restore?(messages: TMessage[]): void;
reset?(): void;
prune?(): void;
+ /** Repaint the plugin's panel from what it already holds. Called when the
+ * selected rig changes: the panels show one rig's traffic, and the decodes
+ * behind them arrived long before the switch. */
+ rerender?(): void;
/** Replay everything the plugin is holding onto the map. Called when the map
* module attaches, which can happen long after the decodes arrived. */
syncMap?(): void;
@@ -22,6 +26,7 @@ export interface TrxPluginRuntime {
reset(id: string): boolean;
resetAll(): void;
prune(id: string): boolean;
+ rerenderAll(): void;
syncMapAll(): void;
clearQueued(): void;
hasDecoder(id: string): boolean;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sstv.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sstv.ts
index c7f1bd60..d2e62c28 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sstv.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sstv.ts
@@ -14,11 +14,13 @@
import { hostCore } from "./host.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
interface SstvImage {
+ rig_id?: string | null;
ts_ms?: number;
vis?: number;
mode?: string;
@@ -32,6 +34,7 @@ interface SstvImage {
}
interface SstvProgress {
+ rig_id?: string | null;
state?: string;
mode?: string;
width?: number;
@@ -185,6 +188,9 @@ function paintRow(line: number, rgb: Uint8Array): void {
// ── Server messages ─────────────────────────────────────────────────
function onProgress(msg: SstvProgress): void {
+ // The live canvas draws one picture at a time: a background rig's rows would
+ // paint straight over the one being watched.
+ if (!isActiveRigDecode(msg.rig_id)) return;
if (msg.state) {
if (sstvDom.status) sstvDom.status.textContent = msg.state;
// A state update carries the geometry; a row update carries only the row.
@@ -208,6 +214,9 @@ function onImage(msg: SstvImage): void {
if (sstvHistory.length > SSTV_MAX_IMAGES) sstvHistory.shift();
pruneHistory();
+ // Kept in the history either way, so switching to that rig finds it there.
+ if (!isActiveRigDecode(image.rig_id)) return;
+
if (sstvDom.status) {
sstvDom.status.textContent = image.complete
? `Received ${image.mode ?? "picture"}`
@@ -219,9 +228,15 @@ function onImage(msg: SstvImage): void {
// ── Rendering ───────────────────────────────────────────────────────
+/** The pictures the selected rig decoded, oldest first, as the history is kept. */
+function sstvRigImages(): SstvImage[] {
+ return forActiveRig(sstvHistory);
+}
+
function renderLatestCard(): void {
if (!sstvDom.liveLatest) return;
- const latest = sstvHistory[sstvHistory.length - 1];
+ const rigImages = sstvRigImages();
+ const latest = rigImages[rigImages.length - 1];
if (!latest) {
sstvDom.liveLatest.innerHTML = "";
return;
@@ -245,10 +260,12 @@ function renderLatestCard(): void {
}
function filteredHistory(): SstvImage[] {
+ // Every rig's pictures are kept; the panel shows the selected rig's.
+ const rigImages = sstvRigImages();
const text = filterText.trim().toLowerCase();
const matching = text
- ? sstvHistory.filter((image) => (image.mode ?? "").toLowerCase().includes(text))
- : sstvHistory.slice();
+ ? rigImages.filter((image) => (image.mode ?? "").toLowerCase().includes(text))
+ : rigImages;
const newestFirst = (sstvDom.sortSelect?.value ?? "newest") === "newest";
matching.sort((a, b) => (newestFirst ? 1 : -1) * ((b._tsMs ?? 0) - (a._tsMs ?? 0)));
return matching;
@@ -348,6 +365,7 @@ sstvWindow.trxPluginRuntime.registerDecoder({
restore: restoreHistory,
prune: renderHistoryTable,
reset: resetHistoryView,
+ rerender: () => { renderLatestCard(); renderHistoryTable(); },
});
sstvWindow.trxPluginRuntime.registerDecoder({
id: "sstv_progress",
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
index fd083801..5b458526 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
@@ -6,7 +6,7 @@ import { hostCore } from "./host.js";
export {};
-import { isActiveRigDecode } from "./active-rig.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract";
interface VdesMessage {
@@ -117,17 +117,24 @@ function vdesHexPreview(rawBytes: number[] | undefined): string {
.toUpperCase();
}
+/** What the selected rig heard. The history holds every rig's traffic, for
+ * the map; the panel is about the rig the operator is working. */
+function vdesRigMessages(): VdesMessage[] {
+ return forActiveRig(vdesMessageHistory);
+}
+
function updateVdesSummary() {
pruneVdesMessageHistory();
if (vdesChannelSummaryEl) {
vdesChannelSummaryEl.textContent = currentVdesCenterText();
}
+ const rigMessages = vdesRigMessages();
if (vdesFrameCountEl) {
- const count = vdesMessageHistory.length;
+ const count = rigMessages.length;
vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
}
if (vdesLatestSeenEl) {
- const latest = vdesMessageHistory[0];
+ const latest = rigMessages[0];
vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
}
}
@@ -281,7 +288,7 @@ function renderVdesHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (const message of vdesMessageHistory) {
+ for (const message of vdesRigMessages()) {
fragment.appendChild(renderVdesRow(message));
}
vdesMessagesEl.replaceChildren(fragment);
@@ -312,10 +319,11 @@ function normalizeServerVdesMessage(msg: VdesMessage): VdesMessage {
function onServerVdesBatch(messages: VdesMessage[]): void {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (vdesStatus) vdesStatus.textContent = "Receiving";
const normalized: VdesMessage[] = [];
for (const msg of messages) {
const next = normalizeServerVdesMessage(msg);
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -357,8 +365,8 @@ function plotVdesMessage(msg: VdesMessage): void {
}
function onServerVdes(msg: VdesMessage): void {
- if (vdesStatus) vdesStatus.textContent = "Receiving";
const next = normalizeServerVdesMessage(msg);
+ if (vdesStatus && isActiveRigDecode(next.rig_id)) vdesStatus.textContent = "Receiving";
addVdesMessage(next);
plotVdesMessage(next);
}
@@ -377,6 +385,7 @@ updateVdesSummary();
restore: onServerVdesBatch,
reset: resetVdesHistoryView,
prune: pruneVdesHistoryView,
+ rerender: () => { updateVdesBar(); renderVdesHistory(); },
// Oldest first, so tracks are rebuilt in the order they happened.
syncMap: () => { for (const entry of [...vdesMessageHistory].reverse()) plotVdesMessage(entry); },
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wefax.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wefax.ts
index bc0e4e45..687212f0 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wefax.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wefax.ts
@@ -4,11 +4,13 @@
import { hostCore } from "./host.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
interface WefaxImage {
+ rig_id?: string | null;
ts_ms?: number;
ioc?: number;
lpm?: number;
@@ -20,6 +22,7 @@ interface WefaxImage {
}
interface WefaxProgress {
+ rig_id?: string | null;
state?: string;
line_data?: string;
line_count?: number;
@@ -156,15 +159,21 @@ function paintLine(lineBytes: Uint8Array): void {
}
// ── Live view: latest image card ────────────────────────────────────
+/** The pictures the selected rig decoded, newest first. */
+function wefaxRigImages(): WefaxImage[] {
+ return forActiveRig(wefaxImageHistory);
+}
+
function renderWefaxLatestCard() {
if (!wefaxDom.liveLatest) return;
- if (wefaxImageHistory.length === 0) {
+ const rigImages = wefaxRigImages();
+ if (rigImages.length === 0) {
wefaxDom.liveLatest.innerHTML =
'No images decoded yet. Enable the decoder and tune to a WEFAX station.
';
return;
}
- const img = wefaxImageHistory[0];
+ const img = rigImages[0];
if (!img) return;
const ts = img._ts || '--';
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
@@ -193,7 +202,8 @@ function renderWefaxLatestCard() {
// ── History view: table ─────────────────────────────────────────────
function getWefaxFilteredHistory() {
- let items = wefaxImageHistory;
+ // Every rig's pictures are kept; the panel shows the selected rig's.
+ let items = wefaxRigImages();
if (wefaxFilterText) {
items = items.filter(function (i) {
@@ -253,7 +263,7 @@ function renderWefaxHistoryTable() {
wefaxDom.historyList.replaceChildren(fragment);
if (wefaxDom.historyCount) {
- const total = wefaxImageHistory.length;
+ const total = wefaxRigImages().length;
const shown = items.length;
wefaxDom.historyCount.textContent =
total === 0
@@ -298,6 +308,9 @@ function addWefaxImage(msg: WefaxImage): void {
// ── SSE event handlers (public API) ─────────────────────────────────
function onServerWefaxProgress(msg: WefaxProgress): void {
+ // The live canvas draws one picture at a time: a background rig's lines
+ // would paint straight over the one being watched.
+ if (!isActiveRigDecode(msg.rig_id)) return;
// State-only update (no image data): show decoder state in status.
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
@@ -331,6 +344,8 @@ function onServerWefaxProgress(msg: WefaxProgress): void {
function onServerWefax(msg: WefaxImage): void {
addWefaxImage(msg);
+ // Kept in the history either way, so switching to that rig finds it there.
+ if (!isActiveRigDecode(msg.rig_id)) return;
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
if (wefaxDom.status) {
@@ -438,6 +453,7 @@ wefaxWindow.trxPluginRuntime.registerDecoder({
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView,
+ rerender: () => { renderWefaxLatestCard(); renderWefaxHistoryTable(); },
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wspr.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wspr.ts
index 9ba9654d..192eef52 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wspr.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/wspr.ts
@@ -4,6 +4,7 @@
import { hostCore } from "./host.js";
+import { forActiveRig, isActiveRigDecode } from "./active-rig.js";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
@@ -15,6 +16,7 @@ interface WsprMessage {
snr_db?: number | undefined;
dt_s?: number | undefined;
freq_hz?: number | undefined;
+ rig_id?: string | null | undefined;
receiver?: unknown;
[key: string]: unknown;
}
@@ -102,10 +104,12 @@ function renderWsprRow(msg: WsprMessage): HTMLDivElement {
function renderWsprHistory(): void {
pruneWsprMessageHistory();
if (!wsprMessagesEl) return;
+ // The history holds every rig's decodes, for the map; the panel is about the
+ // rig the operator is working.
+ const rigMessages = forActiveRig(wsprMessageHistory);
const fragment = document.createDocumentFragment();
- for (let i = 0; i < wsprMessageHistory.length; i += 1) {
- const message = wsprMessageHistory[i];
- if (message) fragment.appendChild(renderWsprRow(message));
+ for (const message of rigMessages) {
+ fragment.appendChild(renderWsprRow(message));
}
wsprMessagesEl.replaceChildren(fragment);
}
@@ -130,6 +134,7 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
station,
rfHz,
history: {
+ rig_id: msg.rig_id ?? null,
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
@@ -142,10 +147,11 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
function onServerWsprBatch(messages: WsprMessage[]): void {
if (!Array.isArray(messages) || messages.length === 0) return;
- if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized: WsprMessage[] = [];
for (const msg of messages) {
const next = normalizeServerWsprMessage(msg);
+ // "Receiving" is the panel's own status, and the panel is the rig's.
+ 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,
@@ -312,7 +318,7 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
});
function onServerWspr(msg: WsprMessage): void {
- if (wsprStatus) wsprStatus.textContent = "Receiving";
+ 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, {
@@ -330,4 +336,5 @@ wsprWindow.trxPluginRuntime.registerDecoder({
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView,
+ rerender: renderWsprHistory,
});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/mini-view-rig.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/active-rig.test.mjs
similarity index 51%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/mini-view-rig.test.mjs
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/active-rig.test.mjs
index 6bdf0e13..71308b72 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/mini-view-rig.test.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/active-rig.test.mjs
@@ -2,11 +2,12 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
-// The mini views over the waterfall caption the spectrum below them, so they
-// show only what the rig on screen heard. The panels on the decoder tabs stay
-// aggregate — every rig's traffic lands there — which is the distinction these
-// tests pin down: a decode from a background rig belongs in the list and not in
-// the overlay.
+// The radio page and the digital modes page both describe one rig: the one the
+// operator selected. Every rig a client is connected to feeds the same decode
+// stream, so both the mini views over the waterfall and the panels behind them
+// filter on the rig that heard each decode. The map is the exception and keeps
+// all of them — it has its own rig filter — which is why the histories these
+// views read from are never pruned by rig.
import assert from "node:assert/strict";
import test from "node:test";
@@ -65,12 +66,14 @@ async function runPlugin(entry, { window, document: doc, extras = {} }) {
return context;
}
-test("the APRS mini view keeps to the active rig while the panel lists both", async () => {
+test("the APRS mini view and panel both keep to the active rig", async () => {
const overlay = new ElementFixture();
const packets = new ElementFixture();
+ const totals = new ElementFixture();
const elements = new Map([
["aprs-bar-overlay", overlay],
["aprs-packets", packets],
+ ["aprs-total-count", totals],
["mode", new ElementFixture("PKT")],
]);
const window = {
@@ -86,7 +89,8 @@ test("the APRS mini view keeps to the active rig while the panel lists both", as
assert.match(overlay.innerHTML, /SP1AAA/, "the active rig's frame is missing from the mini view");
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's frame reached the mini view");
- assert.equal(packets.children.length, 2, "the APRS panel dropped a frame it should still list");
+ assert.equal(packets.children.length, 1, "the APRS panel listed a background rig's frame");
+ assert.equal(totals.textContent, "1 total", `the APRS panel counted "${totals.textContent}"`);
});
test("the APRS mini view shows every frame until a rig is known", async () => {
@@ -196,3 +200,149 @@ test("the FT8 mini view keeps to the active rig", async () => {
assert.match(overlay.innerHTML, /SP1AAA/);
assert.doesNotMatch(overlay.innerHTML, /SP2BBB/, "a background rig's decode reached the mini view");
});
+
+// ── The digital modes panels ───────────────────────────────────────────────
+// The same rule one page over: a panel lists the selected rig's decodes only.
+
+test("the AIS panel lists the active rig's vessels only", async () => {
+ const messages = new ElementFixture();
+ const count = new ElementFixture();
+ const elements = new Map([
+ ["ais-messages", messages],
+ ["ais-vessel-count", count],
+ ["mode", new ElementFixture("AIS")],
+ ]);
+ const window = {
+ ...createHost({ state: { lastActiveRigId: "rig-a" } }),
+ trxUi: { confirm: async () => true },
+ aisMapAddVessel: () => {},
+ };
+ await runPlugin("ais", { window, document: createDocument(elements) });
+
+ const now = Date.now();
+ window.trxPluginRuntime.dispatch("ais", { mmsi: 261000001, vessel_name: "NEARBY", channel: "A", ts_ms: now, rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("ais", { mmsi: 261000002, vessel_name: "ELSEWHERE", channel: "A", ts_ms: now, rig_id: "rig-b" });
+
+ assert.equal(messages.children.length, 1, "the AIS panel listed a background rig's vessel");
+ assert.equal(count.textContent, "1 vessel", `the panel counted "${count.textContent}"`);
+});
+
+test("the VDES panel lists the active rig's bursts only", async () => {
+ const messages = new ElementFixture();
+ const count = new ElementFixture();
+ const elements = new Map([
+ ["vdes-messages", messages],
+ ["vdes-frame-count", count],
+ ["mode", new ElementFixture("VDES")],
+ ]);
+ const window = {
+ ...createHost({ state: { lastActiveRigId: "rig-a" } }),
+ trxUi: { confirm: async () => true },
+ vdesMapAddPoint: () => {},
+ };
+ await runPlugin("vdes", { window, document: createDocument(elements) });
+
+ const now = Date.now();
+ window.trxPluginRuntime.dispatch("vdes", { callsign: "SP1AAA", bit_len: 120, ts_ms: now, rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("vdes", { callsign: "SP2BBB", bit_len: 120, ts_ms: now, rig_id: "rig-b" });
+
+ assert.equal(messages.children.length, 1, "the VDES panel listed a background rig's burst");
+ assert.equal(count.textContent, "1 burst", `the panel counted "${count.textContent}"`);
+});
+
+test("the FT8 panel lists the active rig's decodes only", async () => {
+ const messages = new ElementFixture();
+ const elements = new Map([
+ ["ft8-messages", messages],
+ ["mode", new ElementFixture("DIG")],
+ ]);
+ const window = {
+ ...createHost({ state: { lastActiveRigId: "rig-a" } }),
+ ft8BaseHz: 7_074_000,
+ trxUi: { confirm: async () => true },
+ mapAddLocator: () => {},
+ setInterval() { return 1; },
+ };
+ await runPlugin("ft8", { window, document: createDocument(elements) });
+
+ const now = Date.now();
+ window.trxPluginRuntime.dispatch("ft8", { message: "CQ SP1AAA JO91", freq_hz: 500, ts_ms: now, rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("ft8", { message: "CQ SP2BBB JO94", freq_hz: 800, ts_ms: now, rig_id: "rig-b" });
+
+ assert.equal(messages.children.length, 1, "the FT8 panel listed a background rig's decode");
+});
+
+test("the WSPR panel lists the active rig's spots only", async () => {
+ const messages = new ElementFixture();
+ const elements = new Map([
+ ["wspr-messages", messages],
+ ["mode", new ElementFixture("DIG")],
+ ]);
+ const window = {
+ ...createHost({ state: { lastActiveRigId: "rig-a" } }),
+ ft8BaseHz: 14_095_600,
+ trxUi: { confirm: async () => true },
+ mapAddLocator: () => {},
+ };
+ await runPlugin("wspr", {
+ window,
+ document: createDocument(elements),
+ extras: { setInterval: () => 1 },
+ });
+
+ const now = Date.now();
+ window.trxPluginRuntime.dispatch("wspr", { message: "SP1AAA JO91 30", freq_hz: 1_500, ts_ms: now, rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("wspr", { message: "SP2BBB JO94 27", freq_hz: 1_520, ts_ms: now, rig_id: "rig-b" });
+
+ assert.equal(messages.children.length, 1, "the WSPR panel listed a background rig's spot");
+});
+
+test("the CW pane copies the active rig only", async () => {
+ const output = new ElementFixture();
+ const elements = new Map([
+ ["cw-output", output],
+ ["cw-bar-overlay", new ElementFixture()],
+ ["mode", new ElementFixture("CW")],
+ ]);
+ const window = {
+ ...createHost({ state: { lastActiveRigId: "rig-a" } }),
+ trxUi: { confirm: async () => true },
+ addEventListener() {},
+ };
+ await runPlugin("cw", { window, document: createDocument(elements) });
+
+ window.trxPluginRuntime.dispatch("cw", { text: "CQ ", wpm: 18, tone_hz: 700, rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("cw", { text: "XX ", wpm: 22, tone_hz: 600, rig_id: "rig-b" });
+ window.trxPluginRuntime.dispatch("cw", { text: "SP1AAA", wpm: 18, tone_hz: 700, rig_id: "rig-a" });
+
+ const copied = output.children.map((line) => line.textContent).join("");
+ assert.equal(copied, "CQ SP1AAA", `the pane copied "${copied}"`);
+});
+
+test("switching rigs repaints the panels through the runtime", async () => {
+ const packets = new ElementFixture();
+ const elements = new Map([
+ ["aprs-packets", packets],
+ ["aprs-bar-overlay", new ElementFixture()],
+ ["mode", new ElementFixture("PKT")],
+ ]);
+ // A mutable host: the rig picker writes lastActiveRigId, and the panels read
+ // it as they paint.
+ const host = createHost({ state: { lastActiveRigId: "rig-a" } });
+ let activeRigId = "rig-a";
+ Object.defineProperty(host.trx.state, "lastActiveRigId", { get: () => activeRigId });
+ const window = { ...host, trxUi: { confirm: async () => true }, aprsMapAddStation: () => {} };
+ await runPlugin("aprs", { window, document: createDocument(elements) });
+
+ const frame = { dest_call: "APRS", packet_type: "position", crc_ok: true, info: "beacon" };
+ window.trxPluginRuntime.dispatch("aprs", { ...frame, src_call: "SP1AAA", rig_id: "rig-a" });
+ window.trxPluginRuntime.dispatch("aprs", { ...frame, src_call: "SP2BBB", rig_id: "rig-b" });
+ assert.equal(packets.children.length, 1);
+
+ activeRigId = "rig-b";
+ window.trxPluginRuntime.rerenderAll();
+ assert.equal(packets.children.length, 1, "the panel did not follow the switch");
+ const shown = packets.children.map((row) => row.innerHTML).join("");
+ assert.match(shown, /SP2BBB/, "the panel still shows the rig that was switched away from");
+ assert.doesNotMatch(shown, /SP1AAA/);
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
index 6159bc13..7689c121 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
@@ -22,9 +22,9 @@ const BEACON = {
packet_type: "position", crc_ok: true, lat: 54.35, lon: 18.65,
symbol_table: "/", symbol_code: ">", rig_id: "rig-a",
};
-// A second rig listening in the background, on its own band. Its traffic
-// belongs in the panels, which aggregate every rig, but not in the mini view,
-// which captions the spectrum of the rig on screen.
+// A second rig listening in the background, on its own band. Both pages
+// describe the rig on screen, so its traffic belongs in neither the panel nor
+// the mini view — only on the map, which shows the whole station.
const OTHER_RIG_VESSEL = {
...VESSEL, mmsi: 244660001, vessel_name: "ELDERBERRY", callsign: "PBTY",
lat: 51.92, lon: 4.48, rig_id: "rig-b",
@@ -51,12 +51,13 @@ try {
aisStatus: document.getElementById("ais-status")?.textContent ?? "",
aprsStatus: document.getElementById("aprs-status")?.textContent ?? "",
otherRig: document.getElementById("ais-messages")?.textContent.includes("ELDERBERRY") ?? false,
+ aisRows: document.getElementById("ais-messages")?.children.length ?? 0,
mapLoaded: !!window.trx.modules.map,
}));
assert.equal(panels.mapLoaded, false, "the map module was loaded, so this proves nothing");
assert.ok(panels.ais > 0, `the AIS panel is empty (status: ${panels.aisStatus})`);
assert.ok(panels.aprs > 0, `the APRS panel is empty (status: ${panels.aprsStatus})`);
- assert.equal(panels.otherRig, true, "the AIS panel dropped the background rig's vessel");
+ assert.equal(panels.otherRig, false, "the AIS panel listed a background rig's vessel");
// The mini view rides over the waterfall on the radio page.
await page.locator('.tab[data-tab="main"]').click();
@@ -102,8 +103,11 @@ try {
: Object.keys(collection ?? {}).length);
return { ais: size(map?.aisMarkers), stations: size(map?.stationMarkers) };
});
- assert.ok(markers.ais > 0, "the vessel never reached the map");
assert.ok(markers.stations > 0, "the APRS station never reached the map");
+ // The map is the whole station's view — the one place a background rig's
+ // traffic belongs — so both vessels are on it even though the panel and the
+ // mini view show only the selected rig's.
+ assert.equal(markers.ais, 2, `${markers.ais} of 2 vessels reached the map`);
assert.deepEqual(runtimeErrors, []);
} finally {