Attempt 4

This commit is contained in:
James Brunton 2025-11-27 17:14:12 +00:00
parent 60649c095f
commit 3c8e1054ad

View File

@ -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<BackendProbeState>({
status: 'starting',
status: initialStatus,
loginDisabled: false,
loading: true,
});
const modeRef = useRef<ConnectionMode>('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<ProbeTarget | null> {
async function resolveRemoteBaseUrl(): Promise<string | null> {
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<number | null>('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<ProbeTarget | null> {
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';
}