From 45db219b83d8951e1b7b54ef8bf07223f0f6bf3b Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Fri, 7 Aug 2026 19:10:59 +0200 Subject: [PATCH] [fix](trx-frontend-http): rebuild the background decode panel around one list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel listed the same bookmarks twice: a status card at the top naming the selected ones and what each was doing, and a checklist below naming all of them with the checkboxes. Neither list said which row in the other it meant, so choosing what to decode and reading what was being decoded were two passes over the same names. The checklist itself sat squeezed against the right-hand edge, because the row it lives in sets `align-items: flex-end` and an inline `flex-direction: column` turned that into "push everything right". It is one list now. A row carries its checkbox, its name, its frequency, mode and decoder, and its own state, so picking and watching happen in the same place. What the rig can hear moves up beside the switch, where it explains why a selected bookmark reads out of span, and the selection adds itself up under the list. The states lose the ✓/△/✗ they carried next to a dot that already said as much, and say what they mean: "Out of span", "Scheduler has it", "Nobody listening", each with the reason on hover. The empty list now says which of the two reasons it is empty for, and what to do. Save was live from the moment the panel opened, with nothing to save; it now offers itself only when something has changed. Two races behind it, both of which left the panel useless rather than ugly: * The rig was whatever the panel was handed at load. Loading before the rig list arrived handed it null, and the next telling only came when the operator switched rigs — so the panel stayed empty and silent. Every rig list refresh now passes the rig on, and both panels ignore one they have. * The settings panels are wired once the session is up, but their modules import asynchronously and the wiring skipped whichever had not arrived. A panel that missed it kept dead buttons for the rest of the session: no filter, no Select All, no Save. Wiring runs again when the modules land. Closes #52 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams --- .../assets/web/generated/app.js | 10 +- .../assets/web/generated/background-decode.js | 163 ++++++++++---- .../trx-frontend-http/assets/web/index.html | 37 +-- .../trx-frontend-http/assets/web/style.css | 201 ++++++++++------- .../trx-frontend-http/frontend/package.json | 2 +- .../trx-frontend-http/frontend/src/app.ts | 23 +- .../frontend/src/plugins/background-decode.ts | 213 ++++++++++++------ .../frontend/tests/background-decode.mjs | 138 ++++++++++++ .../frontend/tests/web-fixture.mjs | 38 +++- 9 files changed, 611 insertions(+), 214 deletions(-) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/background-decode.mjs diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index 052bd6e9..4e315f52 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -2905,9 +2905,9 @@ function applyRigList(activeRigId, rigIds, displayNames) { populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch); updateRigSubtitle(lastActiveRigId); window.trxUi?.setActiveRig(lastActiveRigId); + window.trx.modules.scheduler?.setRig(lastActiveRigId); + window.trx.modules.backgroundDecode?.setRig(lastActiveRigId); if (rigListChanged) { - window.trx.modules.scheduler?.setRig(lastActiveRigId); - window.trx.modules.backgroundDecode?.setRig(lastActiveRigId); window.trx.modules.bookmarks?.populateScopePicker(); void window.trx.modules.bookmarks?.fetch(document.getElementById("bm-category-filter")?.value || ""); } @@ -6001,7 +6001,9 @@ async function initializeApp() { showAuthGate(allowGuest); } } +var settingsUiReady = false; function initSettingsUI() { + settingsUiReady = true; window.trx.modules.scheduler?.initialize(lastActiveRigId, authRole); window.trx.modules.scheduler?.wireEvents(); if (window.trx.modules.backgroundDecode) { @@ -6272,7 +6274,9 @@ Object.defineProperties(trxState, { } } }); window.trx = Object.freeze({ state: trxState, core: trxCore, modules: trxModules }); -void loadEagerPlugins().catch((error) => { +void loadEagerPlugins().then(() => { + if (settingsUiReady) initSettingsUI(); +}).catch((error) => { console.error(error); }); void initializeApp(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js index 24c09973..a14bc591 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js @@ -19,9 +19,11 @@ var bgdWindow = window; let bookmarkList = []; let statusInterval = null; let bgdDirty = false; + let statusByBookmark = /* @__PURE__ */ new Map(); + let lastStatus = null; function initBackgroundDecode(rigId, role) { backgroundDecodeRole = role; - currentRigId = rigId || null; + currentRigId = rigId || hostState.lastActiveRigId || null; if (currentRigId) loadBackgroundDecode(); startStatusPolling(); } @@ -108,7 +110,7 @@ var bgdWindow = window; } setCheckbox("background-decode-enabled", currentConfig.enabled); renderBookmarkChecklist(); - const isControl = backgroundDecodeRole === "control" || hostState.authEnabled === false; + const isControl = isControlRole(); const panel = document.getElementById("background-decode-panel"); if (panel) { panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) { @@ -119,6 +121,10 @@ var bgdWindow = window; const resetBtn = document.getElementById("background-decode-reset-btn"); if (saveBtn) saveBtn.style.display = isControl ? "" : "none"; if (resetBtn) resetBtn.style.display = isControl ? "" : "none"; + syncSaveButton(); + } + function currentFilterText() { + return document.getElementById("bgd-bookmark-filter")?.value ?? ""; } function renderBookmarkChecklist(filterText = "") { const container = document.getElementById("bgd-bookmark-checklist"); @@ -134,20 +140,49 @@ var bgdWindow = window; return text.indexOf(filter) >= 0; }) : all; if (filtered.length === 0) { - container.innerHTML = '
' + (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") + "
"; + container.innerHTML = '
' + escHtml(emptyListText(all.length)) + "
"; + renderSelectionSummary(); return; } filtered.forEach(function(bookmark) { const row = document.createElement("label"); row.className = "bgd-checklist-row"; + row.dataset.bmId = bookmark.id; const decoders = bookmarkDecoderKinds(bookmark); - const checked = selectedIds.has(bookmark.id) ? " checked" : ""; - row.innerHTML = '' + escHtml(bookmark.name) + '' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + ""; + const selected = selectedIds.has(bookmark.id); + if (selected) row.classList.add("is-selected"); + row.innerHTML = '' + escHtml(bookmark.name) + '' + escHtml(formatFreq(bookmark.freq_hz)) + '' + escHtml(bookmark.mode + " · " + decoders.join("/").toUpperCase()) + "" + stateBadgeHtml(bookmark.id, selected); row.querySelector("input")?.addEventListener("change", function(e) { onChecklistToggle(bookmark.id, e.currentTarget.checked); }); container.appendChild(row); }); + renderSelectionSummary(); + } + function emptyListText(supportedCount) { + if (supportedCount > 0) return "No bookmark matches that filter."; + if (bookmarkList.length > 0) { + return "None of your bookmarks name a decoder that can run in the background. Give one a decoder on the Bookmarks tab to list it here."; + } + return "No bookmarks yet. Save one on the Bookmarks tab and it can be decoded here."; + } + function stateBadgeHtml(bookmarkId, selected) { + if (!selected) return ''; + const entry = statusByBookmark.get(bookmarkId); + const state = entry?.state ?? (currentConfig?.enabled ? "pending" : "disabled"); + return '' + escHtml(prettyState(state)) + ""; + } + function renderSelectionSummary() { + const el = document.getElementById("bgd-selection-summary"); + if (!el) return; + const selected = currentConfig?.bookmark_ids.length ?? 0; + if (selected === 0) { + el.textContent = "Nothing selected — background decoding is idle."; + return; + } + const active = [...statusByBookmark.values()].filter((entry) => entry.state === "active").length; + const noun = `${String(selected)} bookmark${selected === 1 ? "" : "s"} selected`; + el.textContent = currentConfig?.enabled ? `${noun}, ${String(active)} decoding now.` : `${noun}. Switch Enabled on to start decoding them.`; } function onChecklistToggle(bookmarkId, checked) { if (!currentConfig) { @@ -182,7 +217,7 @@ var bgdWindow = window; }).catch(function(err) { showToast(`Save failed: ${errorMessage(err)}`, true); }).finally(function() { - if (btn) btn.disabled = false; + syncSaveButton(); }); } async function resetBackgroundDecode() { @@ -210,59 +245,83 @@ var bgdWindow = window; }); } function renderStatus(status) { - const card = document.getElementById("background-decode-status-card"); - if (!card) return; - const entries = status.entries ?? []; - if (!entries.length) { - card.textContent = "No background decode bookmarks configured."; + lastStatus = status; + statusByBookmark = new Map( + (status.entries ?? []).filter((entry) => typeof entry.bookmark_id === "string" && entry.bookmark_id.length > 0).map((entry) => [entry.bookmark_id, entry]) + ); + renderSpanSummary(); + renderBookmarkChecklist(currentFilterText()); + } + function renderSpanSummary() { + const el = document.getElementById("bgd-span-summary"); + if (!el) return; + const status = lastStatus; + if (!status) { + el.textContent = ""; 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."); + if (!status.active_rig) { + el.textContent = "This rig is not the one playing audio."; + el.dataset.tone = "warn"; + return; + } + const centre = typeof status.center_hz === "number" && Number.isFinite(status.center_hz) ? formatFreq(status.center_hz) : null; + const half = typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0 ? formatFreq(status.sample_rate / 2) : null; + el.dataset.tone = ""; + el.textContent = centre && half ? `Span ${centre} ±${half}` : centre ? `Centre ${centre}` : ""; + } + function stateHelp(state) { + switch (state) { + case "active": + return "Decoding on a hidden channel."; + case "out_of_span": + return "Outside the span the rig is tuned across, so it cannot be heard from here."; + case "waiting_for_spectrum": + return "Waiting for the first spectrum frame from the rig."; + case "waiting_for_user": + return "Nobody is listening to this rig, so no audio is being pulled."; + case "missing_bookmark": + return "The bookmark this was selected from is gone."; + case "no_supported_decoders": + return "No decoder that runs in the background can decode this bookmark."; + case "disabled": + return "Background decoding is switched off."; + case "handled_by_scheduler": + case "scheduler_has_control": + return "The scheduler is running this bookmark instead."; + case "handled_by_virtual_channel": + return "A virtual channel is already on this frequency."; + case "pending": + return "Selected, and not started yet — save to apply."; + default: + return "Selected, but not decoding."; } - let html = summary.length ? '
' + escHtml(summary.join(" · ")) + "
" : ""; - html += '
'; - 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 += '
' + escHtml(name) + '
' + escHtml(parts.join(" · ")) + '
' + escHtml(prettyState(entry.state)) + "
"; - }); - html += "
"; - card.innerHTML = html; } function prettyState(state) { switch (state) { case "active": - return "✓ Active"; + return "Decoding"; case "out_of_span": - return "△ Out of span"; + return "Out of span"; case "waiting_for_spectrum": - return "△ Waiting"; + return "Waiting for spectrum"; case "waiting_for_user": - return "△ No user"; + return "Nobody listening"; case "missing_bookmark": - return "✗ Missing"; + return "Bookmark gone"; case "no_supported_decoders": - return "✗ Unsupported"; + return "No decoder"; case "disabled": - return "△ Disabled"; + return "Off"; case "handled_by_scheduler": - return "△ Scheduler"; case "scheduler_has_control": - return "△ Scheduler"; + return "Scheduler has it"; case "handled_by_virtual_channel": - return "△ VChan"; + return "On a channel"; + case "pending": + return "Not saved"; default: - return "△ Inactive"; + return "Idle"; } } function setCheckbox(id, value) { @@ -285,13 +344,21 @@ var bgdWindow = window; function markBgdDirty() { if (bgdDirty) return; bgdDirty = true; - const btn = document.getElementById("background-decode-save-btn"); - if (btn) btn.classList.add("sch-dirty"); + syncSaveButton(); } function clearBgdDirty() { bgdDirty = false; + syncSaveButton(); + } + function syncSaveButton() { const btn = document.getElementById("background-decode-save-btn"); - if (btn) btn.classList.remove("sch-dirty"); + if (!btn) return; + btn.classList.toggle("sch-dirty", bgdDirty); + btn.disabled = !bgdDirty || !isControlRole(); + btn.title = bgdDirty ? "Apply these bookmarks to the background decoder" : "No changes to save"; + } + function isControlRole() { + return backgroundDecodeRole === "control" || hostState.authEnabled === false; } function showToast(msg, isError) { const el = document.getElementById("background-decode-toast"); @@ -311,7 +378,7 @@ var bgdWindow = window; return bm.id; }); currentConfig.bookmark_ids = ids; - renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value); + renderBookmarkChecklist(currentFilterText()); markBgdDirty(); } function deselectAllBookmarks() { @@ -319,7 +386,7 @@ var bgdWindow = window; currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] }; } currentConfig.bookmark_ids = []; - renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value); + renderBookmarkChecklist(currentFilterText()); markBgdDirty(); } function wireBackgroundDecodeEvents() { @@ -334,7 +401,9 @@ var bgdWindow = window; if (enabledCb && !enabledCb._wired) { enabledCb._wired = true; enabledCb.addEventListener("change", function() { + if (currentConfig) currentConfig.enabled = enabledCb.checked; markBgdDirty(); + renderBookmarkChecklist(currentFilterText()); }); } const selectAllBtn = document.getElementById("bgd-select-all-btn"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index ec936b08..1e1b49dd 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -1467,30 +1467,31 @@ SPDX-License-Identifier: GPL-2.0-or-later