refactor: enforce typed frontend runtime boundaries
This commit is contained in:
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user