Fix sidecar pipe crash on state changes and show logged-in state in settings
All checks were successful
Tests / Python Backend Tests (push) Successful in 14s
Tests / Frontend Tests (push) Successful in 8s
Tests / Rust Sidecar Tests (push) Successful in 2m0s

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 <email>" 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) <noreply@anthropic.com>
This commit is contained in:
Developer
2026-04-11 18:55:43 -07:00
parent b8d718caa6
commit 8c7f4e8008
5 changed files with 73 additions and 26 deletions

View File

@@ -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:
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)

View File

@@ -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}
try:
print(json.dumps(event), flush=True)
except (OSError, ValueError):
pass
controller.on_state_changed = on_state_changed

View File

@@ -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

View File

@@ -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,6 +509,15 @@
{/if}
{#if remoteMode === "managed"}
<div class="managed-auth">
{#if managedLoggedIn}
<p style="font-size: 13px; margin: 0 0 8px;">
<span style="color: var(--accent-green, #4CAF50);">✓ Logged in</span>
as <strong>{managedEmail}</strong>
</p>
<div class="auth-buttons">
<button onclick={handleManagedLogout}>Log out</button>
</div>
{:else}
<div class="field">
<label for="managed-email">Email</label>
<input
@@ -509,14 +538,15 @@
<div class="auth-buttons">
<button onclick={handleManagedLogin}>Login</button>
</div>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;">
Don't have an account? <a href="https://transcribe.shadowdao.com/register.html" target="_blank" rel="noopener" style="color: var(--accent-blue);">Sign up here</a>
</p>
{/if}
{#if loginMessage}
<p style="font-size: 12px; margin-top: 6px; color: {loginMessage.startsWith('Logged') ? 'var(--accent-green, #4CAF50)' : 'var(--accent-red, #f44336)'};">
{loginMessage}
</p>
{/if}
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;">
Don't have an account? <a href="https://transcribe.shadowdao.com/register.html" target="_blank" rel="noopener" style="color: var(--accent-blue);">Sign up here</a>
</p>
</div>
{/if}
</section>

View File

@@ -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",