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