refactor: enforce typed frontend runtime boundaries

This commit is contained in:
sjg
2026-08-01 22:19:50 +02:00
parent d929ad3bde
commit 812359c744
4 changed files with 523 additions and 341 deletions
@@ -939,52 +939,6 @@ var runtime = {
};
window.trxPluginRuntime = runtime;
// src/plugin-loader.ts
var pluginGroups = {
"digital-modes": ["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/background-decode.js", "/sat.js", "/wefax.js"],
"map-data": ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js"],
map: ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js"],
statistics: ["/map-core.js"],
bookmarks: ["/bookmarks.js"],
recorder: [],
settings: ["/vchan.js", "/scheduler.js"]
};
var loaded = /* @__PURE__ */ new Set();
var loading = /* @__PURE__ */ new Map();
async function loadPlugin(path) {
if (loaded.has(path)) return;
const pending = loading.get(path);
if (pending) return pending;
const request = import(path).then(() => {
loaded.add(path);
loading.delete(path);
}).catch((error) => {
loading.delete(path);
throw new Error(`Failed to load plugin module: ${path}`, { cause: error });
});
loading.set(path, request);
return request;
}
async function loadPlugins(group) {
if (!(group in pluginGroups)) return;
for (const path of pluginGroups[group]) await loadPlugin(path);
}
function requestPlugins(group) {
void loadPlugins(group).catch((error) => {
console.error(error);
});
}
var loaderWindow = window;
loaderWindow.loadEagerPlugins = async () => {
await Promise.all(["digital-modes", "map-data", "bookmarks", "settings"].map(loadPlugins));
};
loaderWindow.loadPluginsForTab = loadPlugins;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const tab = event.target.closest("[data-tab]")?.dataset.tab;
if (tab) requestPlugins(tab);
});
// src/leaflet-ais-tracksymbol.ts
(function() {
const leaflet = globalThis.L;
@@ -1605,6 +1559,60 @@ function estimateNoiseFloorDb(bins) {
return nthElement(bins, Math.floor(bins.length * 0.15));
}
// src/plugin-loader.ts
var pluginGroups = {
"digital-modes": ["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/background-decode.js", "/sat.js", "/wefax.js"],
"map-data": ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js"],
map: ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js"],
statistics: ["/map-core.js"],
bookmarks: ["/bookmarks.js"],
recorder: [],
settings: ["/vchan.js", "/scheduler.js"]
};
var loaded = /* @__PURE__ */ new Set();
var loading = /* @__PURE__ */ new Map();
async function loadPlugin(path) {
if (loaded.has(path)) return;
const pending = loading.get(path);
if (pending) return pending;
const request = import(path).then(() => {
loaded.add(path);
loading.delete(path);
}).catch((error) => {
loading.delete(path);
throw new Error(`Failed to load plugin module: ${path}`, { cause: error });
});
loading.set(path, request);
return request;
}
async function loadPlugins(group) {
if (!(group in pluginGroups)) return;
for (const path of pluginGroups[group]) await loadPlugin(path);
}
async function loadEagerPlugins() {
await Promise.all(["digital-modes", "map-data", "bookmarks", "settings"].map(loadPlugins));
}
async function loadPluginsForTab(tab) {
await loadPlugins(tab);
}
// src/api/client.ts
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isRigSnapshot(value) {
if (!isRecord(value) || !isRecord(value.info) || !isRecord(value.status)) {
return false;
}
const { info, status } = value;
return typeof value.initialized === "boolean" && typeof info.manufacturer === "string" && typeof info.model === "string" && isRecord(status.freq) && typeof status.freq.hz === "number" && (typeof status.mode === "string" || isRecord(status.mode)) && typeof status.tx_en === "boolean";
}
function isRigListResponse(value) {
return isRecord(value) && (value.active_remote === null || typeof value.active_remote === "string") && Array.isArray(value.rigs) && value.rigs.every(
(rig) => isRecord(rig) && typeof rig.remote === "string" && typeof rig.manufacturer === "string" && typeof rig.model === "string" && Array.isArray(rig.supported_modes) && typeof rig.tx === "boolean" && typeof rig.filter_controls === "boolean" && typeof rig.initialized === "boolean"
);
}
// src/app.ts
function requiredElement(id) {
const element = document.getElementById(id);
@@ -1614,6 +1622,41 @@ function requiredElement(id) {
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function primitiveString(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
}
function isRecord2(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseJsonUnknown(source) {
return JSON.parse(source);
}
function messageEventData(event) {
const data = event.data;
if (typeof data !== "string") throw new TypeError("Expected a text event payload");
return data;
}
async function responseJsonUnknown(response) {
return await response.json();
}
function isAppUpdate(value) {
return isRecord2(value) && (value.status === void 0 || isRecord2(value.status));
}
function isAudioStreamInfo(value) {
return isRecord2(value) && typeof value.sample_rate === "number" && typeof value.channels === "number";
}
function isRecorderActiveList(value) {
return Array.isArray(value) && value.every((entry) => isRecord2(entry) && typeof entry.rig_id === "string" && typeof entry.path === "string" && typeof entry.started_at === "number");
}
function isRecorderFileList(value) {
return Array.isArray(value) && value.every((entry) => isRecord2(entry) && typeof entry.name === "string" && typeof entry.size === "number");
}
function isRdsData(value) {
return isRecord2(value);
}
function isVchanRdsEntry(value) {
return isRecord2(value) && typeof value.id === "string" && (value.rds === void 0 || value.rds === null || isRdsData(value.rds)) && (value.signal_db === void 0 || value.signal_db === null || typeof value.signal_db === "number");
}
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
var authRole = null;
var authEnabled = true;
@@ -1731,7 +1774,6 @@ function applyAuthRestrictions() {
const txLimitBtn2 = document.getElementById("tx-limit-btn");
const txAudioBtn2 = document.getElementById("tx-audio-btn");
const txLimitRow2 = document.getElementById("tx-limit-row");
const vfoPicker2 = document.getElementById("vfo-picker");
const jogUp = document.getElementById("jog-up");
const jogDown = document.getElementById("jog-down");
const jogButtons = document.querySelectorAll(".jog-step button");
@@ -1809,7 +1851,7 @@ function applyCapabilities(caps) {
if (txAudioBtn2) txAudioBtn2.style.display = caps.tx ? "" : "none";
if (txVolControl) txVolControl.style.display = caps.tx ? "" : "none";
if (!caps.tx && typeof stopTxAudio === "function" && txActive) {
stopTxAudio();
void stopTxAudio();
}
const txLimitRow2 = document.getElementById("tx-limit-row");
if (txLimitRow2) txLimitRow2.style.display = caps.tx_limit ? "" : "none";
@@ -2063,7 +2105,6 @@ function decodeHistoryMapRenderingDeferred() {
}
var lastSpectrumData = null;
window.lastSpectrumData = null;
var lastControl;
var lastTxEn = null;
var lastHasTx = true;
var lastRendered = null;
@@ -2659,7 +2700,7 @@ function applyRigList(activeRigId, rigIds, displayNames = {}) {
window.trx.modules.scheduler?.setRig(lastActiveRigId);
window.trx.modules.backgroundDecode?.setRig(lastActiveRigId);
window.trx.modules.bookmarks?.populateScopePicker();
window.trx.modules.bookmarks?.fetch(document.getElementById("bm-category-filter")?.value || "");
void window.trx.modules.bookmarks?.fetch(document.getElementById("bm-category-filter")?.value || "");
}
window.trx.modules.map?.updateMapRigFilter();
}
@@ -2667,8 +2708,9 @@ async function refreshRigList() {
try {
const resp = await fetch("/rigs", { cache: "no-store" });
if (!resp.ok) return;
const data = await resp.json();
const rigs = Array.isArray(data.rigs) ? data.rigs : [];
const data = await responseJsonUnknown(resp);
if (!isRigListResponse(data)) return;
const rigs = data.rigs;
const rigIds = rigs.map((r) => r.remote).filter(Boolean);
const displayNames = {};
rigs.forEach((r) => {
@@ -2684,7 +2726,7 @@ async function refreshRigList() {
});
serverRigs = rigs;
refreshOperatorLayoutCapabilities();
serverActiveRigId = data.active_remote || null;
serverActiveRigId = data.active_remote;
applyRigList(data.active_remote, rigIds, displayNames);
window.trx.modules.map?.syncAprsReceiverMarker();
} catch (e) {
@@ -2740,8 +2782,7 @@ var overviewWfTexPushCount = 0;
var overviewWfTexPalKey = "";
var overviewWfTexReady = false;
function cssColorToRgba(color, alphaMul = 1) {
const parser = typeof window.trxParseCssColor === "function" ? window.trxParseCssColor : null;
const parsed = parser ? parser(color) : [0, 0, 0, 1];
const parsed = typeof window.trxParseCssColor === "function" ? window.trxParseCssColor(color) : [0, 0, 0, 1];
return [
parsed[0] ?? 0,
parsed[1] ?? 0,
@@ -2769,7 +2810,7 @@ function overviewWfResetTextureCache() {
overviewWfTexReady = false;
}
function overviewWfPaletteKey(pal, viewKey = "") {
return `${pal.waterfallHue}|${pal.waterfallSat}|${pal.waterfallLight}|${pal.waterfallAlpha}|${spectrumFloor}|${spectrumRange}|${waterfallGamma}|${viewKey}`;
return `${pal.waterfallHue.join(",")}|${pal.waterfallSat}|${pal.waterfallLight.join(",")}|${pal.waterfallAlpha.join(",")}|${spectrumFloor}|${spectrumRange}|${waterfallGamma}|${viewKey}`;
}
function resizeHeaderSignalCanvas() {
if (!ensureOverviewCanvasBackingStore()) return;
@@ -2837,7 +2878,7 @@ function drawSignalOverlay() {
const bwStroke = BW_OVERLAY_COLORS.stroke;
const bwHard = BW_OVERLAY_COLORS.hard;
const bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
if (Array.isArray(bmRef) && bmRef.length > 0) {
if (bmRef && bmRef.length > 0) {
const colorMap = bmCategoryColorMap();
const grouped = /* @__PURE__ */ new Map();
for (const bm of bmRef) {
@@ -2847,8 +2888,9 @@ function drawSignalOverlay() {
const x = hzToX(f);
if (!isFiniteNumber(x) || x < 0 || x > W) continue;
const color = colorMap[bm?.category || ""] || BOOKMARK_MARKER_FALLBACK;
if (!grouped.has(color)) grouped.set(color, []);
grouped.get(color).push(x, 0, x, H);
const segments = grouped.get(color) ?? [];
segments.push(x, 0, x, H);
grouped.set(color, segments);
}
for (const [color, segments] of grouped.entries()) {
if (!Array.isArray(segments) || segments.length === 0) continue;
@@ -3138,7 +3180,7 @@ function waterfallColorRgba(db, pal, minDb, maxDb) {
var _wfLutKey = "";
var _wfLut = new Uint8Array(256 * 4);
function ensureWaterfallLut(pal, minDb, maxDb) {
const key = `${pal.waterfallHue}|${pal.waterfallSat}|${pal.waterfallLight}|${pal.waterfallAlpha}|${minDb}|${maxDb}|${waterfallGamma}`;
const key = `${pal.waterfallHue.join(",")}|${pal.waterfallSat}|${pal.waterfallLight.join(",")}|${pal.waterfallAlpha.join(",")}|${minDb}|${maxDb}|${waterfallGamma}`;
if (key === _wfLutKey) return;
_wfLutKey = key;
for (let i = 0; i < 256; i++) {
@@ -3256,7 +3298,7 @@ function renderRdsOverlays() {
el.innerHTML = html;
el.addEventListener("click", (evt) => {
evt.stopPropagation();
copyRdsPsToClipboard(entry.rds, entry.freq_hz);
void copyRdsPsToClipboard(entry.rds, entry.freq_hz);
});
el.addEventListener("mouseenter", () => {
el.style.zIndex = String(entries.length + 10);
@@ -3915,7 +3957,7 @@ function updateJogStepSupport(cap) {
}
function normalizeMode(modeVal) {
if (typeof modeVal === "string") return modeVal;
if (modeVal && typeof modeVal === "object") {
if (isRecord2(modeVal)) {
const entries = Object.entries(modeVal);
if (entries.length > 0) {
const firstEntry = entries[0];
@@ -4436,7 +4478,7 @@ function render(update) {
}
lastModeName = modeUpper2;
if (lastSpectrumData && !update.filter) {
applyBwDefaultForMode(mode, false);
void applyBwDefaultForMode(mode, false);
}
}
updateWfmControls();
@@ -4590,7 +4632,6 @@ function render(update) {
powerBtn.setAttribute("aria-pressed", "false");
powerHint.textContent = "State unknown";
}
lastControl = update.enabled;
if (update.status && update.status.tx && typeof update.status.tx.limit === "number") {
txLimitInput.value = String(update.status.tx.limit);
txLimitRow.style.display = "";
@@ -4725,9 +4766,10 @@ async function pollFreshSnapshot() {
const statusUrl = lastActiveRigId ? `/status?remote=${encodeURIComponent(lastActiveRigId)}` : "/status";
const resp = await fetch(statusUrl, { cache: "no-store" });
if (!resp.ok) return;
const data = await resp.json();
const data = await responseJsonUnknown(resp);
if (!isRigSnapshot(data)) return;
render(data);
refreshRigList();
void refreshRigList();
lastEventAt = Date.now();
} catch (e) {
}
@@ -4742,7 +4784,7 @@ function connect() {
}
stopMeterStreaming();
startMeterStreaming();
pollFreshSnapshot();
void pollFreshSnapshot();
const eventsUrl = lastActiveRigId ? `/events?remote=${encodeURIComponent(lastActiveRigId)}` : "/events";
es = new EventSource(eventsUrl);
const source = es;
@@ -4751,13 +4793,14 @@ function connect() {
setConnLostOverlay(false);
if (tabMainEl) tabMainEl.classList.remove("server-disconnected");
if (!aboutUptimeStart) aboutUptimeStart = Date.now();
pollFreshSnapshot();
refreshRigList();
void pollFreshSnapshot();
void refreshRigList();
};
source.onmessage = (evt) => {
try {
if (evt.data === lastRendered) return;
const data = JSON.parse(evt.data);
const data = parseJsonUnknown(evt.data);
if (!isAppUpdate(data)) throw new TypeError("Unexpected status event shape");
lastRendered = evt.data;
render(data);
lastEventAt = Date.now();
@@ -4777,21 +4820,22 @@ function connect() {
});
source.addEventListener("session", (evt) => {
try {
const d = JSON.parse(evt.data);
sseSessionId = d.session_id || null;
const eventData = messageEventData(evt);
const d = parseJsonUnknown(eventData);
sseSessionId = isRecord2(d) && typeof d.session_id === "string" ? d.session_id : null;
} catch (_) {
}
window.trx.modules.vchan?.handleSession(evt.data);
window.trx.modules.vchan?.handleSession(messageEventData(evt));
});
source.addEventListener("channels", (evt) => {
window.trx.modules.vchan?.handleChannels(evt.data);
window.trx.modules.vchan?.handleChannels(messageEventData(evt));
});
source.onerror = () => {
if (source.readyState === EventSource.CLOSED) {
powerHint.textContent = "trx-client connection lost, retrying…";
setConnLostOverlay(true, "trx-client connection lost", "Retrying…", true);
source.close();
pollFreshSnapshot();
void pollFreshSnapshot();
scheduleReconnect(1e3);
}
};
@@ -4801,7 +4845,7 @@ function connect() {
powerHint.textContent = "trx-client connection lost, retrying…";
setConnLostOverlay(true, "trx-client connection lost", "Retrying…", true);
source.close();
pollFreshSnapshot();
void pollFreshSnapshot();
scheduleReconnect(250);
}
}, 5e3);
@@ -4915,7 +4959,7 @@ async function switchRigFromSelect(selectEl) {
window.trxUi?.setActiveRig(lastActiveRigId);
window.trx.modules.scheduler?.setRig(lastActiveRigId);
window.trx.modules.backgroundDecode?.setRig(lastActiveRigId);
window.trx.modules.bookmarks?.fetch(document.getElementById("bm-category-filter")?.value || "");
void window.trx.modules.bookmarks?.fetch(document.getElementById("bm-category-filter")?.value || "");
window.trx.modules.map?.syncAprsReceiverMarker();
connect();
stopSpectrumStreaming();
@@ -4940,7 +4984,7 @@ async function switchRigFromSelect(selectEl) {
}
if (headerRigSwitchSelect) {
headerRigSwitchSelect.addEventListener("change", () => {
switchRigFromSelect(headerRigSwitchSelect);
void switchRigFromSelect(headerRigSwitchSelect);
});
}
function setControlPending(control, pending) {
@@ -5037,7 +5081,7 @@ if (centerFreqEl) {
centerFreqDirty = true;
if (e.key === "Enter") {
e.preventDefault();
applyCenterFreqFromInput();
void applyCenterFreqFromInput();
} else if (e.key === "Escape") {
centerFreqDirty = false;
refreshCenterFreqDisplay();
@@ -5382,18 +5426,18 @@ if (spectrumBwInput) {
spectrumBwInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
applyBandwidthFromInput();
void applyBandwidthFromInput();
}
});
}
if (spectrumBwSetBtn) {
spectrumBwSetBtn.addEventListener("click", () => {
applyBandwidthFromInput();
void applyBandwidthFromInput();
});
}
if (spectrumBwAutoBtn) {
spectrumBwAutoBtn.addEventListener("click", () => {
applyAutoBandwidth();
void applyAutoBandwidth();
});
}
if (spectrumBwSweetBtn) {
@@ -5466,7 +5510,9 @@ function navigateToTab(name, options = {}) {
updateTabHistory(name, replaceHistory);
}
scheduleSpectrumLayout();
if (typeof window.loadPluginsForTab === "function") window.loadPluginsForTab(name);
void loadPluginsForTab(name).catch((error) => {
console.error(error);
});
if (name === "map") {
_initMapWhenReady();
}
@@ -5474,7 +5520,7 @@ function navigateToTab(name, options = {}) {
window.trx.modules.map?.scheduleStatsRender();
}
if (name === "recorder") {
refreshRecorderStatus();
void refreshRecorderStatus();
}
}
window.navigateToTab = navigateToTab;
@@ -5604,7 +5650,7 @@ requiredElement("auth-form").addEventListener("submit", async (e) => {
});
var guestBtn = document.getElementById("auth-guest-btn");
if (guestBtn) {
guestBtn.addEventListener("click", async () => {
guestBtn.addEventListener("click", () => {
authRole = "rx";
requiredElement("auth-passphrase").value = "";
hideAuthGate();
@@ -5630,7 +5676,7 @@ if (headerAuthBtn) {
});
}
var trxState = /* @__PURE__ */ Object.create(null);
var trxModules = /* @__PURE__ */ Object.create(null);
var trxModules = {};
Object.defineProperties(trxState, {
serverLat: { get() {
return serverLat;
@@ -5804,8 +5850,10 @@ Object.defineProperties(trxState, {
} }
});
window.trx = Object.freeze({ state: trxState, core: trxCore, modules: trxModules });
if (typeof window.loadEagerPlugins === "function") window.loadEagerPlugins();
initializeApp();
void loadEagerPlugins().catch((error) => {
console.error(error);
});
void initializeApp();
window.addEventListener("resize", resizeHeaderSignalCanvas);
function bookmarkDistanceText(bm) {
if (!bm || serverLat == null || serverLon == null) return null;
@@ -5830,7 +5878,7 @@ function buildBookmarkTooltipText(bm) {
}
function nearestBookmarkForHz(hz, widthPx, range) {
const ref = window.trx.modules.bookmarks?.overlayList ?? null;
if (!Array.isArray(ref) || !isFiniteNumber(hz) || !widthPx || !range || !isFiniteNumber(range.visSpanHz) || range.visSpanHz <= 0) {
if (!ref || !isFiniteNumber(hz) || !widthPx || !range || !isFiniteNumber(range.visSpanHz) || range.visSpanHz <= 0) {
return null;
}
const maxDeltaHz = Math.max(range.visSpanHz / widthPx * 6, 10);
@@ -5951,7 +5999,6 @@ var wfmDeemphasisEl = document.getElementById("wfm-deemphasis");
var wfmAudioModeEl = document.getElementById("wfm-audio-mode");
var wfmDenoiseEl = document.getElementById("wfm-denoise");
var sdrSettingsRowEl = document.getElementById("sdr-settings-row");
var sdrGainControlsEl = document.getElementById("sdr-gain-controls");
var sdrGainEl = document.getElementById("sdr-gain-db");
var sdrGainSetBtn = document.getElementById("sdr-gain-set");
var sdrLnaGainControlsEl = document.getElementById("sdr-lna-gain-controls");
@@ -6056,7 +6103,7 @@ function levelFromChannels(channels, frameCount) {
return Math.min(100, rms * 220);
}
function normalizeWfmDenoiseLevel(value) {
const next = String(value ?? "").toLowerCase();
const next = primitiveString(value).toLowerCase();
if (next === "off" || next === "auto" || next === "low" || next === "medium" || next === "high") return next;
return "auto";
}
@@ -6282,7 +6329,7 @@ function resetTxTimeout() {
if (txTimeoutTimer) clearTimeout(txTimeoutTimer);
txTimeoutTimer = setTimeout(() => {
console.warn("PTT safety timeout — stopping TX");
stopTxAudio();
void stopTxAudio();
}, TX_TIMEOUT_SECS * 1e3);
}
function startTxTimeoutCountdown() {
@@ -6441,7 +6488,9 @@ function startRxAudio() {
audioWs.onmessage = (evt) => {
if (typeof evt.data === "string") {
try {
configureRxStream(JSON.parse(evt.data));
const info = parseJsonUnknown(evt.data);
if (!isAudioStreamInfo(info)) throw new TypeError("Unexpected audio stream metadata");
configureRxStream(info);
} catch (e) {
console.error("Audio stream info parse error", e);
}
@@ -6518,7 +6567,7 @@ function startRxAudio() {
};
audioWs.onclose = () => {
if (txActive) {
stopTxAudio();
void stopTxAudio();
}
rxActive = false;
window.trxUi?.setButtonState(rxAudioBtn, { active: false, activeLabel: "Stop Audio", inactiveLabel: "Play Audio" });
@@ -6559,7 +6608,7 @@ function stopRxAudio() {
audioWs = null;
}
if (audioCtx) {
audioCtx.close();
void audioCtx.close();
audioCtx = null;
}
updateWfmControls();
@@ -6587,7 +6636,7 @@ function stopRxAudio() {
}
function startTxAudio() {
if (txActive) {
stopTxAudio();
void stopTxAudio();
return;
}
if (!hasWebCodecs) {
@@ -6780,12 +6829,15 @@ async function refreshRecorderStatus() {
fetch("/api/recorder/files")
]);
if (statusResp.ok) {
const active = await statusResp.json();
renderRecorderActive(active);
const active = await responseJsonUnknown(statusResp);
if (isRecorderActiveList(active)) renderRecorderActive(active);
}
if (filesResp.ok) {
_recorderFiles = await filesResp.json();
renderRecorderFiles();
const files = await responseJsonUnknown(filesResp);
if (isRecorderFileList(files)) {
_recorderFiles = files;
renderRecorderFiles();
}
}
} catch (e) {
console.error("Recorder status fetch failed", e);
@@ -6923,10 +6975,8 @@ function renderRecorderFiles() {
row.parentNode?.insertBefore(playerRow, row.nextSibling);
btn.setAttribute("aria-expanded", "true");
btn.textContent = "Hide";
try {
audio.play();
} catch (_) {
}
void audio.play().catch(() => {
});
});
});
el.querySelectorAll(".rec-delete-btn").forEach(function(btn) {
@@ -7011,7 +7061,6 @@ if (sdrSquelchEl) {
}
requiredElement("copyright-year").textContent = String((/* @__PURE__ */ new Date()).getFullYear());
var decodeSource = null;
var decodeConnected = false;
var decodeHistoryWorker = null;
function setModeBoundDecodeStatus(el, activeModes, inactiveText, connectedText) {
if (!el) return;
@@ -7215,9 +7264,10 @@ function connectDecode() {
decodeHistoryWorker = worker;
worker.onmessage = (evt) => {
if (historySettled || worker !== decodeHistoryWorker) return;
const data = evt?.data || {};
const data = evt.data;
if (!isRecord2(data) || typeof data.type !== "string") return;
if (data.type === "status") {
const phase = String(data.phase || "");
const phase = primitiveString(data.phase);
if (phase === "fetching") {
setDecodeHistoryOverlayVisible(true, "Loading decode history…", "Fetching recent decodes from the client buffer");
} else if (phase === "decoding") {
@@ -7235,7 +7285,8 @@ function connectDecode() {
return;
}
if (data.type === "group") {
enqueueDecodeHistoryGroup(String(data.kind || ""), data.messages);
const messages = Array.isArray(data.messages) ? data.messages.filter((message) => isRecord2(message) && typeof message.type === "string") : [];
enqueueDecodeHistoryGroup(typeof data.kind === "string" ? data.kind : "", messages);
return;
}
if (data.type === "done") {
@@ -7246,7 +7297,7 @@ function connectDecode() {
return;
}
if (data.type === "error") {
console.error("Decode history worker failed", data.message || "unknown worker failure");
console.error("Decode history worker failed", typeof data.message === "string" ? data.message : "unknown worker failure");
terminateDecodeHistoryWorker();
startDecodeHistoryFallback();
}
@@ -7268,21 +7319,21 @@ function connectDecode() {
decodeSource = new EventSource("/decode");
const source = decodeSource;
source.onopen = () => {
decodeConnected = true;
updateDecodeStatus("Connected, listening for packets");
};
source.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data);
if (historySettled) dispatchDecodeMessage(msg);
else liveBuffer.push(msg);
const msg = parseJsonUnknown(evt.data);
if (!isRecord2(msg) || typeof msg.type !== "string") return;
const decoded = { ...msg, type: msg.type };
if (historySettled) dispatchDecodeMessage(decoded);
else liveBuffer.push(decoded);
} catch (e) {
}
};
source.onerror = () => {
const wasClosed = source.readyState === 2;
source.close();
decodeConnected = false;
terminateDecodeHistoryWorker();
if (!historySettled) flushLiveBuffer();
if (wasClosed) {
@@ -7612,11 +7663,12 @@ function startSpectrumStreaming() {
};
source.addEventListener("b", (evt) => {
try {
const commaA = evt.data.indexOf(",");
const commaB = evt.data.indexOf(",", commaA + 1);
const centerHz = Number(evt.data.slice(0, commaA));
const sampleRate = Number(evt.data.slice(commaA + 1, commaB));
const b64 = evt.data.slice(commaB + 1);
const eventData = messageEventData(evt);
const commaA = eventData.indexOf(",");
const commaB = eventData.indexOf(",", commaA + 1);
const centerHz = Number(eventData.slice(0, commaA));
const sampleRate = Number(eventData.slice(commaA + 1, commaB));
const b64 = eventData.slice(commaB + 1);
const hadSpectrum = !!lastSpectrumData;
const bins = decodeBase64Int8(b64);
const rds = lastSpectrumData?.rds;
@@ -7652,7 +7704,9 @@ function startSpectrumStreaming() {
});
source.addEventListener("rds", (evt) => {
try {
const rds = evt.data === "null" ? void 0 : JSON.parse(evt.data);
const eventData = messageEventData(evt);
const value = eventData === "null" ? void 0 : parseJsonUnknown(eventData);
const rds = value !== void 0 && isRdsData(value) ? value : null;
if (lastSpectrumData) lastSpectrumData.rds = rds;
updateRdsPsOverlay(rds ?? null);
} catch (_) {
@@ -7660,22 +7714,20 @@ function startSpectrumStreaming() {
});
source.addEventListener("rds_vchan", (evt) => {
try {
const payload = evt.data === "null" ? [] : JSON.parse(evt.data);
const eventData = messageEventData(evt);
const value = eventData === "null" ? [] : parseJsonUnknown(eventData);
const payload = Array.isArray(value) ? value.filter(isVchanRdsEntry) : [];
const next = /* @__PURE__ */ new Map();
const nextSig = /* @__PURE__ */ new Map();
if (Array.isArray(payload)) {
payload.forEach((entry) => {
if (entry && entry.id) {
next.set(entry.id, entry.rds ?? null);
if (typeof entry.signal_db === "number") nextSig.set(entry.id, entry.signal_db);
}
});
}
payload.forEach((entry) => {
next.set(entry.id, entry.rds ?? null);
if (typeof entry.signal_db === "number") nextSig.set(entry.id, entry.signal_db);
});
vchanRdsById = next;
vchanSignalDbById = nextSig;
const virtualChannelId = window.trx.modules.vchan?.activeId;
if (virtualChannelId && nextSig.has(virtualChannelId)) {
sigLastDbm = nextSig.get(virtualChannelId);
sigLastDbm = nextSig.get(virtualChannelId) ?? null;
refreshSigStrengthDisplay();
}
updateRdsPsOverlay(primaryRds);
@@ -7758,8 +7810,8 @@ function startMeterStreaming() {
meterSource = new EventSource(url);
meterSource.onmessage = (evt) => {
try {
const { sig } = JSON.parse(evt.data);
applyMeterSample(sig);
const value = parseJsonUnknown(evt.data);
if (isRecord2(value) && typeof value.sig === "number") applyMeterSample(value.sig);
} catch (_) {
}
};
@@ -7794,10 +7846,10 @@ function clearSpectrumCanvas() {
}
}
function formatOverlayPs(ps) {
return String(ps ?? "").slice(0, 8).padEnd(8, "_").replaceAll(" ", "_");
return primitiveString(ps).slice(0, 8).padEnd(8, "_").replaceAll(" ", "_");
}
function formatPsHtml(ps) {
const clipped = String(ps ?? "").slice(0, 8);
const clipped = primitiveString(ps).slice(0, 8);
let html = "";
for (let i = 0; i < 8; i += 1) {
const ch = clipped[i];
@@ -7813,8 +7865,10 @@ function formatOverlayPi(pi) {
return pi != null ? `PI 0x${Number(pi).toString(16).toUpperCase().padStart(4, "0")}` : "PI --";
}
function formatOverlayPty(pty, ptyName) {
if (ptyName) return ptyName;
return pty != null ? String(pty) : "--";
const name = primitiveString(ptyName);
if (name) return name;
const code = primitiveString(pty);
return code || "--";
}
function overlayTrafficFlagHtml(label, active) {
const stateClass = active === true ? "rds-flag-active" : "rds-flag-inactive";
@@ -7913,13 +7967,13 @@ async function copyRdsRawToClipboard() {
var rdsPsValueEl = document.getElementById("rds-ps");
if (rdsPsValueEl) {
rdsPsValueEl.addEventListener("click", () => {
copyRdsPsToClipboard();
void copyRdsPsToClipboard();
});
}
var rdsRawCopyBtn = document.getElementById("rds-raw-copy-btn");
if (rdsRawCopyBtn) {
rdsRawCopyBtn.addEventListener("click", () => {
copyRdsRawToClipboard();
void copyRdsRawToClipboard();
});
}
var rdsAfListEl = document.getElementById("rds-af-list");
@@ -8202,7 +8256,7 @@ function drawSpectrumWaterfall() {
const maxDb = minDb + Math.max(20, isFiniteNumber(spectrumRange) ? spectrumRange : 90);
const view = spectrumVisibleRange(lastSpectrumData);
const viewKey = `${Math.round(view.visLoHz)}:${Math.round(view.visHiHz)}`;
const palKey = `swf|${pal.waterfallHue}|${pal.waterfallSat}|${pal.waterfallLight}|${pal.waterfallAlpha}|${spectrumFloor}|${spectrumRange}|${waterfallGamma}|${viewKey}`;
const palKey = `swf|${pal.waterfallHue.join(",")}|${pal.waterfallSat}|${pal.waterfallLight.join(",")}|${pal.waterfallAlpha.join(",")}|${spectrumFloor}|${spectrumRange}|${waterfallGamma}|${viewKey}`;
const rowStride = iW * 4;
const expectedSize = iW * iH * 4;
const newPushes = spectrumWfPushCount - spectrumWfTexPushCount;
@@ -8329,7 +8383,6 @@ function bmCategoryColorMap() {
function createBookmarkChip(bm, colorMap, options = {}) {
const span = document.createElement("span");
const freqStr = window.trx.modules.bookmarks ? window.trx.modules.bookmarks.formatFrequency(bm.freq_hz) : bm.freq_hz + "Hz";
const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
span.className = "spectrum-bookmark-chip";
if (options.sideStack) {
span.classList.add("spectrum-bookmark-chip-side");
@@ -8374,8 +8427,7 @@ function updateBookmarkAxis(range) {
const leftSideEl = document.getElementById("spectrum-bookmark-side-left");
const rightSideEl = document.getElementById("spectrum-bookmark-side-right");
if (!axisEl) return;
const _bmRef = window.trx.modules.bookmarks?.overlayList ?? null;
const allBookmarks = Array.isArray(_bmRef) ? _bmRef : [];
const allBookmarks = window.trx.modules.bookmarks?.overlayList ?? [];
const visBookmarks = allBookmarks.filter((bm) => bm.freq_hz >= range.visLoHz && bm.freq_hz <= range.visHiHz);
const leftBookmarks = allBookmarks.filter((bm) => bm.freq_hz < range.visLoHz).sort((a, b) => b.freq_hz - a.freq_hz).slice(0, 3);
const rightBookmarks = allBookmarks.filter((bm) => bm.freq_hz > range.visHiHz).sort((a, b) => a.freq_hz - b.freq_hz).slice(0, 3);
@@ -9106,11 +9158,12 @@ var bandplanStripEl = document.getElementById("spectrum-bandplan-strip");
var bandplanRegionSelect = document.getElementById("bandplan-region-select");
var bandplanLabelsCheck = document.getElementById("bandplan-labels-check");
(function loadBandplanJson() {
fetch("/bandplan.json").then((r) => {
if (!r.ok) throw new Error(String(r.status));
return r.json();
}).then((d) => {
bandplanData = d;
fetch("/bandplan.json").then(async (response) => {
if (!response.ok) throw new Error(String(response.status));
return await responseJsonUnknown(response);
}).then((data) => {
if (!isRecord2(data)) return;
bandplanData = data;
bandplanSegmentsCache = null;
bandplanCacheKey = "";
}).catch(() => {