From 8c7f4e80089ab7679d660649153fa514316367f6 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 11 Apr 2026 18:55:43 -0700 Subject: [PATCH] Fix sidecar pipe crash on state changes and show logged-in state in settings The state callback in main_headless.py wrote events to stdout synchronously, so an EINVAL on the Tauri sidecar pipe (Windows) bubbled up through _set_state and tore down engine init and reload_engine. That turned PUT /api/config into a "Failed to fetch" for the user. The print is now pipe-safe and api_server isolates the chained callback so a future misbehaving listener cannot break the engine state machine. Settings also now persists remote.email on login and shows a "Logged in as " indicator with a Log out button when an auth_token is present, instead of leaving the email/password fields blank on reload. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/api_server.py | 10 +++- backend/main_headless.py | 10 +++- config/default_config.yaml | 1 + src/lib/components/Settings.svelte | 76 +++++++++++++++++++++--------- src/lib/stores/config.svelte.ts | 2 + 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/backend/api_server.py b/backend/api_server.py index 2107bdd..03e11f7 100644 --- a/backend/api_server.py +++ b/backend/api_server.py @@ -73,8 +73,15 @@ class APIServer: original_state_cb = self.controller.on_state_changed def on_state_changed(state: str, message: str): + # Isolate the upstream callback so a failure there (e.g. a + # broken stdout pipe in main_headless) cannot propagate into + # _set_state and tear down engine init / reload_engine / + # apply_settings request handling. if original_state_cb: - original_state_cb(state, message) + try: + original_state_cb(state, message) + except Exception: + pass self._broadcast_control({"type": "state_changed", "state": state, "message": message}) self.controller.on_state_changed = on_state_changed @@ -273,6 +280,7 @@ class APIServer: data = resp.json() ctrl.config.set('remote.auth_token', data.get('token', '')) ctrl.config.set('remote.server_url', req.server_url) + ctrl.config.set('remote.email', req.email) return {"status": "ok", "token": data.get('token', '')} else: raise HTTPException(status_code=resp.status_code, detail=resp.text) diff --git a/backend/main_headless.py b/backend/main_headless.py index 4be921e..666e1a7 100644 --- a/backend/main_headless.py +++ b/backend/main_headless.py @@ -75,10 +75,16 @@ def main(): # Create controller and initialize controller = AppController(config=config) - # Wire a state callback that prints the ready event + # Wire a state callback that prints state events for the parent + # process to read. Stdout writes can fail with EINVAL on Windows + # when the parent stops reading the sidecar pipe; swallow those + # so the engine state machine isn't taken down by a logging path. def on_state_changed(state, message): event = {"event": "state", "state": state, "message": message} - print(json.dumps(event), flush=True) + try: + print(json.dumps(event), flush=True) + except (OSError, ValueError): + pass controller.on_state_changed = on_state_changed diff --git a/config/default_config.yaml b/config/default_config.yaml index 6ab33eb..ce5b7f4 100644 --- a/config/default_config.yaml +++ b/config/default_config.yaml @@ -72,6 +72,7 @@ remote: mode: byok # local | managed | byok server_url: "https://transcribe.shadowdao.com" # Proxy server URL for managed mode auth_token: "" # JWT stored after login (managed mode) + email: "" # Email of the logged-in managed-mode account (for UI display) byok_api_key: "" # Deepgram API key for BYOK mode deepgram_model: nova-2 # Deepgram model to use language: en-US # Language code diff --git a/src/lib/components/Settings.svelte b/src/lib/components/Settings.svelte index d876310..83a0566 100644 --- a/src/lib/components/Settings.svelte +++ b/src/lib/components/Settings.svelte @@ -44,6 +44,7 @@ let byokApiKey = $state(""); let managedEmail = $state(""); let managedPassword = $state(""); + let managedLoggedIn = $state(false); let autoCheckUpdates = $state(true); let isCloudMode = $derived(remoteMode === "managed" || remoteMode === "byok"); @@ -131,6 +132,8 @@ remoteMode = cfg.remote.mode; remoteServerUrl = cfg.remote.server_url; byokApiKey = cfg.remote.byok_api_key ?? ""; + managedEmail = cfg.remote.email ?? ""; + managedLoggedIn = !!(cfg.remote.auth_token && cfg.remote.email); autoCheckUpdates = cfg.updates.auto_check; }); @@ -268,12 +271,29 @@ server_url: remoteServerUrl || MANAGED_SERVER_URL, }); loginMessage = "Logged in successfully!"; + managedPassword = ""; + managedLoggedIn = true; + await configStore.fetchConfig(); } catch (err) { console.error("Login failed:", err); loginMessage = "Login failed. Check your email and password."; } } + async function handleManagedLogout() { + try { + await configStore.updateConfig({ + remote: { auth_token: "", email: "" }, + }); + managedLoggedIn = false; + managedPassword = ""; + loginMessage = ""; + } catch (err) { + console.error("Logout failed:", err); + loginMessage = `Error: ${err}`; + } + } + const CAPTION_SERVER = "https://caption.shadowdao.com"; function generateRandomName(): string { @@ -489,34 +509,44 @@ {/if} {#if remoteMode === "managed"}
-
- - -
-
- - -
-
- -
+ {#if managedLoggedIn} +

+ ✓ Logged in + as {managedEmail} +

+
+ +
+ {:else} +
+ + +
+
+ + +
+
+ +
+

+ Don't have an account? Sign up here +

+ {/if} {#if loginMessage}

{loginMessage}

{/if} -

- Don't have an account? Sign up here -

{/if} diff --git a/src/lib/stores/config.svelte.ts b/src/lib/stores/config.svelte.ts index 0f70229..8abe075 100644 --- a/src/lib/stores/config.svelte.ts +++ b/src/lib/stores/config.svelte.ts @@ -65,6 +65,7 @@ export interface AppConfig { mode: string; server_url: string; auth_token: string; + email: string; byok_api_key: string; deepgram_model: string; language: string; @@ -131,6 +132,7 @@ function getDefaultConfig(): AppConfig { mode: "byok", server_url: "", auth_token: "", + email: "", byok_api_key: "", deepgram_model: "nova-2", language: "en-US",