From b31790ff487d4658d517789782253c105f1c0b28 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 17:46:56 +0200 Subject: [PATCH] [fix](trx-frontend-http): keep layout sections togglable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A layout seeds the collapsible sections; it should not hold them there. applyLayout writes the disclosure state of the advanced, audio and scheduler sections, and it runs far more often than a layout change: render() calls applyRigList() for every SSE frame carrying `remotes`, which calls setActiveRig() unconditionally, which re-applies the layout. An operator who expanded a section that the selected layout collapses by default therefore had it shut again within about a second, which read as the section being locked by the layout — most visibly the scheduler under Compact. Write the section state only when the layout actually changes, or the first time each section appears in the DOM, since the advanced controls are constructed after the first applyLayout call. Switching layout still reseeds every section, so choosing a layout keeps meaning "give me these defaults". Verified in Chromium: with Compact selected, activating the scheduler summary opens the section and it survives both a rig-state refresh and a repeated applyLayout, while selecting Full still reseeds it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- .../assets/web/generated/app.js | 25 ++++++++++---- .../trx-frontend-http/frontend/src/ui-core.ts | 34 +++++++++++++++---- .../frontend/tests/ui-core.test.mjs | 27 +++++++++++++++ 3 files changed, 74 insertions(+), 12 deletions(-) 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 a380fbc3..98c621c6 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 @@ -637,6 +637,22 @@ function elementById(id) { }; const layoutCapabilities = { broadcast: false, digital: false }; let activeRigId = null; + const layoutSections = [ + { id: "advanced-radio-controls", key: "advanced" }, + { id: "audio-controls", key: "audio" }, + { id: "scheduler-controls", key: "scheduler" } + ]; + const seededSections = /* @__PURE__ */ new Set(); + let appliedLayoutName = null; + function seedLayoutSections(layout, layoutChanged) { + layoutSections.forEach(({ id, key }) => { + const section = document.getElementById(id); + if (!section) return; + if (!layoutChanged && seededSections.has(id)) return; + seededSections.add(id); + section.open = layout[key]; + }); + } function layoutStorageKey() { return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout"; } @@ -690,12 +706,9 @@ function elementById(id) { const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName); - const details = document.getElementById("advanced-radio-controls"); - if (details) details.open = layout.advanced; - const audioDetails = document.getElementById("audio-controls"); - if (audioDetails) audioDetails.open = layout.audio; - const schedulerDetails = document.getElementById("scheduler-controls"); - if (schedulerDetails) schedulerDetails.open = layout.scheduler; + const layoutChanged = appliedLayoutName !== permittedName; + appliedLayoutName = permittedName; + seedLayoutSections(layout, layoutChanged); if (options.navigate && typeof browserWindow.navigateToTab === "function") { browserWindow.navigateToTab(layout.preferredTab); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts index 899e13ef..5b660105 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts @@ -195,6 +195,31 @@ function elementById(id: string): T { const layoutCapabilities: Record = { broadcast: false, digital: false }; let activeRigId: string | null = null; + // A layout seeds the collapsible sections; it does not hold them there. + // applyLayout runs on every rig-state refresh, so re-applying the disclosure + // state unconditionally would reopen or close sections under the operator + // once a second — the reason a section could not be expanded while a layout + // that collapses it was selected. Sections are therefore written only when + // the layout actually changes, or the first time each one appears in the DOM + // (the advanced controls are built after the first applyLayout call). + const layoutSections: { id: string; key: "advanced" | "audio" | "scheduler" }[] = [ + { id: "advanced-radio-controls", key: "advanced" }, + { id: "audio-controls", key: "audio" }, + { id: "scheduler-controls", key: "scheduler" }, + ]; + const seededSections = new Set(); + let appliedLayoutName: LayoutName | null = null; + + function seedLayoutSections(layout: OperatorLayout, layoutChanged: boolean) { + layoutSections.forEach(({ id, key }) => { + const section = document.getElementById(id) as HTMLDetailsElement | null; + if (!section) return; + if (!layoutChanged && seededSections.has(id)) return; + seededSections.add(id); + section.open = layout[key]; + }); + } + function layoutStorageKey() { return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout"; } @@ -255,12 +280,9 @@ function elementById(id: string): T { const layout = layouts[permittedName]; document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact"; if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName); - const details = document.getElementById("advanced-radio-controls") as HTMLDetailsElement | null; - if (details) details.open = layout.advanced; - const audioDetails = document.getElementById("audio-controls") as HTMLDetailsElement | null; - if (audioDetails) audioDetails.open = layout.audio; - const schedulerDetails = document.getElementById("scheduler-controls") as HTMLDetailsElement | null; - if (schedulerDetails) schedulerDetails.open = layout.scheduler; + const layoutChanged = appliedLayoutName !== permittedName; + appliedLayoutName = permittedName; + seedLayoutSections(layout, layoutChanged); if (options.navigate && typeof browserWindow.navigateToTab === "function") { browserWindow.navigateToTab(layout.preferredTab); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs index 1828ba97..ecae4e7a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ui-core.test.mjs @@ -143,4 +143,31 @@ ui.applyLayout("digital"); assert.equal(document.body.dataset.operatorLayout, "compact"); assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact"); +// A layout seeds the collapsible sections but must not hold them there: +// applyLayout runs on every rig-state refresh, so an operator who expands a +// section a layout collapses by default had it shut again about once a second. +const schedulerSection = new Element("details", document); +schedulerSection.id = "scheduler-controls"; +const audioSection = new Element("details", document); +audioSection.id = "audio-controls"; + +ui.setActiveRig("rig-c"); +ui.applyLayout("compact"); +assert.equal(schedulerSection.open, false, "compact seeds the scheduler section closed"); + +schedulerSection.open = true; // operator expands it +ui.applyLayout("compact"); // the next rig-state refresh +assert.equal(schedulerSection.open, true, "re-applying the same layout leaves sections alone"); +ui.setActiveRig("rig-c"); +assert.equal(schedulerSection.open, true, "a rig-state refresh leaves sections alone"); + +// Choosing a different layout is an explicit request for its defaults. +ui.applyLayout("full"); +assert.equal(schedulerSection.open, true, "full seeds the scheduler section open"); +assert.equal(audioSection.open, true, "full seeds the audio section open"); +audioSection.open = false; +ui.applyLayout("compact"); +assert.equal(audioSection.open, false, "compact seeds the audio section closed"); +assert.equal(schedulerSection.open, false, "switching layout reseeds every section"); + console.log("ui-core component tests passed");