[refactor](trx-frontend-http): wire JS frontend to decoder registry

Fetch /decoders on page load and use the registry to drive all
decoder-related UI instead of hardcoded lists:

- bookmarks.js: bmReadDecoders/bmWriteDecoders and bookmark form
  checkboxes generated from registry; bmApply() decoder toggle gate
  uses registry active_modes instead of hardcoded DIG/FM check
- background-decode.js: delete SUPPORTED_DECODERS constant, derive
  bookmarkDecoderKinds() from registry
- app.js: _decoderToggles and SSE status sync built from registry;
  updateDecodeStatus() and setModeBoundDecodeStatus() driven by
  registry mode_bound/toggle entries
- index.html: replace 8 hardcoded decoder checkboxes with dynamic
  container populated from registry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-30 20:57:13 +02:00
parent e6dbfd1edb
commit bb1fdbb43d
4 changed files with 147 additions and 113 deletions
@@ -228,29 +228,36 @@ function bmChangePage(delta) {
// Read decoder checkboxes and return an array of selected decoder names.
function bmReadDecoders() {
const decoders = [];
if (document.getElementById("bm-dec-aprs").checked) decoders.push("aprs");
if (document.getElementById("bm-dec-ais").checked) decoders.push("ais");
if (document.getElementById("bm-dec-ft8").checked) decoders.push("ft8");
if (document.getElementById("bm-dec-ft4").checked) decoders.push("ft4");
if (document.getElementById("bm-dec-ft2").checked) decoders.push("ft2");
if (document.getElementById("bm-dec-wspr").checked) decoders.push("wspr");
if (document.getElementById("bm-dec-hf-aprs").checked) decoders.push("hf-aprs");
if (document.getElementById("bm-dec-lrpt").checked) decoders.push("lrpt");
return decoders;
return (window.decoderRegistry || [])
.filter(d => d.bookmark_selectable)
.filter(d => document.getElementById("bm-dec-" + d.id)?.checked)
.map(d => d.id);
}
// Set decoder checkboxes to match the given array.
function bmWriteDecoders(decoders) {
const list = decoders || [];
document.getElementById("bm-dec-aprs").checked = list.includes("aprs");
document.getElementById("bm-dec-ais").checked = list.includes("ais");
document.getElementById("bm-dec-ft8").checked = list.includes("ft8");
document.getElementById("bm-dec-ft4").checked = list.includes("ft4");
document.getElementById("bm-dec-ft2").checked = list.includes("ft2");
document.getElementById("bm-dec-wspr").checked = list.includes("wspr");
document.getElementById("bm-dec-hf-aprs").checked = list.includes("hf-aprs");
document.getElementById("bm-dec-lrpt").checked = list.includes("lrpt");
const set = new Set(decoders || []);
(window.decoderRegistry || [])
.filter(d => d.bookmark_selectable)
.forEach(d => {
const el = document.getElementById("bm-dec-" + d.id);
if (el) el.checked = set.has(d.id);
});
}
// Build decoder checkboxes dynamically from the registry.
function bmBuildDecoderCheckboxes() {
const container = document.getElementById("bm-decoder-checkboxes");
if (!container) return;
container.innerHTML = "";
(window.decoderRegistry || [])
.filter(d => d.bookmark_selectable)
.forEach(d => {
const label = document.createElement("label");
label.className = "bm-decoder-check";
label.innerHTML = '<input type="checkbox" id="bm-dec-' + d.id + '" value="' + d.id + '" /> ' + d.label;
container.appendChild(label);
});
}
function bmOpenForm(bm) {
@@ -428,20 +435,27 @@ async function bmApply(bm) {
await postPath("/set_freq?hz=" + bm.freq_hz);
}
})();
// Decoder toggles (DIG / FM modes) — also fire-and-forget.
// Decoder toggles fire-and-forget.
// Only toggle decoders that are toggle-gated and whose active modes
// include the bookmark's mode (driven by the decoder registry).
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
const decoderMode = bm.mode === "DIG" || bm.mode === "FM";
const decoderPromise = (hasDecoders && decoderMode) ? (async () => {
const modeUp = (bm.mode || "").toUpperCase();
const toggleDecoders = (window.decoderRegistry || []).filter(d =>
d.activation === "toggle" && d.active_modes.includes(modeUp)
);
const shouldToggle = hasDecoders && toggleDecoders.length > 0;
const decoderPromise = shouldToggle ? (async () => {
const statusResp = await fetch("/status");
if (statusResp.ok) {
const st = await statusResp.json();
const toggles = [];
const check = (key) => {
if (bm.decoders.includes(key) !== !!st[key.replace(/-/g, "_") + "_decode_enabled"]) {
toggles.push(postPath("/toggle_" + key.replace(/-/g, "_") + "_decode"));
for (const d of toggleDecoders) {
const statusKey = d.id.replace(/-/g, "_") + "_decode_enabled";
const wanted = bm.decoders.includes(d.id);
if (wanted !== !!st[statusKey]) {
toggles.push(postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
}
};
check("ft8"); check("ft4"); check("ft2"); check("wspr"); check("hf-aprs"); check("lrpt");
}
if (toggles.length) await Promise.all(toggles);
}
})() : Promise.resolve();
@@ -604,6 +618,13 @@ function bmPopulateScopePicker() {
// scripts run if auth is disabled; otherwise bmFetch() will sync it).
bmSyncAccess();
// Build decoder checkboxes from registry. The registry is fetched async
// so we rebuild once it arrives to ensure checkboxes are present.
bmBuildDecoderCheckboxes();
if (typeof window.onDecoderRegistryReady === "function") {
window.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
}
// Scope picker
bmPopulateScopePicker();
const scopePicker = document.getElementById("bm-scope-picker");