From 3c8e1054ad1b7dabb9b3be8d565f0b58ebf9b3d6 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 27 Nov 2025 17:14:12 +0000 Subject: [PATCH] Attempt 4 --- frontend/src/desktop/hooks/useBackendProbe.ts | 169 +++++++++++------- 1 file changed, 103 insertions(+), 66 deletions(-) diff --git a/frontend/src/desktop/hooks/useBackendProbe.ts b/frontend/src/desktop/hooks/useBackendProbe.ts index deb7538eb..e4d497b85 100644 --- a/frontend/src/desktop/hooks/useBackendProbe.ts +++ b/frontend/src/desktop/hooks/useBackendProbe.ts @@ -1,48 +1,98 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { fetch } from '@tauri-apps/plugin-http'; -import { invoke } from '@tauri-apps/api/core'; import { connectionModeService } from '@app/services/connectionModeService'; -import { tauriBackendService } from '@app/services/tauriBackendService'; - -type BackendStatus = 'up' | 'starting' | 'down'; +import { tauriBackendService, type BackendStatus as ServiceBackendStatus } from '@app/services/tauriBackendService'; type ConnectionMode = 'saas' | 'selfhosted'; +type ProbeStatus = 'up' | 'starting' | 'down'; + interface BackendProbeState { - status: BackendStatus; + status: ProbeStatus; loginDisabled: boolean; loading: boolean; } -interface ProbeTarget { - baseUrl: string; - mode: ConnectionMode; -} - /** - * Desktop override of useBackendProbe. - * Hits the local/remote backend directly via the Tauri HTTP client. + * Desktop-specific backend probe. + * + * In SaaS mode we trust the embedded backend's health status exposed by tauriBackendService, + * avoiding redundant HTTP polling that was preventing the Login/Landing screens from advancing. + * In self-hosted mode we still hit the remote server directly to detect login-disabled state. */ export function useBackendProbe() { + const initialStatus = useMemo(() => mapServiceStatus(tauriBackendService.getBackendStatus()), []); const [state, setState] = useState({ - status: 'starting', + status: initialStatus, loginDisabled: false, loading: true, }); + const modeRef = useRef('saas'); + + // Track connection mode so the probe knows when to fall back to remote HTTP checks. + useEffect(() => { + let unsubscribe: (() => void) | null = null; + connectionModeService.getCurrentConfig() + .then((config) => { + modeRef.current = config.mode; + unsubscribe = connectionModeService.subscribeToModeChanges((nextConfig) => { + modeRef.current = nextConfig.mode; + }); + }) + .catch((error) => { + console.error('[Desktop useBackendProbe] Failed to load connection mode:', error); + }); + + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, []); + + // React to backend health updates coming from the shared tauriBackendService. + useEffect(() => { + const unsubscribe = tauriBackendService.subscribeToStatus((serviceStatus) => { + setState((prev) => ({ + ...prev, + status: mapServiceStatus(serviceStatus), + loading: false, + })); + }); + return () => { + unsubscribe(); + }; + }, []); const probe = useCallback(async () => { - const target = await resolveProbeBaseUrl(); - if (!target) { - const pendingState: BackendProbeState = { + const mode = modeRef.current; + const serviceStatus = tauriBackendService.getBackendStatus(); + + if (mode === 'saas') { + // SaaS desktop always relies on the embedded backend. Kick off a health check if needed. + if (serviceStatus !== 'healthy') { + void tauriBackendService.checkBackendHealth(); + } + const nextState: BackendProbeState = { + status: mapServiceStatus(serviceStatus), + loginDisabled: false, + loading: false, + }; + setState(nextState); + return nextState; + } + + const baseUrl = await resolveRemoteBaseUrl(); + if (!baseUrl) { + const pending: BackendProbeState = { status: 'starting', loginDisabled: false, loading: false, }; - setState(pendingState); - return pendingState; + setState(pending); + return pending; } - const { baseUrl, mode } = target; const statusUrl = `${baseUrl}/api/v1/info/status`; const loginUrl = `${baseUrl}/api/v1/proprietary/ui-data/login`; @@ -71,28 +121,24 @@ export function useBackendProbe() { next.status = 'down'; } - // SaaS desktop always runs bundled backend with login disabled, - // so only check the login endpoint in self-hosted mode. - if (mode === 'selfhosted') { - try { - const res = await fetch(loginUrl, { method: 'GET', connectTimeout: 5000 }); - if (res.ok) { - next.status = 'up'; - const data = await res.json().catch(() => null); - if (data && data.enableLogin === false) { - next.loginDisabled = true; - } - } else if (res.status === 404) { - next.status = 'up'; + try { + const res = await fetch(loginUrl, { method: 'GET', connectTimeout: 5000 }); + if (res.ok) { + next.status = 'up'; + const data = await res.json().catch(() => null); + if (data && data.enableLogin === false) { next.loginDisabled = true; - } else if (res.status === 503) { - next.status = 'starting'; - } else { - next.status = 'down'; } - } catch { - // keep existing inferred state (down/starting) + } else if (res.status === 404) { + next.status = 'up'; + next.loginDisabled = true; + } else if (res.status === 503) { + next.status = 'starting'; + } else { + next.status = 'down'; } + } catch { + // keep inferred state } setState(next); @@ -109,38 +155,19 @@ export function useBackendProbe() { }; } -async function resolveProbeBaseUrl(): Promise { +async function resolveRemoteBaseUrl(): Promise { try { const config = await connectionModeService.getCurrentConfig(); - if (config.mode === 'selfhosted') { - const serverUrl = config.server_config?.url; - if (!serverUrl) { - return null; - } - return { - baseUrl: stripTrailingSlash(serverUrl), - mode: 'selfhosted', - }; - } - - const directUrl = tauriBackendService.getBackendUrl(); - if (directUrl) { - return { - baseUrl: stripTrailingSlash(directUrl), - mode: 'saas', - }; - } - - const port = await invoke('get_backend_port').catch(() => null); - if (!port) { + if (config.mode !== 'selfhosted') { return null; } - return { - baseUrl: stripTrailingSlash(`http://localhost:${port}`), - mode: 'saas', - }; + const serverUrl = config.server_config?.url; + if (!serverUrl) { + return null; + } + return stripTrailingSlash(serverUrl); } catch (error) { - console.error('[Desktop useBackendProbe] Failed to resolve backend URL:', error); + console.error('[Desktop useBackendProbe] Failed to resolve remote backend URL:', error); return null; } } @@ -148,3 +175,13 @@ async function resolveProbeBaseUrl(): Promise { function stripTrailingSlash(value: string): string { return value.replace(/\/$/, ''); } + +function mapServiceStatus(status: ServiceBackendStatus): ProbeStatus { + if (status === 'healthy') { + return 'up'; + } + if (status === 'unhealthy') { + return 'down'; + } + return 'starting'; +}