refactor: convert background decode controls to TypeScript

This commit is contained in:
sjg
2026-08-01 12:36:27 +02:00
parent 7251ec276d
commit ec59908e0d
6 changed files with 531 additions and 423 deletions
@@ -1,355 +1,365 @@
"use strict"; "use strict";
(function() { (() => {
"use strict"; // src/plugins/background-decode.ts
function bgdSupportedIds() { var bgdWindow = window;
return (window.decoderRegistry || []).filter(function(d) { (function() {
return d.background_decode; "use strict";
}).map(function(d) { function bgdSupportedIds() {
return d.id; return (bgdWindow.decoderRegistry || []).filter(function(d) {
}); return d.background_decode;
} }).map(function(d) {
let backgroundDecodeRole = null; return d.id;
let currentRigId = null;
let currentConfig = null;
let bookmarkList = [];
let statusInterval = null;
let bgdDirty = false;
function initBackgroundDecode(rigId, role) {
backgroundDecodeRole = role;
currentRigId = rigId || null;
if (currentRigId) loadBackgroundDecode();
startStatusPolling();
}
function setBackgroundDecodeRig(rigId) {
const nextRigId = rigId || null;
if (nextRigId === currentRigId) return;
currentRigId = nextRigId;
if (!currentRigId) return;
loadBackgroundDecode();
}
function apiGetConfig(rigId) {
return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function apiPutConfig(rigId, config) {
return fetch("/background-decode/" + encodeURIComponent(rigId), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config)
}).then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function apiResetConfig(rigId) {
return fetch("/background-decode/" + encodeURIComponent(rigId), {
method: "DELETE"
}).then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function apiGetStatus(rigId) {
return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function apiGetBookmarks() {
return fetch("/bookmarks").then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function loadBackgroundDecode() {
const rigId = currentRigId;
if (!rigId) return;
Promise.all([apiGetConfig(rigId), apiGetBookmarks()]).then(function([config, bookmarks]) {
currentConfig = config || { remote: rigId, enabled: false, bookmark_ids: [] };
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
}).catch(function(err) {
console.error("background decode load failed", err);
});
}
function supportedBookmarks() {
return bookmarkList.filter(function(bookmark) {
return bookmarkDecoderKinds(bookmark).length > 0;
});
}
function bookmarkDecoderKinds(bookmark) {
var ids = bgdSupportedIds();
var decoders = Array.isArray(bookmark && bookmark.decoders) ? bookmark.decoders : [];
var explicit = decoders.map(function(item) {
return String(item || "").trim().toLowerCase();
}).filter(function(item, index, arr) {
return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
});
if (explicit.length > 0) return explicit;
var mode = String(bookmark && bookmark.mode || "").trim().toUpperCase();
return (window.decoderRegistry || []).filter(function(d) {
return d.activation === "mode_bound" && d.background_decode && d.active_modes.indexOf(mode) >= 0;
}).map(function(d) {
return d.id;
});
}
function renderBackgroundDecode() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
setCheckbox("background-decode-enabled", !!currentConfig.enabled);
renderBookmarkChecklist();
const isControl = backgroundDecodeRole === "control" || typeof authEnabled !== "undefined" && !authEnabled;
const panel = document.getElementById("background-decode-panel");
if (panel) {
panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) {
el.disabled = !isControl;
}); });
} }
const saveBtn = document.getElementById("background-decode-save-btn"); let backgroundDecodeRole = null;
const resetBtn = document.getElementById("background-decode-reset-btn"); let currentRigId = null;
if (saveBtn) saveBtn.style.display = isControl ? "" : "none"; let currentConfig = null;
if (resetBtn) resetBtn.style.display = isControl ? "" : "none"; let bookmarkList = [];
} let statusInterval = null;
function renderBookmarkChecklist(filterText) { let bgdDirty = false;
const container = document.getElementById("bgd-bookmark-checklist"); function initBackgroundDecode(rigId, role) {
if (!container) return; backgroundDecodeRole = role;
container.innerHTML = ""; currentRigId = rigId || null;
const selectedIds = new Set( if (currentRigId) loadBackgroundDecode();
currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : [] startStatusPolling();
);
const all = supportedBookmarks();
const filter = (filterText || "").trim().toLowerCase();
const filtered = filter ? all.filter(function(bm) {
var text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
return text.indexOf(filter) >= 0;
}) : all;
if (filtered.length === 0) {
container.innerHTML = '<div class="bgd-checklist-empty">' + (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") + "</div>";
return;
} }
filtered.forEach(function(bookmark) { function setBackgroundDecodeRig(rigId) {
var row = document.createElement("label"); const nextRigId = rigId || null;
row.className = "bgd-checklist-row"; if (nextRigId === currentRigId) return;
var decoders = bookmarkDecoderKinds(bookmark); currentRigId = nextRigId;
var checked = selectedIds.has(bookmark.id) ? " checked" : ""; if (!currentRigId) return;
row.innerHTML = '<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" /><span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span><span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + "</span>"; loadBackgroundDecode();
row.querySelector("input").addEventListener("change", function(e) {
onChecklistToggle(bookmark.id, e.target.checked);
});
container.appendChild(row);
});
}
function onChecklistToggle(bookmarkId, checked) {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = []; function apiGetConfig(rigId) {
if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) { return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function(r) {
currentConfig.bookmark_ids.push(bookmarkId); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
} else if (!checked) { return r.json();
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function(id) {
return id !== bookmarkId;
}); });
} }
markBgdDirty(); function apiPutConfig(rigId, config) {
} return fetch("/background-decode/" + encodeURIComponent(rigId), {
function saveBackgroundDecode() { method: "PUT",
const rigId = currentRigId; headers: { "Content-Type": "application/json" },
if (!rigId) return; body: JSON.stringify(config)
const payload = { }).then(function(r) {
remote: rigId, if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
enabled: !!document.getElementById("background-decode-enabled").checked, return r.json();
bookmark_ids: Array.isArray(currentConfig && currentConfig.bookmark_ids) ? currentConfig.bookmark_ids.slice() : [] });
};
const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.disabled = true;
apiPutConfig(rigId, payload).then(function(saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode saved.");
}).catch(function(err) {
showToast("Save failed: " + err.message, true);
}).finally(function() {
if (btn) btn.disabled = false;
});
}
async function resetBackgroundDecode() {
const rigId = currentRigId;
if (!rigId) return;
if (!await window.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
apiResetConfig(rigId).then(function(saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode reset.");
}).catch(function(err) {
showToast("Reset failed: " + err.message, true);
});
}
function startStatusPolling() {
if (statusInterval) clearInterval(statusInterval);
statusInterval = setInterval(pollBackgroundDecodeStatus, 15e3);
}
function pollBackgroundDecodeStatus() {
const rigId = currentRigId;
if (!rigId) return;
apiGetStatus(rigId).then(renderStatus).catch(function() {
});
}
function renderStatus(status) {
const card = document.getElementById("background-decode-status-card");
if (!card) return;
const entries = Array.isArray(status && status.entries) ? status.entries : [];
if (!entries.length) {
card.textContent = "No background decode bookmarks configured.";
return;
} }
const summary = []; function apiResetConfig(rigId) {
if (status.active_rig) { return fetch("/background-decode/" + encodeURIComponent(rigId), {
if (Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz)); method: "DELETE"
if (Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2)); }).then(function(r) {
} else { if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
summary.push("This rig is not currently selected for audio."); return r.json();
});
} }
let html = summary.length ? '<div style="margin-bottom:0.8rem;color:var(--text-muted);">' + escHtml(summary.join(" · ")) + "</div>" : ""; function apiGetStatus(rigId) {
html += '<div class="bgd-status-list">'; return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function(r) {
entries.forEach(function(entry) { if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark"; return r.json();
const parts = []; });
if (Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz)); }
if (entry.mode) parts.push(entry.mode); function apiGetBookmarks() {
if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) { return fetch("/bookmarks").then(function(r) {
parts.push(entry.decoder_kinds.join("/").toUpperCase()); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json();
});
}
function loadBackgroundDecode() {
const rigId = currentRigId;
if (!rigId) return;
Promise.all([apiGetConfig(rigId), apiGetBookmarks()]).then(function([config, bookmarks]) {
currentConfig = config;
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
}).catch(function(err) {
console.error("background decode load failed", err);
});
}
function supportedBookmarks() {
return bookmarkList.filter(function(bookmark) {
return bookmarkDecoderKinds(bookmark).length > 0;
});
}
function bookmarkDecoderKinds(bookmark) {
const ids = bgdSupportedIds();
const decoders = bookmark.decoders ?? [];
const explicit = decoders.map(function(item) {
return item.trim().toLowerCase();
}).filter(function(item, index, arr) {
return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
});
if (explicit.length > 0) return explicit;
const mode = bookmark.mode.trim().toUpperCase();
return (bgdWindow.decoderRegistry || []).filter(function(d) {
return d.activation === "mode_bound" && d.background_decode && d.active_modes.indexOf(mode) >= 0;
}).map(function(d) {
return d.id;
});
}
function renderBackgroundDecode() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
html += '<div class="bgd-status-row"><div><div class="bgd-status-name">' + escHtml(name) + '</div><div class="bgd-status-meta">' + escHtml(parts.join(" · ")) + '</div></div><div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '"><svg class="bgd-state-dot" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3.5"/></svg>' + escHtml(prettyState(entry.state)) + "</div></div>"; setCheckbox("background-decode-enabled", currentConfig.enabled);
}); renderBookmarkChecklist();
html += "</div>"; const isControl = backgroundDecodeRole === "control" || bgdWindow.authEnabled === false;
card.innerHTML = html; const panel = document.getElementById("background-decode-panel");
} if (panel) {
function prettyState(state) { panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) {
switch (state) { el.disabled = !isControl;
case "active": });
return "✓ Active"; }
case "out_of_span": const saveBtn = document.getElementById("background-decode-save-btn");
return "△ Out of span"; const resetBtn = document.getElementById("background-decode-reset-btn");
case "waiting_for_spectrum": if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
return "△ Waiting"; if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
case "waiting_for_user":
return "△ No user";
case "missing_bookmark":
return "✗ Missing";
case "no_supported_decoders":
return "✗ Unsupported";
case "disabled":
return "△ Disabled";
case "handled_by_scheduler":
return "△ Scheduler";
case "scheduler_has_control":
return "△ Scheduler";
case "handled_by_virtual_channel":
return "△ VChan";
default:
return "△ Inactive";
} }
} function renderBookmarkChecklist(filterText = "") {
function setCheckbox(id, value) { const container = document.getElementById("bgd-bookmark-checklist");
const el = document.getElementById(id); if (!container) return;
if (el) el.checked = !!value; container.innerHTML = "";
} const selectedIds = new Set(
function formatFreq(hz) { currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
if (!Number.isFinite(hz) || hz <= 0) return "--"; );
if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz"; const all = supportedBookmarks();
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz"; const filter = (filterText || "").trim().toLowerCase();
return hz + " Hz"; const filtered = filter ? all.filter(function(bm) {
} const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
function escHtml(value) { return text.indexOf(filter) >= 0;
return String(value == null ? "" : value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }) : all;
} if (filtered.length === 0) {
function markBgdDirty() { container.innerHTML = '<div class="bgd-checklist-empty">' + (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") + "</div>";
if (bgdDirty) return; return;
bgdDirty = true; }
var btn = document.getElementById("background-decode-save-btn"); filtered.forEach(function(bookmark) {
if (btn) btn.classList.add("sch-dirty"); const row = document.createElement("label");
} row.className = "bgd-checklist-row";
function clearBgdDirty() { const decoders = bookmarkDecoderKinds(bookmark);
bgdDirty = false; const checked = selectedIds.has(bookmark.id) ? " checked" : "";
var btn = document.getElementById("background-decode-save-btn"); row.innerHTML = '<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" /><span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span><span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + "</span>";
if (btn) btn.classList.remove("sch-dirty"); row.querySelector("input")?.addEventListener("change", function(e) {
} onChecklistToggle(bookmark.id, e.currentTarget.checked);
function showToast(msg, isError) { });
const el = document.getElementById("background-decode-toast"); container.appendChild(row);
if (!el) return;
el.textContent = msg;
el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
el.style.display = "block";
setTimeout(function() {
el.style.display = "none";
}, 3e3);
}
function selectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
var ids = supportedBookmarks().map(function(bm) {
return bm.id;
});
currentConfig.bookmark_ids = ids;
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function deselectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
currentConfig.bookmark_ids = [];
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function wireBackgroundDecodeEvents() {
const filterInput = document.getElementById("bgd-bookmark-filter");
if (filterInput && !filterInput._wired) {
filterInput._wired = true;
filterInput.addEventListener("input", function() {
renderBookmarkChecklist(filterInput.value);
}); });
} }
const enabledCb = document.getElementById("background-decode-enabled"); function onChecklistToggle(bookmarkId, checked) {
if (enabledCb && !enabledCb._wired) { if (!currentConfig) {
enabledCb._wired = true; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
enabledCb.addEventListener("change", function() { }
markBgdDirty(); if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
currentConfig.bookmark_ids.push(bookmarkId);
} else if (!checked) {
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function(id) {
return id !== bookmarkId;
});
}
markBgdDirty();
}
function saveBackgroundDecode() {
const rigId = currentRigId;
if (!rigId) return;
const payload = {
remote: rigId,
enabled: document.getElementById("background-decode-enabled")?.checked ?? false,
bookmark_ids: currentConfig?.bookmark_ids.slice() ?? []
};
const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.disabled = true;
apiPutConfig(rigId, payload).then(function(saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode saved.", false);
}).catch(function(err) {
showToast(`Save failed: ${errorMessage(err)}`, true);
}).finally(function() {
if (btn) btn.disabled = false;
}); });
} }
const selectAllBtn = document.getElementById("bgd-select-all-btn"); async function resetBackgroundDecode() {
if (selectAllBtn && !selectAllBtn._wired) { const rigId = currentRigId;
selectAllBtn._wired = true; if (!rigId) return;
selectAllBtn.addEventListener("click", selectAllBookmarks); if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
apiResetConfig(rigId).then(function(saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode reset.", false);
}).catch(function(err) {
showToast(`Reset failed: ${errorMessage(err)}`, true);
});
} }
const deselectAllBtn = document.getElementById("bgd-deselect-all-btn"); function startStatusPolling() {
if (deselectAllBtn && !deselectAllBtn._wired) { if (statusInterval) clearInterval(statusInterval);
deselectAllBtn._wired = true; statusInterval = setInterval(pollBackgroundDecodeStatus, 15e3);
deselectAllBtn.addEventListener("click", deselectAllBookmarks);
} }
const saveBtn = document.getElementById("background-decode-save-btn"); function pollBackgroundDecodeStatus() {
if (saveBtn && !saveBtn._wired) { const rigId = currentRigId;
saveBtn._wired = true; if (!rigId) return;
saveBtn.addEventListener("click", saveBackgroundDecode); apiGetStatus(rigId).then(renderStatus).catch(function() {
});
} }
const resetBtn = document.getElementById("background-decode-reset-btn"); function renderStatus(status) {
if (resetBtn && !resetBtn._wired) { const card = document.getElementById("background-decode-status-card");
resetBtn._wired = true; if (!card) return;
resetBtn.addEventListener("click", resetBackgroundDecode); const entries = status.entries ?? [];
if (!entries.length) {
card.textContent = "No background decode bookmarks configured.";
return;
}
const summary = [];
if (status.active_rig) {
if (typeof status.center_hz === "number" && Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
if (typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
} else {
summary.push("This rig is not currently selected for audio.");
}
let html = summary.length ? '<div style="margin-bottom:0.8rem;color:var(--text-muted);">' + escHtml(summary.join(" · ")) + "</div>" : "";
html += '<div class="bgd-status-list">';
entries.forEach(function(entry) {
const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
const parts = [];
if (typeof entry.freq_hz === "number" && Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
if (entry.mode) parts.push(entry.mode);
if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
parts.push(entry.decoder_kinds.join("/").toUpperCase());
}
html += '<div class="bgd-status-row"><div><div class="bgd-status-name">' + escHtml(name) + '</div><div class="bgd-status-meta">' + escHtml(parts.join(" · ")) + '</div></div><div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '"><svg class="bgd-state-dot" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3.5"/></svg>' + escHtml(prettyState(entry.state)) + "</div></div>";
});
html += "</div>";
card.innerHTML = html;
} }
} function prettyState(state) {
window.initBackgroundDecode = initBackgroundDecode; switch (state) {
window.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents; case "active":
window.setBackgroundDecodeRig = setBackgroundDecodeRig; return "✓ Active";
case "out_of_span":
return "△ Out of span";
case "waiting_for_spectrum":
return "△ Waiting";
case "waiting_for_user":
return "△ No user";
case "missing_bookmark":
return "✗ Missing";
case "no_supported_decoders":
return "✗ Unsupported";
case "disabled":
return "△ Disabled";
case "handled_by_scheduler":
return "△ Scheduler";
case "scheduler_has_control":
return "△ Scheduler";
case "handled_by_virtual_channel":
return "△ VChan";
default:
return "△ Inactive";
}
}
function setCheckbox(id, value) {
const el = document.getElementById(id);
if (el) el.checked = value;
}
function formatFreq(hz) {
if (!Number.isFinite(hz) || hz <= 0) return "--";
if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
return `${String(hz)} Hz`;
}
function escHtml(value) {
const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function markBgdDirty() {
if (bgdDirty) return;
bgdDirty = true;
const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.add("sch-dirty");
}
function clearBgdDirty() {
bgdDirty = false;
const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.remove("sch-dirty");
}
function showToast(msg, isError) {
const el = document.getElementById("background-decode-toast");
if (!el) return;
el.textContent = msg;
el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
el.style.display = "block";
setTimeout(function() {
el.style.display = "none";
}, 3e3);
}
function selectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
const ids = supportedBookmarks().map(function(bm) {
return bm.id;
});
currentConfig.bookmark_ids = ids;
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function deselectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
currentConfig.bookmark_ids = [];
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function wireBackgroundDecodeEvents() {
const filterInput = document.getElementById("bgd-bookmark-filter");
if (filterInput && !filterInput._wired) {
filterInput._wired = true;
filterInput.addEventListener("input", function() {
renderBookmarkChecklist(filterInput.value);
});
}
const enabledCb = document.getElementById("background-decode-enabled");
if (enabledCb && !enabledCb._wired) {
enabledCb._wired = true;
enabledCb.addEventListener("change", function() {
markBgdDirty();
});
}
const selectAllBtn = document.getElementById("bgd-select-all-btn");
if (selectAllBtn && !selectAllBtn._wired) {
selectAllBtn._wired = true;
selectAllBtn.addEventListener("click", selectAllBookmarks);
}
const deselectAllBtn = document.getElementById("bgd-deselect-all-btn");
if (deselectAllBtn && !deselectAllBtn._wired) {
deselectAllBtn._wired = true;
deselectAllBtn.addEventListener("click", deselectAllBookmarks);
}
const saveBtn = document.getElementById("background-decode-save-btn");
if (saveBtn && !saveBtn._wired) {
saveBtn._wired = true;
saveBtn.addEventListener("click", saveBackgroundDecode);
}
const resetBtn = document.getElementById("background-decode-reset-btn");
if (resetBtn && !resetBtn._wired) {
resetBtn._wired = true;
resetBtn.addEventListener("click", () => {
void resetBackgroundDecode();
});
}
}
bgdWindow.initBackgroundDecode = initBackgroundDecode;
bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
})();
})(); })();
@@ -10,7 +10,7 @@ const pluginGroups = {
}; };
const loaded = /* @__PURE__ */ new Set(); const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map(); const loading = /* @__PURE__ */ new Map();
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]); const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js"]);
function loadLegacyScript(path) { function loadLegacyScript(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
@@ -24,7 +24,6 @@ await build({
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"), "webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
ais: path.join(sourceDir, "plugins", "ais.js"), ais: path.join(sourceDir, "plugins", "ais.js"),
aprs: path.join(sourceDir, "plugins", "aprs.js"), aprs: path.join(sourceDir, "plugins", "aprs.js"),
"background-decode": path.join(sourceDir, "plugins", "background-decode.js"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"), bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"), "hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"),
sat: path.join(sourceDir, "plugins", "sat.js"), sat: path.join(sourceDir, "plugins", "sat.js"),
@@ -51,6 +50,7 @@ await build({
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"), "leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"),
vdes: path.join(sourceDir, "plugins", "vdes.ts"), vdes: path.join(sourceDir, "plugins", "vdes.ts"),
wefax: path.join(sourceDir, "plugins", "wefax.ts"), wefax: path.join(sourceDir, "plugins", "wefax.ts"),
"background-decode": path.join(sourceDir, "plugins", "background-decode.ts"),
}, },
outdir: outputDir, outdir: outputDir,
bundle: true, bundle: true,
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
const loaded = new Set<string>(); const loaded = new Set<string>();
const loading = new Map<string, Promise<void>>(); const loading = new Map<string, Promise<void>>();
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]); const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js"]);
function loadLegacyScript(path: string): Promise<void> { function loadLegacyScript(path: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -2,30 +2,75 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
export {};
interface DecoderDescriptor {
id: string;
background_decode?: boolean;
activation?: string;
active_modes: string[];
}
interface Bookmark {
id: string;
name: string;
freq_hz: number;
mode: string;
decoders?: string[];
}
interface BackgroundDecodeConfig {
remote: string | null;
enabled: boolean;
bookmark_ids: string[];
}
interface BackgroundStatusEntry {
bookmark_name?: string;
bookmark_id?: string;
freq_hz?: number;
mode?: string;
decoder_kinds?: string[];
state?: string;
}
interface BackgroundDecodeStatus {
entries?: BackgroundStatusEntry[];
active_rig?: boolean;
center_hz?: number;
sample_rate?: number;
}
interface BackgroundBridge {
decoderRegistry?: DecoderDescriptor[];
authEnabled?: boolean;
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
initBackgroundDecode?: (rigId: string | null, role: string | null) => void;
wireBackgroundDecodeEvents?: () => void;
setBackgroundDecodeRig?: (rigId: string | null) => void;
}
interface WiredElement extends HTMLElement { _wired?: boolean }
const bgdWindow = window as unknown as BackgroundBridge;
(function () { (function () {
"use strict"; "use strict";
function bgdSupportedIds() { function bgdSupportedIds(): string[] {
return (window.decoderRegistry || []) return (bgdWindow.decoderRegistry || [])
.filter(function (d) { return d.background_decode; }) .filter(function (d) { return d.background_decode; })
.map(function (d) { return d.id; }); .map(function (d) { return d.id; });
} }
let backgroundDecodeRole = null; let backgroundDecodeRole: string | null = null;
let currentRigId = null; let currentRigId: string | null = null;
let currentConfig = null; let currentConfig: BackgroundDecodeConfig | null = null;
let bookmarkList = []; let bookmarkList: Bookmark[] = [];
let statusInterval = null; let statusInterval: ReturnType<typeof setInterval> | null = null;
let bgdDirty = false; let bgdDirty = false;
function initBackgroundDecode(rigId, role) { function initBackgroundDecode(rigId: string | null, role: string | null): void {
backgroundDecodeRole = role; backgroundDecodeRole = role;
currentRigId = rigId || null; currentRigId = rigId || null;
if (currentRigId) loadBackgroundDecode(); if (currentRigId) loadBackgroundDecode();
startStatusPolling(); startStatusPolling();
} }
function setBackgroundDecodeRig(rigId) { function setBackgroundDecodeRig(rigId: string | null): void {
const nextRigId = rigId || null; const nextRigId = rigId || null;
if (nextRigId === currentRigId) return; if (nextRigId === currentRigId) return;
currentRigId = nextRigId; currentRigId = nextRigId;
@@ -33,44 +78,44 @@
loadBackgroundDecode(); loadBackgroundDecode();
} }
function apiGetConfig(rigId) { function apiGetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function (r) { return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json(); return r.json() as Promise<BackgroundDecodeConfig>;
}); });
} }
function apiPutConfig(rigId, config) { function apiPutConfig(rigId: string, config: BackgroundDecodeConfig): Promise<BackgroundDecodeConfig> {
return fetch("/background-decode/" + encodeURIComponent(rigId), { return fetch("/background-decode/" + encodeURIComponent(rigId), {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(config), body: JSON.stringify(config),
}).then(function (r) { }).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json(); return r.json() as Promise<BackgroundDecodeConfig>;
}); });
} }
function apiResetConfig(rigId) { function apiResetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
return fetch("/background-decode/" + encodeURIComponent(rigId), { return fetch("/background-decode/" + encodeURIComponent(rigId), {
method: "DELETE", method: "DELETE",
}).then(function (r) { }).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json(); return r.json() as Promise<BackgroundDecodeConfig>;
}); });
} }
function apiGetStatus(rigId) { function apiGetStatus(rigId: string): Promise<BackgroundDecodeStatus> {
return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function (r) { return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json(); return r.json() as Promise<BackgroundDecodeStatus>;
}); });
} }
function apiGetBookmarks() { function apiGetBookmarks(): Promise<Bookmark[]> {
return fetch("/bookmarks").then(function (r) { return fetch("/bookmarks").then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status); if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
return r.json(); return r.json() as Promise<Bookmark[]>;
}); });
} }
@@ -79,35 +124,35 @@
if (!rigId) return; if (!rigId) return;
Promise.all([apiGetConfig(rigId), apiGetBookmarks()]) Promise.all([apiGetConfig(rigId), apiGetBookmarks()])
.then(function ([config, bookmarks]) { .then(function ([config, bookmarks]) {
currentConfig = config || { remote: rigId, enabled: false, bookmark_ids: [] }; currentConfig = config;
bookmarkList = Array.isArray(bookmarks) ? bookmarks : []; bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
renderBackgroundDecode(); renderBackgroundDecode();
clearBgdDirty(); clearBgdDirty();
pollBackgroundDecodeStatus(); pollBackgroundDecodeStatus();
}) })
.catch(function (err) { .catch(function (err: unknown) {
console.error("background decode load failed", err); console.error("background decode load failed", err);
}); });
} }
function supportedBookmarks() { function supportedBookmarks(): Bookmark[] {
return bookmarkList.filter(function (bookmark) { return bookmarkList.filter(function (bookmark) {
return bookmarkDecoderKinds(bookmark).length > 0; return bookmarkDecoderKinds(bookmark).length > 0;
}); });
} }
function bookmarkDecoderKinds(bookmark) { function bookmarkDecoderKinds(bookmark: Bookmark): string[] {
var ids = bgdSupportedIds(); const ids = bgdSupportedIds();
var decoders = Array.isArray(bookmark && bookmark.decoders) ? bookmark.decoders : []; const decoders = bookmark.decoders ?? [];
var explicit = decoders const explicit = decoders
.map(function (item) { return String(item || "").trim().toLowerCase(); }) .map(function (item) { return item.trim().toLowerCase(); })
.filter(function (item, index, arr) { .filter(function (item, index, arr) {
return ids.indexOf(item) >= 0 && arr.indexOf(item) === index; return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
}); });
if (explicit.length > 0) return explicit; if (explicit.length > 0) return explicit;
// Fall back: infer from mode via mode-bound entries in the registry. // Fall back: infer from mode via mode-bound entries in the registry.
var mode = String(bookmark && bookmark.mode || "").trim().toUpperCase(); const mode = bookmark.mode.trim().toUpperCase();
return (window.decoderRegistry || []) return (bgdWindow.decoderRegistry || [])
.filter(function (d) { .filter(function (d) {
return d.activation === "mode_bound" && d.background_decode return d.activation === "mode_bound" && d.background_decode
&& d.active_modes.indexOf(mode) >= 0; && d.active_modes.indexOf(mode) >= 0;
@@ -119,13 +164,13 @@
if (!currentConfig) { if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] }; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
setCheckbox("background-decode-enabled", !!currentConfig.enabled); setCheckbox("background-decode-enabled", currentConfig.enabled);
renderBookmarkChecklist(); renderBookmarkChecklist();
const isControl = backgroundDecodeRole === "control" || (typeof authEnabled !== "undefined" && !authEnabled); const isControl = backgroundDecodeRole === "control" || bgdWindow.authEnabled === false;
const panel = document.getElementById("background-decode-panel"); const panel = document.getElementById("background-decode-panel");
if (panel) { if (panel) {
panel.querySelectorAll("input, select, button.sch-write").forEach(function (el) { panel.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>("input, select, button.sch-write").forEach(function (el) {
el.disabled = !isControl; el.disabled = !isControl;
}); });
} }
@@ -135,7 +180,7 @@
if (resetBtn) resetBtn.style.display = isControl ? "" : "none"; if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
} }
function renderBookmarkChecklist(filterText) { function renderBookmarkChecklist(filterText = ""): void {
const container = document.getElementById("bgd-bookmark-checklist"); const container = document.getElementById("bgd-bookmark-checklist");
if (!container) return; if (!container) return;
container.innerHTML = ""; container.innerHTML = "";
@@ -148,7 +193,7 @@
const filtered = filter const filtered = filter
? all.filter(function (bm) { ? all.filter(function (bm) {
var text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase(); const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
return text.indexOf(filter) >= 0; return text.indexOf(filter) >= 0;
}) })
: all; : all;
@@ -161,22 +206,22 @@
} }
filtered.forEach(function (bookmark) { filtered.forEach(function (bookmark) {
var row = document.createElement("label"); const row = document.createElement("label");
row.className = "bgd-checklist-row"; row.className = "bgd-checklist-row";
var decoders = bookmarkDecoderKinds(bookmark); const decoders = bookmarkDecoderKinds(bookmark);
var checked = selectedIds.has(bookmark.id) ? " checked" : ""; const checked = selectedIds.has(bookmark.id) ? " checked" : "";
row.innerHTML = row.innerHTML =
'<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" />' + '<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" />' +
'<span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span>' + '<span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span>' +
'<span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + '</span>'; '<span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + '</span>';
row.querySelector("input").addEventListener("change", function (e) { row.querySelector<HTMLInputElement>("input")?.addEventListener("change", function (e) {
onChecklistToggle(bookmark.id, e.target.checked); onChecklistToggle(bookmark.id, (e.currentTarget as HTMLInputElement).checked);
}); });
container.appendChild(row); container.appendChild(row);
}); });
} }
function onChecklistToggle(bookmarkId, checked) { function onChecklistToggle(bookmarkId: string, checked: boolean): void {
if (!currentConfig) { if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] }; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
@@ -192,12 +237,12 @@
function saveBackgroundDecode() { function saveBackgroundDecode() {
const rigId = currentRigId; const rigId = currentRigId;
if (!rigId) return; if (!rigId) return;
const payload = { const payload: BackgroundDecodeConfig = {
remote: rigId, remote: rigId,
enabled: !!document.getElementById("background-decode-enabled").checked, enabled: (document.getElementById("background-decode-enabled") as HTMLInputElement | null)?.checked ?? false,
bookmark_ids: Array.isArray(currentConfig && currentConfig.bookmark_ids) ? currentConfig.bookmark_ids.slice() : [], bookmark_ids: currentConfig?.bookmark_ids.slice() ?? [],
}; };
const btn = document.getElementById("background-decode-save-btn"); const btn = document.getElementById("background-decode-save-btn") as HTMLButtonElement | null;
if (btn) btn.disabled = true; if (btn) btn.disabled = true;
apiPutConfig(rigId, payload) apiPutConfig(rigId, payload)
.then(function (saved) { .then(function (saved) {
@@ -205,10 +250,10 @@
renderBackgroundDecode(); renderBackgroundDecode();
clearBgdDirty(); clearBgdDirty();
pollBackgroundDecodeStatus(); pollBackgroundDecodeStatus();
showToast("Background decode saved."); showToast("Background decode saved.", false);
}) })
.catch(function (err) { .catch(function (err: unknown) {
showToast("Save failed: " + err.message, true); showToast(`Save failed: ${errorMessage(err)}`, true);
}) })
.finally(function () { .finally(function () {
if (btn) btn.disabled = false; if (btn) btn.disabled = false;
@@ -218,17 +263,17 @@
async function resetBackgroundDecode() { async function resetBackgroundDecode() {
const rigId = currentRigId; const rigId = currentRigId;
if (!rigId) return; if (!rigId) return;
if (!await window.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return; if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
apiResetConfig(rigId) apiResetConfig(rigId)
.then(function (saved) { .then(function (saved) {
currentConfig = saved; currentConfig = saved;
renderBackgroundDecode(); renderBackgroundDecode();
clearBgdDirty(); clearBgdDirty();
pollBackgroundDecodeStatus(); pollBackgroundDecodeStatus();
showToast("Background decode reset."); showToast("Background decode reset.", false);
}) })
.catch(function (err) { .catch(function (err: unknown) {
showToast("Reset failed: " + err.message, true); showToast(`Reset failed: ${errorMessage(err)}`, true);
}); });
} }
@@ -245,18 +290,18 @@
.catch(function () {}); .catch(function () {});
} }
function renderStatus(status) { function renderStatus(status: BackgroundDecodeStatus): void {
const card = document.getElementById("background-decode-status-card"); const card = document.getElementById("background-decode-status-card");
if (!card) return; if (!card) return;
const entries = Array.isArray(status && status.entries) ? status.entries : []; const entries = status.entries ?? [];
if (!entries.length) { if (!entries.length) {
card.textContent = "No background decode bookmarks configured."; card.textContent = "No background decode bookmarks configured.";
return; return;
} }
const summary = []; const summary = [];
if (status.active_rig) { if (status.active_rig) {
if (Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz)); if (typeof status.center_hz === "number" && Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
if (Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2)); if (typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
} else { } else {
summary.push("This rig is not currently selected for audio."); summary.push("This rig is not currently selected for audio.");
} }
@@ -265,7 +310,7 @@
entries.forEach(function (entry) { entries.forEach(function (entry) {
const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark"; const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
const parts = []; const parts = [];
if (Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz)); if (typeof entry.freq_hz === "number" && Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
if (entry.mode) parts.push(entry.mode); if (entry.mode) parts.push(entry.mode);
if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) { if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
parts.push(entry.decoder_kinds.join("/").toUpperCase()); parts.push(entry.decoder_kinds.join("/").toUpperCase());
@@ -285,7 +330,7 @@
card.innerHTML = html; card.innerHTML = html;
} }
function prettyState(state) { function prettyState(state: string | undefined): string {
switch (state) { switch (state) {
case "active": return "\u2713 Active"; case "active": return "\u2713 Active";
case "out_of_span": return "\u25B3 Out of span"; case "out_of_span": return "\u25B3 Out of span";
@@ -301,40 +346,47 @@
} }
} }
function setCheckbox(id, value) { function setCheckbox(id: string, value: boolean): void {
const el = document.getElementById(id); const el = document.getElementById(id) as HTMLInputElement | null;
if (el) el.checked = !!value; if (el) el.checked = value;
} }
function formatFreq(hz) { function formatFreq(hz: number): string {
if (!Number.isFinite(hz) || hz <= 0) return "--"; if (!Number.isFinite(hz) || hz <= 0) return "--";
if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz"; if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz"; if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
return hz + " Hz"; return `${String(hz)} Hz`;
} }
function escHtml(value) { function escHtml(value: unknown): string {
return String(value == null ? "" : value) const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean"
? String(value)
: "";
return text
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;"); .replace(/"/g, "&quot;");
} }
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function markBgdDirty() { function markBgdDirty() {
if (bgdDirty) return; if (bgdDirty) return;
bgdDirty = true; bgdDirty = true;
var btn = document.getElementById("background-decode-save-btn"); const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.add("sch-dirty"); if (btn) btn.classList.add("sch-dirty");
} }
function clearBgdDirty() { function clearBgdDirty() {
bgdDirty = false; bgdDirty = false;
var btn = document.getElementById("background-decode-save-btn"); const btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.remove("sch-dirty"); if (btn) btn.classList.remove("sch-dirty");
} }
function showToast(msg, isError) { function showToast(msg: string, isError: boolean): void {
const el = document.getElementById("background-decode-toast"); const el = document.getElementById("background-decode-toast");
if (!el) return; if (!el) return;
el.textContent = msg; el.textContent = msg;
@@ -349,9 +401,9 @@
if (!currentConfig) { if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] }; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
var ids = supportedBookmarks().map(function (bm) { return bm.id; }); const ids = supportedBookmarks().map(function (bm) { return bm.id; });
currentConfig.bookmark_ids = ids; currentConfig.bookmark_ids = ids;
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value); renderBookmarkChecklist((document.getElementById("bgd-bookmark-filter") as HTMLInputElement | null)?.value);
markBgdDirty(); markBgdDirty();
} }
@@ -360,12 +412,12 @@
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] }; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
} }
currentConfig.bookmark_ids = []; currentConfig.bookmark_ids = [];
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value); renderBookmarkChecklist((document.getElementById("bgd-bookmark-filter") as HTMLInputElement | null)?.value);
markBgdDirty(); markBgdDirty();
} }
function wireBackgroundDecodeEvents() { function wireBackgroundDecodeEvents() {
const filterInput = document.getElementById("bgd-bookmark-filter"); const filterInput = document.getElementById("bgd-bookmark-filter") as (HTMLInputElement & WiredElement) | null;
if (filterInput && !filterInput._wired) { if (filterInput && !filterInput._wired) {
filterInput._wired = true; filterInput._wired = true;
filterInput.addEventListener("input", function () { filterInput.addEventListener("input", function () {
@@ -373,38 +425,38 @@
}); });
} }
const enabledCb = document.getElementById("background-decode-enabled"); const enabledCb = document.getElementById("background-decode-enabled") as (HTMLInputElement & WiredElement) | null;
if (enabledCb && !enabledCb._wired) { if (enabledCb && !enabledCb._wired) {
enabledCb._wired = true; enabledCb._wired = true;
enabledCb.addEventListener("change", function () { markBgdDirty(); }); enabledCb.addEventListener("change", function () { markBgdDirty(); });
} }
const selectAllBtn = document.getElementById("bgd-select-all-btn"); const selectAllBtn = document.getElementById("bgd-select-all-btn") as WiredElement | null;
if (selectAllBtn && !selectAllBtn._wired) { if (selectAllBtn && !selectAllBtn._wired) {
selectAllBtn._wired = true; selectAllBtn._wired = true;
selectAllBtn.addEventListener("click", selectAllBookmarks); selectAllBtn.addEventListener("click", selectAllBookmarks);
} }
const deselectAllBtn = document.getElementById("bgd-deselect-all-btn"); const deselectAllBtn = document.getElementById("bgd-deselect-all-btn") as WiredElement | null;
if (deselectAllBtn && !deselectAllBtn._wired) { if (deselectAllBtn && !deselectAllBtn._wired) {
deselectAllBtn._wired = true; deselectAllBtn._wired = true;
deselectAllBtn.addEventListener("click", deselectAllBookmarks); deselectAllBtn.addEventListener("click", deselectAllBookmarks);
} }
const saveBtn = document.getElementById("background-decode-save-btn"); const saveBtn = document.getElementById("background-decode-save-btn") as WiredElement | null;
if (saveBtn && !saveBtn._wired) { if (saveBtn && !saveBtn._wired) {
saveBtn._wired = true; saveBtn._wired = true;
saveBtn.addEventListener("click", saveBackgroundDecode); saveBtn.addEventListener("click", saveBackgroundDecode);
} }
const resetBtn = document.getElementById("background-decode-reset-btn"); const resetBtn = document.getElementById("background-decode-reset-btn") as WiredElement | null;
if (resetBtn && !resetBtn._wired) { if (resetBtn && !resetBtn._wired) {
resetBtn._wired = true; resetBtn._wired = true;
resetBtn.addEventListener("click", resetBackgroundDecode); resetBtn.addEventListener("click", () => { void resetBackgroundDecode(); });
} }
} }
window.initBackgroundDecode = initBackgroundDecode; bgdWindow.initBackgroundDecode = initBackgroundDecode;
window.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents; bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
window.setBackgroundDecodeRig = setBackgroundDecodeRig; bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
})(); })();
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
test("background decode loads configuration for the explicitly selected rig", async () => {
const requested = [];
const window = {
decoderRegistry: [],
trxUi: { confirm: async () => true },
};
const context = vm.createContext({
window,
document: { getElementById: () => null },
fetch: async (url) => {
requested.push(url);
return {
ok: true,
json: async () => url === "/bookmarks"
? []
: url.endsWith("/status")
? { entries: [] }
: { remote: "rig/a", enabled: false, bookmark_ids: [] },
};
},
setInterval: () => 1,
clearInterval() {},
setTimeout,
URL,
Set,
Error,
console,
});
const source = await readFile(new URL("../../assets/web/generated/background-decode.js", import.meta.url), "utf8");
new vm.Script(source).runInContext(context);
window.initBackgroundDecode("rig/a", "control");
await new Promise((resolve) => setTimeout(resolve, 0));
assert.ok(requested.includes("/background-decode/rig%2Fa"));
assert.ok(requested.includes("/bookmarks"));
assert.ok(requested.includes("/background-decode/rig%2Fa/status"));
});