diff --git a/app/src/App.tsx b/app/src/App.tsx index b6964db..ff913cd 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,6 +4,7 @@ import voiceboxLogo from '@/assets/voicebox-logo.png'; import ShinyText from '@/components/ShinyText'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { useAutoUpdater } from '@/hooks/useAutoUpdater'; +import { apiClient } from '@/lib/api/client'; import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui'; import { cn } from '@/lib/utils/cn'; import { usePlatform } from '@/platform/PlatformContext'; @@ -122,6 +123,24 @@ function App() { serverStartingRef.current = false; // @ts-expect-error - adding property to window window.__voiceboxServerStartedByApp = false; + + // Fall back to polling: the server may already be running externally + // (e.g. started via python/uvicorn/Docker). Poll the health endpoint + // until it responds, then transition to the main UI. + console.log('Falling back to health-check polling...'); + const pollInterval = setInterval(async () => { + try { + await apiClient.getHealth(); + console.log('External server detected via health check'); + clearInterval(pollInterval); + setServerReady(true); + } catch { + // Server not ready yet, keep polling + } + }, 2000); + + // Stop polling after 2 minutes to avoid polling forever + setTimeout(() => clearInterval(pollInterval), 120_000); }); // Cleanup: stop server on actual unmount (not StrictMode remount) diff --git a/app/src/main.tsx b/app/src/main.tsx index e4a5e48..d6cb902 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; -const queryClient = new QueryClient({ +export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 minutes diff --git a/app/src/stores/serverStore.ts b/app/src/stores/serverStore.ts index 8f98304..9f843f4 100644 --- a/app/src/stores/serverStore.ts +++ b/app/src/stores/serverStore.ts @@ -30,11 +30,29 @@ interface ServerStore { setCustomModelsDir: (dir: string | null) => void; } +/** + * Invalidate all React Query caches and reset UI selection state. + * Called when the server URL changes so stale data from the previous + * server is not shown. + */ +function invalidateAllServerData() { + // Lazy import to avoid circular dependency (main.tsx -> serverStore -> main.tsx) + import('@/main').then(({ queryClient }) => { + queryClient.invalidateQueries(); + }); +} + export const useServerStore = create()( persist( - (set) => ({ + (set, get) => ({ serverUrl: 'http://127.0.0.1:17493', - setServerUrl: (url) => set({ serverUrl: url }), + setServerUrl: (url) => { + const prev = get().serverUrl; + set({ serverUrl: url }); + if (url !== prev) { + invalidateAllServerData(); + } + }, isConnected: false, setIsConnected: (connected) => set({ isConnected: connected }), diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 415961f..cfdb829 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -53,6 +53,35 @@ fn find_voicebox_pid_on_port(port: u16) -> Option { None } +/// Check if a Voicebox server is responding on the given port. +/// +/// Sends an HTTP GET to `/health` and returns `true` if the response +/// contains the expected JSON field (`"status"`), confirming it's +/// a Voicebox backend rather than an unrelated service. +#[allow(dead_code)] // Used in platform-specific cfg blocks +fn check_health(port: u16) -> bool { + let url = format!("http://127.0.0.1:{}/health", port); + match reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(3)) + .build() + { + Ok(client) => match client.get(&url).send() { + Ok(resp) => { + if !resp.status().is_success() { + return false; + } + // Verify the body looks like a Voicebox health response + match resp.text() { + Ok(body) => body.contains("status"), + Err(_) => false, + } + } + Err(_) => false, + }, + Err(_) => false, + } +} + struct ServerState { child: Mutex>, server_pid: Mutex>, @@ -80,7 +109,8 @@ async fn start_server( return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); } - // Check if a voicebox server is already running on our port (from previous session with keep_running=true) + // Check if a voicebox server is already running on our port (from previous session with keep_running=true, + // or an externally started server e.g. via `python`, `uvicorn`, Docker, etc.) #[cfg(unix)] { use std::process::Command; @@ -101,6 +131,20 @@ async fn start_server( *state.server_pid.lock().unwrap() = Some(pid); return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); } + } else { + // Process name doesn't contain "voicebox" — could be an external + // Python/uvicorn/Docker server. Verify via HTTP health check. + println!("Port {} in use by '{}' (PID: {}), checking if it's a Voicebox server...", SERVER_PORT, command, pid_str); + if check_health(SERVER_PORT) { + println!("Health check passed — reusing external server on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + println!("Health check failed — port is occupied by a non-Voicebox process"); + return Err(format!( + "Port {} is already in use by another application ({}). \ + Close it or change the Voicebox server port.", + SERVER_PORT, command + )); } } } @@ -114,18 +158,24 @@ async fn start_server( &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), std::time::Duration::from_secs(1), ).is_ok() { - // Port is in use — check if it's a voicebox process + // Port is in use — check if it's a voicebox process by name first if let Some(pid) = find_voicebox_pid_on_port(SERVER_PORT) { println!("Found existing voicebox-server on port {} (PID: {}), reusing it", SERVER_PORT, pid); *state.server_pid.lock().unwrap() = Some(pid); return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); - } else { - return Err(format!( - "Port {} is already in use by another application. \ - Close the other application or change the Voicebox port.", - SERVER_PORT - )); } + // Process name doesn't match — could be an external Python/Docker server. + // Verify via HTTP health check before giving up. + println!("Port {} in use by unknown process, checking if it's a Voicebox server...", SERVER_PORT); + if check_health(SERVER_PORT) { + println!("Health check passed — reusing external server on port {}", SERVER_PORT); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); + } + return Err(format!( + "Port {} is already in use by another application. \ + Close the other application or change the Voicebox port.", + SERVER_PORT + )); } }