From d393b3cefcf2cafcadb8099a9f3bb82b4df90b40 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:11:47 +0200 Subject: [PATCH 1/9] feat(ui): drive workspaces from rig capabilities --- .../trx-frontend-http/assets/web/app.js | 29 +++++++++++++++---- .../trx-frontend-http/assets/web/ui-core.js | 21 ++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 7114fd45..7f435438 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -23,6 +23,7 @@ window.onDecoderRegistryReady = function (fn) { for (const fn of _decoderRegistryReadyCallbacks) fn(); _decoderRegistryReadyCallbacks.length = 0; hideUnsupportedDecoderTabs(); + refreshOperatorLayoutCapabilities(); } } catch (e) { console.error("Failed to fetch decoder registry:", e); @@ -333,8 +334,14 @@ function applyCapabilities(caps) { const txAudioBtn = document.getElementById("tx-audio-btn"); const txVolSlider = document.getElementById("tx-vol"); const txVolControl = txVolSlider ? txVolSlider.closest(".vol-label") : null; - if (txPowerCol) txPowerCol.style.display = caps.tx ? "" : "none"; + if (txPowerCol) { + txPowerCol.style.display = ""; + const label = txPowerCol.querySelector(".label span"); + if (label) label.textContent = caps.tx ? "Transmit / Power" : "Power / Tuning"; + } if (pttBtn) pttBtn.style.display = caps.tx ? "" : "none"; + if (powerBtn) powerBtn.style.display = ""; + if (lockBtn) lockBtn.style.display = caps.lockable ? "" : "none"; if (txMetersRow) txMetersRow.style.display = caps.tx ? "" : "none"; if (txAudioBtn) txAudioBtn.style.display = caps.tx ? "" : "none"; if (txVolControl) txVolControl.style.display = caps.tx ? "" : "none"; @@ -344,7 +351,7 @@ function applyCapabilities(caps) { // TX limit row const txLimitRow = document.getElementById("tx-limit-row"); - if (txLimitRow && !caps.tx_limit) txLimitRow.style.display = "none"; + if (txLimitRow) txLimitRow.style.display = caps.tx_limit ? "" : "none"; // VFO row const vfoRow = document.getElementById("vfo-row"); @@ -1305,10 +1312,7 @@ async function refreshRigList() { } }); serverRigs = rigs; - window.trxUi?.setBroadcastLayoutAvailable(rigs.some((rig) => - Array.isArray(rig?.supported_modes) - && rig.supported_modes.map(normalizeMode).includes("WFM") - )); + refreshOperatorLayoutCapabilities(); serverActiveRigId = data.active_remote || null; applyRigList(data.active_remote, rigIds, displayNames); window.trx.modules.map?.syncAprsReceiverMarker(); @@ -1317,6 +1321,19 @@ async function refreshRigList() { } } +function refreshOperatorLayoutCapabilities() { + const rigModes = serverRigs.map((rig) => + Array.isArray(rig?.supported_modes) ? rig.supported_modes.map(normalizeMode).filter(Boolean) : [] + ); + const decoderModes = new Set(decoderRegistry.flatMap((decoder) => + Array.isArray(decoder?.active_modes) ? decoder.active_modes.map(normalizeMode).filter(Boolean) : [] + )); + window.trxUi?.setLayoutCapabilities({ + broadcast: rigModes.some((modes) => modes.includes("WFM")), + digital: decoderModes.size > 0 && rigModes.some((modes) => modes.some((mode) => decoderModes.has(mode))), + }); +} + function showHint(msg, duration) { powerHint.textContent = msg; if (hintTimer) clearTimeout(hintTimer); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js index fb72d6cd..f29ebb73 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js @@ -128,13 +128,13 @@ const layouts = { compact: { label: "Compact", advanced: false, preferredTab: "main" }, broadcast: { label: "Broadcast", advanced: false, preferredTab: "main", capability: "broadcast" }, - digital: { label: "Digital", advanced: false, preferredTab: "digital-modes" }, + digital: { label: "Digital", advanced: false, preferredTab: "digital-modes", capability: "digital" }, full: { label: "Full controls", advanced: true, preferredTab: "main" }, }; - let hasBroadcastRig = false; + const layoutCapabilities = { broadcast: false, digital: false }; - function broadcastLayoutAvailable() { - return hasBroadcastRig; + function layoutAvailable(layout) { + return !layout.capability || layoutCapabilities[layout.capability] === true; } function refreshLayoutOptions() { @@ -143,7 +143,7 @@ const previous = select.value || document.body.dataset.operatorLayout || "compact"; select.replaceChildren(); Object.entries(layouts).forEach(([value, layout]) => { - if (layout.capability === "broadcast" && !broadcastLayoutAvailable()) return; + if (!layoutAvailable(layout)) return; select.add(new Option(layout.label, value)); }); const available = Array.from(select.options).some(option => option.value === previous); @@ -151,14 +151,17 @@ if (!available && previous === "broadcast") api.applyLayout("compact"); } - api.setBroadcastLayoutAvailable = function setBroadcastLayoutAvailable(available) { - hasBroadcastRig = Boolean(available); + api.setLayoutCapabilities = function setLayoutCapabilities(capabilities = {}) { + Object.keys(layoutCapabilities).forEach((name) => { + if (name in capabilities) layoutCapabilities[name] = Boolean(capabilities[name]); + }); refreshLayoutOptions(); }; api.applyLayout = function applyLayout(name, options = {}) { - const permittedName = name === "broadcast" && !broadcastLayoutAvailable() ? "compact" : name; - const layout = layouts[permittedName] || layouts.compact; + const requestedLayout = layouts[name]; + const permittedName = requestedLayout && layoutAvailable(requestedLayout) ? name : "compact"; + const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; localStorage.setItem("trxOperatorLayout", document.body.dataset.operatorLayout); const details = document.getElementById("advanced-radio-controls"); -- 2.55.0 From 9fbf90ee91a1295e84c2fd8fb46d35ec4c1602a1 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:12:33 +0200 Subject: [PATCH 2/9] feat(ui): explain operator workspace choices --- .../trx-frontend-http/assets/web/style.css | 33 ++++++++++++++++++- .../trx-frontend-http/assets/web/ui-core.js | 12 ++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 05c7bb73..0332ef8d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -2949,7 +2949,8 @@ button.is-active { font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; - overflow: hidden; + overflow: visible; + position: relative; } .operator-layout-picker::before { content: "Layout"; } .operator-layout-picker select { @@ -2967,6 +2968,36 @@ button.is-active { } .operator-layout-picker:focus-within { border-color: var(--accent-green); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-green) 20%, transparent); } .operator-layout-picker select:focus-visible { outline: 0; } +.operator-layout-description { + position: absolute; + top: calc(100% + 0.35rem); + left: 0; + z-index: 25; + width: max-content; + max-width: min(22rem, calc(100vw - 2rem)); + padding: 0.35rem 0.55rem; + border: 1px solid var(--border-light); + border-radius: var(--radius-sm); + background: var(--card-bg); + color: var(--text-muted); + box-shadow: 0 8px 24px color-mix(in srgb, #000 28%, transparent); + font-size: var(--fs-xs); + font-weight: 500; + letter-spacing: 0; + line-height: 1.35; + text-transform: none; + opacity: 0; + transform: translateY(-0.2rem); + visibility: hidden; + pointer-events: none; + transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility var(--dur-fast); +} +.operator-layout-picker:hover .operator-layout-description, +.operator-layout-picker:focus-within .operator-layout-description { + opacity: 1; + transform: none; + visibility: visible; +} .advanced-radio-controls { margin-top: var(--space-3); border: 1px solid color-mix(in srgb, var(--border-light) 75%, transparent); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js index f29ebb73..1bfe06f9 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js @@ -126,10 +126,10 @@ }; const layouts = { - compact: { label: "Compact", advanced: false, preferredTab: "main" }, - broadcast: { label: "Broadcast", advanced: false, preferredTab: "main", capability: "broadcast" }, - digital: { label: "Digital", advanced: false, preferredTab: "digital-modes", capability: "digital" }, - full: { label: "Full controls", advanced: true, preferredTab: "main" }, + compact: { label: "Compact", description: "Essential tuning and audio controls", advanced: false, preferredTab: "main" }, + broadcast: { label: "Broadcast", description: "WFM, RDS, stereo and interference monitoring", advanced: false, preferredTab: "main", capability: "broadcast" }, + digital: { label: "Digital", description: "Decoder status, messages and background decoding", advanced: false, preferredTab: "digital-modes", capability: "digital" }, + full: { label: "Full controls", description: "Every control supported by the selected rig", advanced: true, preferredTab: "main" }, }; const layoutCapabilities = { broadcast: false, digital: false }; @@ -166,6 +166,8 @@ localStorage.setItem("trxOperatorLayout", document.body.dataset.operatorLayout); const details = document.getElementById("advanced-radio-controls"); if (details) details.open = layout.advanced; + const description = document.getElementById("operator-layout-description"); + if (description) description.textContent = layout.description; if (options.navigate && typeof window.navigateToTab === "function") { window.navigateToTab(layout.preferredTab); } @@ -176,7 +178,7 @@ if (actions && !document.getElementById("operator-layout-select")) { const label = document.createElement("label"); label.className = "operator-layout-picker"; - label.innerHTML = 'Operator layout'; + label.innerHTML = 'Operator layout'; const select = label.querySelector("select"); const savedLayout = localStorage.getItem("trxOperatorLayout") || "compact"; actions.insertBefore(label, actions.firstChild); -- 2.55.0 From d07a627508ba157d05a89de8f51c85d1614d906b Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:13:21 +0200 Subject: [PATCH 3/9] feat(ui): remember workspaces per rig --- .../trx-frontend-http/assets/web/app.js | 1 + .../trx-frontend-http/assets/web/ui-core.js | 31 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 7f435438..1d101548 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -1282,6 +1282,7 @@ function applyRigList(activeRigId, rigIds, displayNames) { const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx"; populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch); updateRigSubtitle(lastActiveRigId); + window.trxUi?.setActiveRig(lastActiveRigId); if (rigListChanged) { if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js index 1bfe06f9..2e9792dc 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/ui-core.js @@ -132,6 +132,15 @@ full: { label: "Full controls", description: "Every control supported by the selected rig", advanced: true, preferredTab: "main" }, }; const layoutCapabilities = { broadcast: false, digital: false }; + let activeRigId = null; + + function layoutStorageKey() { + return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout"; + } + + function savedLayoutName() { + return localStorage.getItem(layoutStorageKey()) || localStorage.getItem("trxOperatorLayout") || "compact"; + } function layoutAvailable(layout) { return !layout.capability || layoutCapabilities[layout.capability] === true; @@ -148,7 +157,7 @@ }); const available = Array.from(select.options).some(option => option.value === previous); select.value = available ? previous : "compact"; - if (!available && previous === "broadcast") api.applyLayout("compact"); + if (!available && previous !== "compact") api.applyLayout("compact", { persist: false }); } api.setLayoutCapabilities = function setLayoutCapabilities(capabilities = {}) { @@ -156,6 +165,20 @@ if (name in capabilities) layoutCapabilities[name] = Boolean(capabilities[name]); }); refreshLayoutOptions(); + const select = document.getElementById("operator-layout-select"); + const saved = savedLayoutName(); + if (select && Array.from(select.options).some(option => option.value === saved)) { + select.value = saved; + api.applyLayout(saved, { persist: false }); + } + }; + + api.setActiveRig = function setActiveRig(rigId) { + activeRigId = typeof rigId === "string" && rigId ? rigId : null; + const saved = savedLayoutName(); + const select = document.getElementById("operator-layout-select"); + if (select) select.value = Array.from(select.options).some(option => option.value === saved) ? saved : "compact"; + api.applyLayout(select?.value || saved, { persist: false }); }; api.applyLayout = function applyLayout(name, options = {}) { @@ -163,7 +186,7 @@ const permittedName = requestedLayout && layoutAvailable(requestedLayout) ? name : "compact"; const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; - localStorage.setItem("trxOperatorLayout", document.body.dataset.operatorLayout); + if (options.persist !== false) localStorage.setItem(layoutStorageKey(), document.body.dataset.operatorLayout); const details = document.getElementById("advanced-radio-controls"); if (details) details.open = layout.advanced; const description = document.getElementById("operator-layout-description"); @@ -180,7 +203,7 @@ label.className = "operator-layout-picker"; label.innerHTML = 'Operator layout'; const select = label.querySelector("select"); - const savedLayout = localStorage.getItem("trxOperatorLayout") || "compact"; + const savedLayout = savedLayoutName(); actions.insertBefore(label, actions.firstChild); select.value = savedLayout; refreshLayoutOptions(); @@ -201,7 +224,7 @@ if (element) body.appendChild(element); }); tray.appendChild(details); - api.applyLayout(localStorage.getItem("trxOperatorLayout") || "compact"); + api.applyLayout(savedLayoutName(), { persist: false }); } } -- 2.55.0 From d07dbdc6458fdea9e0bcfc9dbdb82f658debc662 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:14:10 +0200 Subject: [PATCH 4/9] feat(ui): focus broadcast workspace on reception --- .../trx-frontend-http/assets/web/style.css | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 0332ef8d..90f1a2ef 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -3034,6 +3034,50 @@ button.is-active { .advanced-radio-body { display: grid; gap: var(--space-3); padding: var(--space-3); } .advanced-radio-body > * { margin: 0; } body[data-operator-layout="digital"] .controls-col-wfm { display: none !important; } +body[data-operator-layout="broadcast"] #tx-power-col, +body[data-operator-layout="broadcast"] #tx-meters, +body[data-operator-layout="broadcast"] #tx-limit-row, +body[data-operator-layout="broadcast"] #vfo-row, +body[data-operator-layout="broadcast"] #sam-controls-col, +body[data-operator-layout="broadcast"] .advanced-radio-controls { + display: none !important; +} +body[data-operator-layout="broadcast"] .controls-row { + grid-template-columns: minmax(8rem, 0.65fr) auto minmax(20rem, 2fr); +} +body[data-operator-layout="broadcast"] #wfm-controls-col, +body[data-operator-layout="broadcast"] #audio-row, +body[data-operator-layout="broadcast"] #spectrum-bw-row { + border-color: color-mix(in srgb, var(--accent-green) 42%, var(--border-light)); + background: color-mix(in srgb, var(--accent-green) 7%, transparent); +} +body[data-operator-layout="broadcast"] #wfm-controls-col { + padding: var(--space-2) var(--space-3); + border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light)); + border-radius: var(--radius-md); +} +body[data-operator-layout="broadcast"] #audio-row { + padding: var(--space-3); + border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light)); + border-radius: var(--radius-md); +} +body[data-operator-layout="broadcast"] #spectrum-bw-row { + padding: var(--space-2); + border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light)); + border-radius: var(--radius-sm); +} +body[data-operator-layout="broadcast"] #ais-bar-overlay, +body[data-operator-layout="broadcast"] #vdes-bar-overlay, +body[data-operator-layout="broadcast"] #ft8-bar-overlay, +body[data-operator-layout="broadcast"] #aprs-bar-overlay, +body[data-operator-layout="broadcast"] #hf-aprs-bar-overlay, +body[data-operator-layout="broadcast"] #cw-bar-overlay { + display: none !important; +} +@media (max-width: 760px) { + body[data-operator-layout="broadcast"] .controls-row { grid-template-columns: 1fr auto; } + body[data-operator-layout="broadcast"] #wfm-controls-col { grid-column: 1 / -1; } +} .mobile-more-btn, .mobile-more-menu, .decoder-tab-select { display: none; } .mobile-more-menu { position: fixed; -- 2.55.0 From af744305515bea8b42b60045329fca5de5cb94bf Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:15:39 +0200 Subject: [PATCH 5/9] fix(ui): make rig switching atomic and visible --- .../trx-frontend-http/assets/web/app.js | 84 +++++++++++++------ .../trx-frontend-http/assets/web/index.html | 1 + .../trx-frontend-http/assets/web/style.css | 27 ++++++ .../trx-frontend-http/src/api/rig.rs | 4 + 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 1d101548..1d8d0e12 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -443,6 +443,7 @@ const signalSplitValueEl = document.getElementById("signal-split-value"); const overviewPeakHoldEl = document.getElementById("overview-peak-hold"); const themeToggleBtn = document.getElementById("theme-toggle"); const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"); +const headerRigSummary = document.getElementById("header-rig-summary"); const headerStylePickSelect = document.getElementById("header-style-pick-select"); const rdsPsOverlay = document.getElementById("rds-ps-overlay"); const tabMainEl = document.getElementById("tab-main"); @@ -902,6 +903,7 @@ async function restorePreviousTuneState() { let lastRigIds = []; let lastRigDisplayNames = {}; let lastActiveRigId = null; +let rigSwitchInProgress = false; let lastCityLabel = ""; let sseSessionId = null; const originalTitle = document.title; @@ -1246,6 +1248,20 @@ function populateRigPicker(selectEl, rigIds, activeRigId, disabled) { selectEl.disabled = disabled; } +function updateRigIdentitySummary(rigId, pending = false) { + if (!headerRigSummary) return; + const rig = serverRigs.find((entry) => entry?.remote === rigId); + if (!rig) { + headerRigSummary.textContent = pending ? "Switching rigs…" : "No rig details available"; + return; + } + const hardware = [rig.manufacturer, rig.model].map(value => String(value || "").trim()).filter(Boolean).join(" ") || rig.remote; + const modes = Array.isArray(rig.supported_modes) ? rig.supported_modes.map(normalizeMode).filter(Boolean) : []; + const features = [rig.tx ? "TX" : "RX", rig.filter_controls ? "SDR filters" : null, ...modes.slice(0, 5)]; + if (modes.length > 5) features.push(`+${modes.length - 5} modes`); + headerRigSummary.textContent = `${pending ? "Switching to " : ""}${hardware} · ${features.filter(Boolean).join(" · ")}`; +} + function updateRigSubtitle(activeRigId) { if (!rigSubtitle) return; const name = (activeRigId && lastRigDisplayNames[activeRigId]) || activeRigId || "--"; @@ -1282,6 +1298,7 @@ function applyRigList(activeRigId, rigIds, displayNames) { const disableSwitch = lastRigIds.length === 0 || !authRole || authRole === "rx"; populateRigPicker(headerRigSwitchSelect, lastRigIds, lastActiveRigId, disableSwitch); updateRigSubtitle(lastActiveRigId); + updateRigIdentitySummary(lastActiveRigId); window.trxUi?.setActiveRig(lastActiveRigId); if (rigListChanged) { if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); @@ -3881,12 +3898,16 @@ function scheduleUiFrameJob(key, job) { window.trxScheduleUiFrameJob = scheduleUiFrameJob; -async function postPath(path) { +async function postPath(path, options = {}) { + if (rigSwitchInProgress && !options.allowDuringRigSwitch) { + throw new Error("Wait for the rig switch to finish"); + } + const targetRigId = options.remote === undefined ? lastActiveRigId : options.remote; // Auto-append remote so each tab targets its own rig. // Skip when the caller already included remote (e.g. /select_rig). - if (lastActiveRigId && !path.includes("remote=")) { + if (targetRigId && !path.includes("remote=")) { const sep = path.includes("?") ? "&" : "?"; - path = `${path}${sep}remote=${encodeURIComponent(lastActiveRigId)}`; + path = `${path}${sep}remote=${encodeURIComponent(targetRigId)}`; } const resp = await fetch(path, { method: "POST" }); if (authEnabled && resp.status === 401) { @@ -3931,36 +3952,45 @@ async function switchRigFromSelect(selectEl) { return; } const prevRig = lastActiveRigId; - lastActiveRigId = selectEl.value; - if (prevRig && prevRig !== lastActiveRigId) { - resetDecoderStateOnRigSwitch(); - } - updateRigSubtitle(lastActiveRigId); - if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); - if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId); - if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || ""); - window.trx.modules.map?.syncAprsReceiverMarker(); - // Switch this session's rig and reconnect SSE to the new rig's - // state channel. + const nextRig = selectEl.value; + if (nextRig === prevRig || rigSwitchInProgress) return; + rigSwitchInProgress = true; + setControlPending(selectEl, true); + selectEl.closest(".header-rig-switch")?.classList.add("is-switching"); + updateRigIdentitySummary(nextRig, true); + showHint(`Switching to ${lastRigDisplayNames[nextRig] || nextRig}…`); try { const sidParam = sseSessionId ? `&session_id=${encodeURIComponent(sseSessionId)}` : ""; - await postPath(`/select_rig?remote=${encodeURIComponent(selectEl.value)}${sidParam}`); + await postPath(`/select_rig?remote=${encodeURIComponent(nextRig)}${sidParam}`, { allowDuringRigSwitch: true, remote: null }); + lastActiveRigId = nextRig; + resetDecoderStateOnRigSwitch(); + updateRigSubtitle(lastActiveRigId); + updateRigIdentitySummary(lastActiveRigId); + window.trxUi?.setActiveRig(lastActiveRigId); + if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId); + if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId); + if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || ""); + window.trx.modules.map?.syncAprsReceiverMarker(); connect(); + stopSpectrumStreaming(); + startSpectrumStreaming(); + stopMeterStreaming(); + startMeterStreaming(); + if (rxActive) { + stopRxAudio(); + startRxAudio(); + } + showHint(`Rig: ${lastRigDisplayNames[lastActiveRigId] || lastActiveRigId}`, 1500); } catch (err) { console.error("select_rig failed:", err); + selectEl.value = prevRig || ""; + updateRigIdentitySummary(prevRig); + window.trxUi?.notify("Rig could not be switched", { kind: "error" }); + } finally { + rigSwitchInProgress = false; + setControlPending(selectEl, false); + selectEl.closest(".header-rig-switch")?.classList.remove("is-switching"); } - // Reconnect spectrum SSE to the new rig's spectrum channel. - stopSpectrumStreaming(); - startSpectrumStreaming(); - // Reconnect meter SSE to the new rig's meter channel. - stopMeterStreaming(); - startMeterStreaming(); - // Reconnect audio to the new rig if audio is active. - if (rxActive) { - stopRxAudio(); - startRxAudio(); - } - showHint(`Rig: ${lastActiveRigId}`, 1500); } if (headerRigSwitchSelect) { 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 c349d3fe..05f18654 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 @@ -85,6 +85,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
+