Files
Stirling-PDF/frontend/src/desktop/hooks/useBackendHealth.ts
ConnorYoh 8bc37bf5ae Desktop: Fallback to local backend if self-hosted server is offline (#5880)
* Adds a fallback mechanism so the desktop app routes tool operations to
the local bundled backend when the user's self-hosted Stirling-PDF
server goes offline, and disables tools in the UI that aren't supported
locally.

* `selfHostedServerMonitor.ts` independently polls the self-hosted
server every 15s and exposes which tool endpoints are unavailable when
it goes offline
* `operationRouter.ts` intercepts operations destined for the
self-hosted server and reroutes them to the local bundled backend when
the monitor reports it offline
* `useSelfHostedToolAvailability.ts` feeds the offline tool set into
useToolManagement, disabling affected tools in the UI with a
selfHostedOffline reason and banner warning

- `SelfHostedOfflineBanner `is a dismissable (session-only) gray bar
shown at the top of the UI when in self-hosted mode and the server goes
offline. It shows:
2026-03-10 10:04:56 +00:00

64 lines
2.1 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react';
import { backendHealthMonitor } from '@app/services/backendHealthMonitor';
import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { connectionModeService } from '@app/services/connectionModeService';
import type { BackendHealthState } from '@app/types/backendHealth';
/**
* Hook to read backend health state for UI (Run button, BackendHealthIndicator).
*
* backendHealthMonitor tracks the local bundled backend.
* selfHostedServerMonitor tracks the remote server in self-hosted mode.
*
* isOnline logic:
* - SaaS mode: true when local backend is healthy
* - Self-hosted mode (server online): true (remote is up)
* - Self-hosted mode (server offline, local port known): true so the Run button
* stays enabled — operationRouter routes supported tools to local
* - Self-hosted mode (server offline, local port unknown): false
*/
export function useBackendHealth() {
const [health, setHealth] = useState<BackendHealthState>(() => backendHealthMonitor.getSnapshot());
const [serverStatus, setServerStatus] = useState(
() => selfHostedServerMonitor.getSnapshot().status
);
const [localUrl, setLocalUrl] = useState<string | null>(
() => tauriBackendService.getBackendUrl()
);
const [connectionMode, setConnectionMode] = useState<string | null>(null);
useEffect(() => {
void connectionModeService.getCurrentMode().then(setConnectionMode);
}, []);
useEffect(() => {
return backendHealthMonitor.subscribe(setHealth);
}, []);
useEffect(() => {
return selfHostedServerMonitor.subscribe(state => setServerStatus(state.status));
}, []);
useEffect(() => {
return tauriBackendService.subscribeToStatus(() => {
setLocalUrl(tauriBackendService.getBackendUrl());
});
}, []);
const checkHealth = useCallback(async () => {
return backendHealthMonitor.checkNow();
}, []);
const isOnline =
connectionMode === 'selfhosted'
? serverStatus !== 'offline' || !!localUrl
: health.isOnline;
return {
...health,
isOnline,
checkHealth,
};
}