Complete TypeScript frontend migration #22

Merged
sjg merged 51 commits from feat/typescript-frontend-migration into main 2026-08-01 23:06:19 +02:00
19 changed files with 255 additions and 319 deletions
Showing only changes of commit 9fef0ebc7b - Show all commits
@@ -488,12 +488,9 @@ function currentDecodeHistoryRetentionMs() {
}
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() {
window.trxPluginRuntime.prune("aprs");
window.trxPluginRuntime.prune("hf_aprs");
window.trxPluginRuntime.prune("ais");
window.trxPluginRuntime.prune("vdes");
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
for (const decoder of ["aprs", "hf_aprs", "ais", "vdes", "ft8", "ft4", "ft2", "wspr", "wefax"]) {
window.trxPluginRuntime.prune(decoder);
}
};
function syncTopBarAccess() {
const loggedOut = authEnabled && !authRole;
@@ -5857,18 +5854,7 @@ function updateDecodeStatus(text) {
}
}
function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") {
window.trxPluginRuntime.dispatch(msg.type, msg);
}
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg);
if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg);
if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg);
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.modules.map?.scheduleStatsRender();
@@ -5884,27 +5870,9 @@ function dispatchDecodeBatch(batch) {
window.trx.modules.map?.scheduleStatsRender();
const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) {
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") {
window.trxPluginRuntime.dispatchBatch(type, batch);
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
window.onServerFt8Batch(batch);
return;
}
if (type === "ft4" && window.onServerFt4Batch) {
window.onServerFt4Batch(batch);
return;
}
if (type === "ft2" && window.onServerFt2Batch) {
window.onServerFt2Batch(batch);
return;
}
if (type === "wspr" && window.onServerWsprBatch) {
window.onServerWsprBatch(batch);
return;
}
if (uniformType && type) {
window.trxPluginRuntime.dispatchBatch(type, batch);
return;
}
for (const msg of batch) {
dispatchDecodeMessage(msg, true);
@@ -5954,34 +5922,7 @@ function restoreDecodeHistoryGroup(kind, messages) {
}
window.trx.modules.map?.scheduleStatsRender();
}
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") {
window.trxPluginRuntime.restore(kind, messages);
return;
}
if (kind === "cw" && window.restoreCwHistory) {
window.restoreCwHistory(messages);
return;
}
if (kind === "ft8" && window.restoreFt8History) {
window.restoreFt8History(messages);
return;
}
if (kind === "ft4" && window.restoreFt4History) {
window.restoreFt4History(messages);
return;
}
if (kind === "ft2" && window.restoreFt2History) {
window.restoreFt2History(messages);
return;
}
if (kind === "wspr" && window.restoreWsprHistory) {
window.restoreWsprHistory(messages);
return;
}
if (kind === "wefax" && window.restoreWefaxHistory) {
window.restoreWefaxHistory(messages);
return;
}
window.trxPluginRuntime.restore(kind, messages);
}
function connectDecode() {
if (decodeSource) {
@@ -5991,14 +5932,7 @@ function connectDecode() {
decodeHistoryReplayActive = false;
decodeMapSyncPending = false;
window.trxPluginRuntime.clearQueued();
window.trxPluginRuntime.reset("ais");
window.trxPluginRuntime.reset("vdes");
window.trxPluginRuntime.reset("aprs");
window.trxPluginRuntime.reset("hf_aprs");
if (window.resetCwHistoryView) window.resetCwHistoryView();
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
window.trxPluginRuntime.resetAll();
let historySettled = false;
let historyWorkerDone = false;
let historyFallbackStarted = false;
@@ -82,7 +82,7 @@
}
cwWindow.updateCwBar = updateCwBar;
cwWindow.clearCwBar = function() {
cwWindow.resetCwHistoryView?.();
resetCwHistoryView();
};
cwWindow.closeCwBar = function() {
cwBarDismissedAtMs = Date.now();
@@ -302,26 +302,26 @@
void setCwTone(tone);
});
}
cwWindow.resetCwHistoryView = function() {
function resetCwHistoryView() {
if (cwOutputEl) cwOutputEl.innerHTML = "";
cwLastAppendTime = 0;
cwBarHistory = [];
cwBarCurrentLine = null;
updateCwBar();
drawCwTonePicker();
};
}
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
void (async () => {
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await cwWindow.postPath?.("/clear_cw_decode");
cwWindow.resetCwHistoryView?.();
resetCwHistoryView();
} catch (error) {
console.error("CW history clear failed", error);
}
})();
});
cwWindow.onServerCw = function(evt) {
function onServerCw(evt) {
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
if (evt.text && cwOutputEl) {
const now = Date.now();
@@ -375,14 +375,20 @@
cwTonePickerRaf = null;
drawCwTonePicker();
});
};
cwWindow.restoreCwHistory = function(events) {
}
function restoreCwHistory(events) {
if (!Array.isArray(events) || events.length === 0) return;
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
for (const evt of events) {
cwWindow.onServerCw?.(evt);
onServerCw(evt);
}
};
}
cwWindow.trxPluginRuntime.registerDecoder({
id: "cw",
onMessage: onServerCw,
restore: restoreCwHistory,
reset: resetCwHistoryView
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution();
drawCwTonePicker();
@@ -167,19 +167,19 @@
}
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
};
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
bridge[`onServer${capitalized}Batch`] = (messages) => {
if (Array.isArray(messages)) receiveBatch(messages);
};
bridge[`restore${capitalized}History`] = receiveBatch;
bridge[`prune${capitalized}HistoryView`] = () => {
prune();
render();
};
bridge[`reset${capitalized}HistoryView`] = reset;
bridge[`onServer${capitalized}`] = (message) => {
receiveBatch([message]);
};
bridge.trxPluginRuntime.registerDecoder({
id,
onMessage: (message) => {
receiveBatch([message]);
},
onBatch: receiveBatch,
restore: receiveBatch,
prune: () => {
prune();
render();
},
reset
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -167,19 +167,19 @@
}
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
};
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
bridge[`onServer${capitalized}Batch`] = (messages) => {
if (Array.isArray(messages)) receiveBatch(messages);
};
bridge[`restore${capitalized}History`] = receiveBatch;
bridge[`prune${capitalized}HistoryView`] = () => {
prune();
render();
};
bridge[`reset${capitalized}HistoryView`] = reset;
bridge[`onServer${capitalized}`] = (message) => {
receiveBatch([message]);
};
bridge.trxPluginRuntime.registerDecoder({
id,
onMessage: (message) => {
receiveBatch([message]);
},
onBatch: receiveBatch,
restore: receiveBatch,
prune: () => {
prune();
render();
},
reset
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -91,7 +91,7 @@
};
bridge.updateFt8Bar = update;
bridge.clearFt8Bar = () => {
({ ft8: bridge.resetFt8HistoryView, ft4: bridge.resetFt4HistoryView, ft2: bridge.resetFt2HistoryView })[active]?.();
bridge.trxPluginRuntime.reset(active);
};
bridge.closeFt8Bar = () => {
dismissed[active] = Date.now();
@@ -209,19 +209,19 @@
}
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
};
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
bridge[`onServer${capitalized}Batch`] = (messages) => {
if (Array.isArray(messages)) receiveBatch(messages);
};
bridge[`restore${capitalized}History`] = receiveBatch;
bridge[`prune${capitalized}HistoryView`] = () => {
prune();
render();
};
bridge[`reset${capitalized}HistoryView`] = reset;
bridge[`onServer${capitalized}`] = (message) => {
receiveBatch([message]);
};
bridge.trxPluginRuntime.registerDecoder({
id,
onMessage: (message) => {
receiveBatch([message]);
},
onBatch: receiveBatch,
restore: receiveBatch,
prune: () => {
prune();
render();
},
reset
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = () => {
if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
@@ -207,29 +207,39 @@
});
}
}
satWindow.onServerLrptProgress = function(msg) {
function onServerLrptProgress(msg) {
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
}
};
satWindow.onServerLrptImage = function(msg) {
}
function onServerLrptImage(msg) {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
satWindow.addSatMapOverlay(msg);
}
};
satWindow.resetSatHistoryView = function() {
}
function resetSatHistoryView() {
satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard();
renderSatHistoryTable();
satWindow.clearSatMapOverlays?.();
};
satWindow.pruneSatHistoryView = function() {
}
function pruneSatHistoryView() {
renderSatHistoryTable();
renderSatLatestCard();
};
}
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_image",
onMessage: onServerLrptImage,
reset: resetSatHistoryView,
prune: pruneSatHistoryView
});
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_progress",
onMessage: onServerLrptProgress
});
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
lrptDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
@@ -257,7 +267,7 @@
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await satWindow.postPath?.("/clear_lrpt_decode");
satWindow.resetSatHistoryView?.();
resetSatHistoryView();
} catch (e) {
console.error("Weather satellite history clear failed", e);
}
@@ -205,7 +205,7 @@
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
}
wefaxWindow.onServerWefaxProgress = function(msg) {
function onServerWefaxProgress(msg) {
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
wefaxDom.status.textContent = msg.state;
@@ -229,16 +229,16 @@
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = "var(--text-accent)";
}
};
wefaxWindow.onServerWefax = function(msg) {
}
function onServerWefax(msg) {
addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
if (wefaxDom.status) {
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = "";
}
};
wefaxWindow.restoreWefaxHistory = function(messages) {
}
function restoreWefaxHistory(messages) {
if (!messages.length) return;
for (const message of messages) {
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
@@ -255,13 +255,13 @@
if (wefaxActiveView === "history") {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
};
wefaxWindow.pruneWefaxHistoryView = function() {
}
function pruneWefaxHistoryView() {
pruneWefaxHistory();
renderWefaxHistoryTable();
renderWefaxLatestCard();
};
wefaxWindow.resetWefaxHistoryView = function() {
}
function resetWefaxHistoryView() {
wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
@@ -273,7 +273,7 @@
wefaxDom.status.textContent = "Idle";
wefaxDom.status.style.color = "";
}
};
}
if (wefaxDom.filterInput) {
const filterInput = wefaxDom.filterInput;
wefaxDom.filterInput.addEventListener("input", function() {
@@ -313,7 +313,7 @@
void (async () => {
try {
await wefaxWindow.postPath?.("/clear_wefax_decode");
wefaxWindow.resetWefaxHistoryView?.();
resetWefaxHistoryView();
} catch (e) {
console.error("WEFAX clear failed", e);
}
@@ -321,4 +321,15 @@
});
}
renderWefaxLatestCard();
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax",
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
onMessage: onServerWefaxProgress
});
})();
@@ -97,7 +97,7 @@
}
};
}
wsprWindow.onServerWsprBatch = function(messages) {
function onServerWsprBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized = [];
@@ -116,15 +116,11 @@
wsprMessageHistory = normalized.concat(wsprMessageHistory);
pruneWsprMessageHistory();
scheduleWsprHistoryRender();
};
wsprWindow.restoreWsprHistory = function(messages) {
const callback = wsprWindow.onServerWsprBatch;
if (typeof callback === "function") callback(messages);
};
wsprWindow.pruneWsprHistoryView = function() {
}
function pruneWsprHistoryView() {
pruneWsprMessageHistory();
renderWsprHistory();
};
}
function escapeWsprHtml(input) {
return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
}
@@ -205,12 +201,12 @@
const message = row.dataset.message || "";
row.style.display = message.includes(wsprFilterText) ? "" : "none";
}
wsprWindow.resetWsprHistoryView = function() {
function resetWsprHistoryView() {
if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
wsprMessageHistory = [];
renderWsprHistory();
if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
};
}
if (wsprFilterInput) {
wsprFilterInput.addEventListener("input", () => {
wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
@@ -246,13 +242,13 @@
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await wsprWindow.postPath?.("/clear_wspr_decode");
wsprWindow.resetWsprHistoryView?.();
resetWsprHistoryView();
} catch (error) {
console.error("WSPR history clear failed", error);
}
})();
});
wsprWindow.onServerWspr = function(msg) {
function onServerWspr(msg) {
if (wsprStatus) wsprStatus.textContent = "Receiving";
const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
@@ -262,5 +258,13 @@
});
}
addWsprMessage(next.history);
};
}
wsprWindow.trxPluginRuntime.registerDecoder({
id: "wspr",
onMessage: onServerWspr,
onBatch: onServerWsprBatch,
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView
});
})();
@@ -567,12 +567,9 @@ function currentDecodeHistoryRetentionMs() {
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() {
window.trxPluginRuntime.prune("aprs");
window.trxPluginRuntime.prune("hf_aprs");
window.trxPluginRuntime.prune("ais");
window.trxPluginRuntime.prune("vdes");
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
for (const decoder of ["aprs", "hf_aprs", "ais", "vdes", "ft8", "ft4", "ft2", "wspr", "wefax"]) {
window.trxPluginRuntime.prune(decoder);
}
};
function syncTopBarAccess() {
@@ -6215,18 +6212,7 @@ function updateDecodeStatus(text) {
}
}
function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") {
window.trxPluginRuntime.dispatch(msg.type, msg);
}
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg);
if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg);
if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg);
if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg);
if (msg.type === "wefax" && window.onServerWefax) window.onServerWefax(msg);
if (msg.type === "wefax_progress" && window.onServerWefaxProgress) window.onServerWefaxProgress(msg);
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress" && msg.type !== "wefax" && msg.type !== "wefax_progress") {
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
window.trx.modules.map?.scheduleStatsRender();
@@ -6244,27 +6230,9 @@ function dispatchDecodeBatch(batch) {
window.trx.modules.map?.scheduleStatsRender();
const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) {
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") {
window.trxPluginRuntime.dispatchBatch(type, batch);
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
window.onServerFt8Batch(batch);
return;
}
if (type === "ft4" && window.onServerFt4Batch) {
window.onServerFt4Batch(batch);
return;
}
if (type === "ft2" && window.onServerFt2Batch) {
window.onServerFt2Batch(batch);
return;
}
if (type === "wspr" && window.onServerWsprBatch) {
window.onServerWsprBatch(batch);
return;
}
if (uniformType && type) {
window.trxPluginRuntime.dispatchBatch(type, batch);
return;
}
for (const msg of batch) {
dispatchDecodeMessage(msg, true);
@@ -6318,34 +6286,7 @@ function restoreDecodeHistoryGroup(kind, messages) {
}
window.trx.modules.map?.scheduleStatsRender();
}
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") {
window.trxPluginRuntime.restore(kind, messages);
return;
}
if (kind === "cw" && window.restoreCwHistory) {
window.restoreCwHistory(messages);
return;
}
if (kind === "ft8" && window.restoreFt8History) {
window.restoreFt8History(messages);
return;
}
if (kind === "ft4" && window.restoreFt4History) {
window.restoreFt4History(messages);
return;
}
if (kind === "ft2" && window.restoreFt2History) {
window.restoreFt2History(messages);
return;
}
if (kind === "wspr" && window.restoreWsprHistory) {
window.restoreWsprHistory(messages);
return;
}
if (kind === "wefax" && window.restoreWefaxHistory) {
window.restoreWefaxHistory(messages);
return;
}
window.trxPluginRuntime.restore(kind, messages);
}
function connectDecode() {
@@ -6354,14 +6295,7 @@ function connectDecode() {
decodeHistoryReplayActive = false;
decodeMapSyncPending = false;
window.trxPluginRuntime.clearQueued();
window.trxPluginRuntime.reset("ais");
window.trxPluginRuntime.reset("vdes");
window.trxPluginRuntime.reset("aprs");
window.trxPluginRuntime.reset("hf_aprs");
if (window.resetCwHistoryView) window.resetCwHistoryView();
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
window.trxPluginRuntime.resetAll();
// Buffer live messages until history fetch settles so history always appears
// before any live updates, regardless of network ordering.
@@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
type Rgba = [number, number, number, number];
@@ -36,12 +38,9 @@ interface CwBridge {
updateCwBar?: () => void;
clearCwBar?: () => void;
closeCwBar?: () => void;
resetCwHistoryView?: () => void;
onServerCw?: (event: CwEvent) => void;
restoreCwHistory?: (events: CwEvent[]) => void;
refreshCwTonePicker?: () => void;
}
const cwWindow = window as unknown as CwBridge;
const cwWindow = window as unknown as CwBridge & PluginRuntimeWindow;
// --- CW (Morse) Decoder Plugin (server-side decode) ---
const cwStatusEl = document.getElementById("cw-status");
@@ -154,7 +153,7 @@ function updateCwBar(): void {
}
cwWindow.updateCwBar = updateCwBar;
cwWindow.clearCwBar = function() {
cwWindow.resetCwHistoryView?.();
resetCwHistoryView();
};
cwWindow.closeCwBar = function() {
cwBarDismissedAtMs = Date.now();
@@ -401,21 +400,21 @@ if (cwToneCanvas) {
});
}
cwWindow.resetCwHistoryView = function() {
function resetCwHistoryView(): void {
if (cwOutputEl) cwOutputEl.innerHTML = "";
cwLastAppendTime = 0;
cwBarHistory = [];
cwBarCurrentLine = null;
updateCwBar();
drawCwTonePicker();
};
}
document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
void (async () => {
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await cwWindow.postPath?.("/clear_cw_decode");
cwWindow.resetCwHistoryView?.();
resetCwHistoryView();
} catch (error: unknown) {
console.error("CW history clear failed", error);
}
@@ -423,7 +422,7 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
});
// --- Server-side CW decode handler ---
cwWindow.onServerCw = function(evt: CwEvent) {
function onServerCw(evt: CwEvent): void {
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
if (evt.text && cwOutputEl) {
// Append decoded text to output
@@ -479,15 +478,22 @@ cwWindow.onServerCw = function(evt: CwEvent) {
cwTonePickerRaf = null;
drawCwTonePicker();
});
};
}
cwWindow.restoreCwHistory = function(events: CwEvent[]) {
function restoreCwHistory(events: CwEvent[]): void {
if (!Array.isArray(events) || events.length === 0) return;
if (cwStatusEl) cwStatusEl.textContent = "Receiving";
for (const evt of events) {
cwWindow.onServerCw?.(evt);
onServerCw(evt);
}
};
}
cwWindow.trxPluginRuntime.registerDecoder({
id: "cw",
onMessage: onServerCw,
restore: restoreCwHistory,
reset: resetCwHistoryView,
});
cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution();
@@ -2,9 +2,11 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export type FtxDecoderId = "ft2" | "ft4" | "ft8";
interface FtxMessage {
export interface FtxMessage {
message?: string | undefined;
ts_ms?: number | undefined;
_tsMs?: number | undefined;
@@ -46,9 +48,6 @@ interface FtxBridge {
postPath?: (path: string) => Promise<unknown>;
fmtTime?: (timestampMs: number) => string;
trxUi: ConfirmApi;
resetFt2HistoryView?: () => void;
resetFt4HistoryView?: () => void;
resetFt8HistoryView?: () => void;
clearFt8Bar?: () => void;
closeFt8Bar?: () => void;
[key: string]: unknown;
@@ -61,7 +60,7 @@ interface FtxConfig {
periodDigits?: number;
}
const bridge = window as unknown as FtxBridge;
const bridge = window as unknown as FtxBridge & PluginRuntimeWindow;
function finiteNumber(value: unknown): number | null {
const number = typeof value === "number" ? value : Number(value);
@@ -164,7 +163,7 @@ export function initializeFt8FamilyBar(): void {
bridge.setFt8FamilyBarDecoder = (decoder) => { active = decoder; update(); };
bridge.updateFt8Bar = update;
bridge.clearFt8Bar = () => {
({ ft8: bridge.resetFt8HistoryView, ft4: bridge.resetFt4HistoryView, ft2: bridge.resetFt2HistoryView })[active]?.();
bridge.trxPluginRuntime.reset(active);
};
bridge.closeFt8Bar = () => {
dismissed[active] = Date.now();
@@ -285,12 +284,14 @@ export function initializeFtxDecoder(config: FtxConfig): void {
return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
};
const capitalized = `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
bridge[`onServer${capitalized}Batch`] = (messages: FtxMessage[]) => { if (Array.isArray(messages)) receiveBatch(messages); };
bridge[`restore${capitalized}History`] = receiveBatch;
bridge[`prune${capitalized}HistoryView`] = () => { prune(); render(); };
bridge[`reset${capitalized}HistoryView`] = reset;
bridge[`onServer${capitalized}`] = (message: FtxMessage) => { receiveBatch([message]); };
bridge.trxPluginRuntime.registerDecoder({
id,
onMessage: (message: FtxMessage) => { receiveBatch([message]); },
onBatch: receiveBatch,
restore: receiveBatch,
prune: () => { prune(); render(); },
reset,
});
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
const updatePeriod = (): void => {
@@ -3,17 +3,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import type { LrptProgress, SatelliteImage, SatelliteLiveUpdate, SatellitePass, SatellitePassResponse } from "./satellite-types";
import type { PluginRuntimeWindow } from "./runtime-contract.js";
type SatelliteView = "live" | "history" | "predictions";
interface SatelliteBridge {
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
clearSatPredictionDom?: () => void;
updateSatLiveState?: (update: SatelliteLiveUpdate) => void;
onServerLrptProgress?: (message: LrptProgress) => void;
onServerLrptImage?: (message: SatelliteImage) => void;
addSatMapOverlay?: (image: SatelliteImage) => void;
resetSatHistoryView?: () => void;
pruneSatHistoryView?: () => void;
clearSatMapOverlays?: () => void;
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
postPath?: (path: string) => Promise<unknown>;
@@ -22,7 +19,7 @@ interface SatelliteBridge {
enableMapSourceFilter?: (source: string) => void;
navigateToAprsMap?: (lat: number, lon: number) => void;
}
const satWindow = window as unknown as SatelliteBridge;
const satWindow = window as unknown as SatelliteBridge & PluginRuntimeWindow;
// --- SAT Plugin ---
// Live view: decoder state, latest image card
@@ -265,32 +262,43 @@ function addSatImage(img: SatelliteImage, decoder: "lrpt"): void {
}
// ── Server callbacks ────────────────────────────────────────────────
satWindow.onServerLrptProgress = function (msg: LrptProgress) {
function onServerLrptProgress(msg: LrptProgress): void {
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
satDom.status.textContent = `Receiving \u2014 ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
}
};
}
satWindow.onServerLrptImage = function (msg: SatelliteImage) {
function onServerLrptImage(msg: SatelliteImage): void {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
satWindow.addSatMapOverlay(msg);
}
};
}
satWindow.resetSatHistoryView = function () {
function resetSatHistoryView(): void {
satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard();
renderSatHistoryTable();
satWindow.clearSatMapOverlays?.();
};
}
satWindow.pruneSatHistoryView = function () {
function pruneSatHistoryView(): void {
renderSatHistoryTable();
renderSatLatestCard();
};
}
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_image",
onMessage: onServerLrptImage,
reset: resetSatHistoryView,
prune: pruneSatHistoryView,
});
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_progress",
onMessage: onServerLrptProgress,
});
// ── Toggle buttons ──────────────────────────────────────────────────
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
@@ -320,7 +328,7 @@ document
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await satWindow.postPath?.("/clear_lrpt_decode");
satWindow.resetSatHistoryView?.();
resetSatHistoryView();
} catch (e) {
console.error("Weather satellite history clear failed", e);
}
@@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
interface WefaxImage {
@@ -29,15 +31,10 @@ interface WefaxBridge {
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
postPath?: (path: string) => Promise<unknown>;
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
onServerWefaxProgress?: (message: WefaxProgress) => void;
onServerWefax?: (message: WefaxImage) => void;
restoreWefaxHistory?: (messages: WefaxImage[]) => void;
pruneWefaxHistoryView?: () => void;
resetWefaxHistoryView?: () => void;
syncWefaxToggle?: (enabled: boolean) => void;
}
const wefaxWindow = window as unknown as WefaxBridge;
const wefaxWindow = window as unknown as WefaxBridge & PluginRuntimeWindow;
// ---------------------------------------------------------------------------
// wefax.js — WEFAX decoder plugin for trx-frontend-http
@@ -299,7 +296,7 @@ function addWefaxImage(msg: WefaxImage): void {
}
// ── SSE event handlers (public API) ─────────────────────────────────
wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) {
function onServerWefaxProgress(msg: WefaxProgress): void {
// State-only update (no image data): show decoder state in status.
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
@@ -329,9 +326,9 @@ wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) {
wefaxDom.status.textContent = `Receiving \u2014 line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = 'var(--text-accent)';
}
};
}
wefaxWindow.onServerWefax = function (msg: WefaxImage) {
function onServerWefax(msg: WefaxImage): void {
addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
@@ -339,9 +336,9 @@ wefaxWindow.onServerWefax = function (msg: WefaxImage) {
wefaxDom.status.textContent = `Complete \u2014 ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = '';
}
};
}
wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) {
function restoreWefaxHistory(messages: WefaxImage[]): void {
if (!messages.length) return;
for (const message of messages) {
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
@@ -358,15 +355,15 @@ wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) {
if (wefaxActiveView === 'history') {
scheduleWefaxUi('wefax-history', renderWefaxHistoryTable);
}
};
}
wefaxWindow.pruneWefaxHistoryView = function () {
function pruneWefaxHistoryView(): void {
pruneWefaxHistory();
renderWefaxHistoryTable();
renderWefaxLatestCard();
};
}
wefaxWindow.resetWefaxHistoryView = function () {
function resetWefaxHistoryView(): void {
wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = '';
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
@@ -378,7 +375,7 @@ wefaxWindow.resetWefaxHistoryView = function () {
wefaxDom.status.textContent = 'Idle';
wefaxDom.status.style.color = '';
}
};
}
// ── Filter / sort handlers ──────────────────────────────────────────
if (wefaxDom.filterInput) {
@@ -424,7 +421,7 @@ if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener('click', () => { void (async () => {
try {
await wefaxWindow.postPath?.('/clear_wefax_decode');
wefaxWindow.resetWefaxHistoryView?.();
resetWefaxHistoryView();
} catch (e) {
console.error('WEFAX clear failed', e);
}
@@ -433,3 +430,15 @@ if (wefaxDom.clearBtn) {
// ── Initial render ──────────────────────────────────────────────────
renderWefaxLatestCard();
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax",
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView,
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
onMessage: onServerWefaxProgress,
});
@@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import type { PluginRuntimeWindow } from "./runtime-contract.js";
export {};
interface WsprMessage {
@@ -25,14 +27,9 @@ interface WsprBridge {
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<void>;
postPath?: (path: string) => Promise<unknown>;
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
onServerWsprBatch?: (messages: WsprMessage[]) => void;
restoreWsprHistory?: (messages: WsprMessage[]) => void;
pruneWsprHistoryView?: () => void;
resetWsprHistoryView?: () => void;
onServerWspr?: (message: WsprMessage) => void;
[key: string]: unknown;
}
const wsprWindow = window as unknown as WsprBridge;
const wsprWindow = window as unknown as WsprBridge & PluginRuntimeWindow;
// --- WSPR Decoder Plugin (server-side decode) ---
const wsprStatus = document.getElementById("wspr-status");
@@ -142,7 +139,7 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
};
}
wsprWindow.onServerWsprBatch = function(messages: WsprMessage[]) {
function onServerWsprBatch(messages: WsprMessage[]): void {
if (!Array.isArray(messages) || messages.length === 0) return;
if (wsprStatus) wsprStatus.textContent = "Receiving";
const normalized: WsprMessage[] = [];
@@ -161,17 +158,12 @@ wsprWindow.onServerWsprBatch = function(messages: WsprMessage[]) {
wsprMessageHistory = normalized.concat(wsprMessageHistory);
pruneWsprMessageHistory();
scheduleWsprHistoryRender();
};
}
wsprWindow.restoreWsprHistory = function(messages: WsprMessage[]) {
const callback = wsprWindow.onServerWsprBatch;
if (typeof callback === "function") callback(messages);
};
wsprWindow.pruneWsprHistoryView = function() {
function pruneWsprHistoryView(): void {
pruneWsprMessageHistory();
renderWsprHistory();
};
}
function escapeWsprHtml(input: string): string {
return input
@@ -266,12 +258,12 @@ function applyWsprFilterToRow(row: HTMLElement): void {
row.style.display = message.includes(wsprFilterText) ? "" : "none";
}
wsprWindow.resetWsprHistoryView = function() {
function resetWsprHistoryView(): void {
if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
wsprMessageHistory = [];
renderWsprHistory();
if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
};
}
if (wsprFilterInput) {
wsprFilterInput.addEventListener("input", () => {
@@ -311,14 +303,14 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await wsprWindow.postPath?.("/clear_wspr_decode");
wsprWindow.resetWsprHistoryView?.();
resetWsprHistoryView();
} catch (error: unknown) {
console.error("WSPR history clear failed", error);
}
})();
});
wsprWindow.onServerWspr = function(msg: WsprMessage) {
function onServerWspr(msg: WsprMessage): void {
if (wsprStatus) wsprStatus.textContent = "Receiving";
const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
@@ -328,4 +320,13 @@ wsprWindow.onServerWspr = function(msg: WsprMessage) {
});
}
addWsprMessage(next.history);
};
}
wsprWindow.trxPluginRuntime.registerDecoder({
id: "wspr",
onMessage: onServerWspr,
onBatch: onServerWsprBatch,
restore: onServerWsprBatch,
prune: pruneWsprHistoryView,
reset: resetWsprHistoryView,
});
@@ -37,13 +37,15 @@ test("CW entry appends server-decoded text and registers lifecycle callbacks", a
Math,
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/cw.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
window.onServerCw({ text: "CQ", wpm: 18, tone_hz: 700, signal_on: true });
window.trxPluginRuntime.dispatch("cw", { text: "CQ", wpm: 18, tone_hz: 700, signal_on: true });
assert.equal(status.textContent, "Receiving");
assert.equal(output.children.length, 1);
assert.equal(output.children[0].textContent, "CQ");
assert.equal(typeof window.resetCwHistoryView, "function");
assert.equal(typeof window.restoreCwHistory, "function");
assert.equal(window.onServerCw, undefined);
assert.equal(window.trxPluginRuntime.hasDecoder("cw"), true);
});
@@ -29,10 +29,12 @@ test("FT2 entry normalizes audio offsets and registers typed callbacks", async (
Array,
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/ft2.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
window.onServerFt2({ message: "CQ SP0ABC JO91", freq_hz: 1_250, ts_ms: Date.now(), snr_db: -8 });
window.trxPluginRuntime.dispatch("ft2", { message: "CQ SP0ABC JO91", freq_hz: 1_250, ts_ms: Date.now(), snr_db: -8 });
assert.equal(mapMessages.length, 1);
assert.equal(mapMessages[0][4].freq_hz, 14_075_250);
const bar = barRenderer();
@@ -59,12 +61,14 @@ test("FT8 entry installs shared parsing without relying on script globals", asyn
Set,
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/ft8.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
const details = window.ft8ExtractLocatorDetails("K1ABC SP0ABC JO91");
assert.equal(details[0].source, "SP0ABC");
assert.equal(details[0].target, "K1ABC");
window.onServerFt8({ message: "K1ABC SP0ABC JO91", freq_hz: 500, ts_ms: Date.now() });
window.trxPluginRuntime.dispatch("ft8", { message: "K1ABC SP0ABC JO91", freq_hz: 500, ts_ms: Date.now() });
assert.equal(forwarded[0][4].freq_hz, 7_074_500);
});
@@ -26,12 +26,14 @@ test("satellite entry registers lifecycle callbacks and forwards georeferenced i
Error,
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/sat.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
window.onServerLrptImage({ path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 });
window.trxPluginRuntime.dispatch("lrpt_image", { path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 });
assert.equal(overlays.length, 1);
assert.deepEqual(overlays[0].geo_bounds, [50, 10, 55, 20]);
assert.equal(typeof window.updateSatLiveState, "function");
assert.equal(typeof window.resetSatHistoryView, "function");
assert.equal(window.onServerLrptImage, undefined);
});
@@ -19,12 +19,14 @@ test("WEFAX entry exposes typed lifecycle handlers and renders decoder state", a
Uint8Array,
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/wefax.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
assert.equal(typeof window.onServerWefaxProgress, "function");
assert.equal(typeof window.restoreWefaxHistory, "function");
window.onServerWefaxProgress({ state: "Scanning 12 MHz" });
assert.equal(window.onServerWefaxProgress, undefined);
assert.equal(window.trxPluginRuntime.hasDecoder("wefax_progress"), true);
window.trxPluginRuntime.dispatch("wefax_progress", { state: "Scanning 12 MHz" });
assert.equal(status.textContent, "Scanning 12 MHz");
assert.equal(status.style.color, "var(--text-accent)");
});
@@ -25,10 +25,12 @@ test("WSPR entry forwards decoded locators at their absolute frequency", async (
Element: class Element {},
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
const source = await readFile(new URL("../../assets/web/generated/wspr.js", import.meta.url), "utf8");
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
window.onServerWspr({ message: "SP0ABC JO91 37", freq_hz: 1_420, ts_ms: Date.now() });
window.trxPluginRuntime.dispatch("wspr", { message: "SP0ABC JO91 37", freq_hz: 1_420, ts_ms: Date.now() });
assert.equal(forwarded.length, 1);
assert.equal(forwarded[0][1][0], "JO91");
assert.equal(forwarded[0][4].freq_hz, 14_097_020);