From a8de39de8415c1591907f655ebec5d571430f4fe Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 7 Apr 2026 07:35:40 -0700 Subject: [PATCH] Fix OBS display and Start button not working Three issues fixed: 1. Port mismatch: The sidecar reported the OBS port (8080) in the ready event but the frontend needs the API port (8081). Now reports the API port so WebSocket/REST connects to the right place. 2. Broadcast from wrong thread: Engine init fires state_changed from a background thread, but _broadcast_control used get_event_loop() which returns the wrong loop. Now captures the uvicorn event loop at startup via on_event("startup"). 3. Missed ready state: If the engine finishes before the WebSocket client connects, the "ready" state_changed was never received. Added status polling (GET /api/status) on WebSocket connect that retries every 2s while appState is "initializing". Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/api_server.py | 14 ++++++++++++- backend/main_headless.py | 15 ++++++++----- src/lib/stores/backend.svelte.ts | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/backend/api_server.py b/backend/api_server.py index 5a40452..23a5867 100644 --- a/backend/api_server.py +++ b/backend/api_server.py @@ -99,11 +99,19 @@ class APIServer: self.controller.on_credits_low = on_credits_low + def set_event_loop(self, loop: asyncio.AbstractEventLoop): + """Set the event loop used for broadcasting (call from uvicorn startup).""" + self._event_loop = loop + def _broadcast_control(self, data: dict): """Send a message to all connected /ws/control clients.""" if not self.control_connections: return + loop = getattr(self, '_event_loop', None) + if loop is None: + return + message = json.dumps(data) disconnected = [] @@ -111,7 +119,7 @@ class APIServer: try: asyncio.run_coroutine_threadsafe( ws.send_text(message), - asyncio.get_event_loop(), + loop, ) except Exception: disconnected.append(ws) @@ -124,6 +132,10 @@ class APIServer: app = self.app ctrl = self.controller + @app.on_event("startup") + async def on_startup(): + self.set_event_loop(asyncio.get_event_loop()) + # ── Status ───────────────────────────────────────────── @app.get("/api/status") diff --git a/backend/main_headless.py b/backend/main_headless.py index e9fc80a..4be921e 100644 --- a/backend/main_headless.py +++ b/backend/main_headless.py @@ -88,11 +88,16 @@ def main(): # Create API server wrapping the controller api_server = APIServer(controller) - # Determine actual port (web server may have shifted if port was in use) - actual_port = controller.actual_web_port or args.port + # OBS display runs on the configured port, API server on port+1 + obs_port = controller.actual_web_port or args.port + api_port = obs_port + 1 - # Print ready event so Tauri can discover the port - print(json.dumps({"event": "ready", "port": actual_port}), flush=True) + # Print ready event so Tauri can discover the API port + print(json.dumps({ + "event": "ready", + "port": api_port, + "obs_port": obs_port, + }), flush=True) # Run the API server (blocks) import uvicorn @@ -104,7 +109,7 @@ def main(): uvicorn.run( api_server.app, host=args.host, - port=actual_port + 1, # API on port+1, OBS display on the main port + port=api_port, log_level="error", access_log=False, ) diff --git a/src/lib/stores/backend.svelte.ts b/src/lib/stores/backend.svelte.ts index 7c1d074..4939b0b 100644 --- a/src/lib/stores/backend.svelte.ts +++ b/src/lib/stores/backend.svelte.ts @@ -54,6 +54,35 @@ async function apiFetch(path: string, options?: RequestInit): Promise return fetch(url, { ...options, headers }); } +// ── Status polling ────────────────────────────────────────────────── + +let statusPollTimer: ReturnType | null = null; + +async function pollStatus() { + try { + const resp = await fetch(apiUrl("/api/status")); + if (resp.ok) { + const data = await resp.json(); + if (data.state) { + state.appState = data.state as AppState; + } + if (data.engine_device) { + state.deviceInfo = data.engine_device; + } + if (data.version) { + state.version = data.version; + } + } + } catch { + // API not ready yet, will retry + } + + // Keep polling every 2s while still initializing + if (state.appState === "initializing" && state.connectionState === "connected") { + statusPollTimer = setTimeout(pollStatus, 2000); + } +} + // ── WebSocket management ───────────────────────────────────────────── function connectWebSocket() { @@ -80,6 +109,9 @@ function _openSocket() { clearTimeout(reconnectTimer); reconnectTimer = null; } + // Poll status to catch engine ready state that may have been + // missed (engine can finish before WebSocket connects) + pollStatus(); }; ws.onmessage = (event) => { @@ -132,6 +164,10 @@ function _scheduleReconnect() { } function disconnect() { + if (statusPollTimer) { + clearTimeout(statusPollTimer); + statusPollTimer = null; + } if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null;