From 62caa721870cd8f905271ad8a1e07e12400fed43 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Wed, 3 Jun 2026 01:34:04 +0100 Subject: [PATCH 1/3] Fix settings page submit/reset buttons never enabling on change (#1131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vue 2 does not make properties reactive when added to an already-observed object via direct assignment. editSettings was declared as {} in data(), so properties set in resetEditSettings() were invisible to the dependency tracker — hasChanges never re-evaluated and buttons stayed disabled. Fix: replace the object wholesale in resetEditSettings() so Vue observes all properties on the new object, and use this.$set() in resetForm() for the same reason. Co-authored-by: Claude Sonnet 4.6 --- client/src/vue_components/config/ConfigSettings.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/vue_components/config/ConfigSettings.vue b/client/src/vue_components/config/ConfigSettings.vue index 0caf58ba2..8a15d30bc 100644 --- a/client/src/vue_components/config/ConfigSettings.vue +++ b/client/src/vue_components/config/ConfigSettings.vue @@ -232,15 +232,17 @@ export default defineComponent({ } }, resetEditSettings(): void { + const newSettings: Record = {}; Object.keys(this.visibleSettings).forEach((x) => { - this.editSettings[x] = this.visibleSettings[x].value; + newSettings[x] = this.visibleSettings[x].value; }); + this.editSettings = newSettings; }, resetForm(toggleLoaded: boolean): void { if (toggleLoaded) this.loaded = false; this.toggle = this.toggle === 0 ? 1 : 0; Object.keys((this as any).RAW_SETTINGS).forEach((x) => { - this.editSettings[x] = (this as any).RAW_SETTINGS[x].value; + this.$set(this.editSettings, x, (this as any).RAW_SETTINGS[x].value); }); (this as any).$v.editSettings.$reset(); if (toggleLoaded) this.loaded = true; From f5de036cdc9261b0fc169d5f088607e3bcc9bf37 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Wed, 3 Jun 2026 01:54:43 +0100 Subject: [PATCH 2/3] Fix session start from show config Sessions tab (#1132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vue 3 SessionList.vue was sending an empty POST body to /api/v1/show/sessions/start, causing a JSONDecodeError on the server. The endpoint requires {"session_id": ""} to identify the initiating client, matching the pattern already used by the navbar. Also removes the unreachable Stop Session button from both the Vue 2 and Vue 3 SessionList components — the START_SHOW WebSocket message pushes all clients to /live the moment a session starts, making the button inaccessible. Stop Session remains available via the navbar. Adds E2E coverage in spec 12 for the sessions tab Start button, including cleanup via the navbar to leave clean state for spec 13. Co-authored-by: Claude Sonnet 4.6 --- .../e2e/tests/12-show-config-sessions.spec.ts | 26 +++++++++- .../show/config/sessions/SessionList.vue | 52 +++++++------------ .../show/config/sessions/SessionList.vue | 38 +++----------- 3 files changed, 50 insertions(+), 66 deletions(-) diff --git a/client-v3/e2e/tests/12-show-config-sessions.spec.ts b/client-v3/e2e/tests/12-show-config-sessions.spec.ts index 1b9a07dc6..0f0b2b9c0 100644 --- a/client-v3/e2e/tests/12-show-config-sessions.spec.ts +++ b/client-v3/e2e/tests/12-show-config-sessions.spec.ts @@ -1,6 +1,6 @@ /** - * Show Config — Sessions tab: session tags CRUD. - * Note: actual session start/stop is tested in 13-live-show.spec.ts. + * Show Config — Sessions tab: session start via the tab button + session tags CRUD. + * Spec 13 covers session start/stop via the navbar. */ import { test, expect, type BrowserContext, type Page } from '@playwright/test'; import { @@ -73,3 +73,25 @@ test('deletes a session tag', async () => { timeout: 5_000, }); }); + +// ── Session start via the tab button ───────────────────────────────────── + +test('switches back to Sessions sub-tab', async () => { + await page.click('button[role="tab"]:has-text("Sessions")'); + await expect(page.locator('button.btn-success:has-text("Start Session")')).toBeVisible({ + timeout: 10_000, + }); +}); + +test('starts a session via the Sessions tab Start button', async () => { + await page.click('button.btn-success:has-text("Start Session")'); + // START_SHOW WS message pushes all clients to /live + await page.waitForURL(`${UI_BASE}/live`, { timeout: 10_000 }); +}); + +test('stops the session via the navbar to restore state for later specs', async () => { + await page.locator('text=Live Config').click(); + await page.click('button:has-text("Stop Session")'); + await confirmDialog(page); + await expect(page.locator('a:has-text("Live")')).toHaveClass(/disabled/, { timeout: 10_000 }); +}); diff --git a/client-v3/src/components/show/config/sessions/SessionList.vue b/client-v3/src/components/show/config/sessions/SessionList.vue index fecc0de6e..f55ab2f3a 100644 --- a/client-v3/src/components/show/config/sessions/SessionList.vue +++ b/client-v3/src/components/show/config/sessions/SessionList.vue @@ -2,22 +2,13 @@ - - - Start Session - - - Stop Session - - + + Start Session + @@ -65,14 +56,15 @@ import log from 'loglevel'; import { makeURL, msToTimerString, contrastColor } from '@/js/utils'; import { useSystemStore } from '@/stores/system'; import { useShowStore } from '@/stores/show'; +import { useWebSocketStore } from '@/stores/websocket'; import { toast } from '@/js/toast'; import SessionTagDropdown from './SessionTagDropdown.vue'; const systemStore = useSystemStore(); const showStore = useShowStore(); +const wsStore = useWebSocketStore(); const startingSession = ref(false); -const stoppingSession = ref(false); const sessionFields = [ { key: 'start_date_time', label: 'Start Time' }, @@ -96,9 +88,17 @@ function revisionLabel(revisionId: number | null): string { } async function startSession(): Promise { + if (!wsStore.internalUUID) { + toast.error('Unable to start new show session'); + return; + } startingSession.value = true; try { - const response = await fetch(makeURL('/api/v1/show/sessions/start'), { method: 'POST' }); + const response = await fetch(makeURL('/api/v1/show/sessions/start'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: wsStore.internalUUID }), + }); if (response.ok) { toast.success('Started new show session'); await showStore.getShowSessionData(); @@ -110,22 +110,6 @@ async function startSession(): Promise { startingSession.value = false; } } - -async function stopSession(): Promise { - stoppingSession.value = true; - try { - const response = await fetch(makeURL('/api/v1/show/sessions/stop'), { method: 'POST' }); - if (response.ok) { - toast.success('Stopped show session'); - await showStore.getShowSessionData(); - } else { - log.error('Unable to stop show session'); - toast.error('Unable to stop show session'); - } - } finally { - stoppingSession.value = false; - } -}