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:
This commit is contained in:
ConnorYoh
2026-03-10 10:04:56 +00:00
committed by GitHub
parent 6d9fc59bc5
commit 8bc37bf5ae
31 changed files with 1003 additions and 343 deletions
@@ -2,14 +2,22 @@ import i18n from '@app/i18n';
import { alert } from '@app/components/toast';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { operationRouter } from '@app/services/operationRouter';
import { connectionModeService } from '@app/services/connectionModeService';
import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor';
const BACKEND_TOAST_COOLDOWN_MS = 4000;
let lastBackendToast = 0;
/**
* Desktop-specific guard that ensures the embedded backend is healthy
* before tools attempt to call any API endpoints.
* Enhanced to skip checks for endpoints routed to SaaS backend.
* Desktop-specific guard that ensures the relevant backend is ready before
* tools attempt to call any API endpoints.
*
* - SaaS mode: checks the local bundled backend via tauriBackendService.isOnline
* - Self-hosted mode (server online/checking): allows through — the operation
* targets the remote server and will surface network errors naturally
* - Self-hosted mode (server confirmed offline): allows through if local port is
* known (operationRouter falls back to local); suppresses toast since
* SelfHostedOfflineBanner already communicates the outage
*
* @param endpoint - Optional endpoint path to check if it needs local backend
* @returns true if backend is ready OR endpoint will be routed to SaaS
@@ -24,14 +32,40 @@ export async function ensureBackendReady(endpoint?: string): Promise<boolean> {
}
}
// Check local backend health
if (tauriBackendService.isBackendHealthy()) {
const mode = await connectionModeService.getCurrentMode();
if (mode === 'selfhosted') {
let { status } = selfHostedServerMonitor.getSnapshot();
// 'checking' means the first poll hasn't returned yet. Wait briefly (up to
// 1.5 s) for it to resolve so we don't surface raw network errors during the
// first few seconds after launch. If it doesn't resolve in time we fall
// through and allow the operation — the HTTP layer will handle any error.
if (status === 'checking') {
await Promise.race([
selfHostedServerMonitor.checkNow(),
new Promise<void>(resolve => setTimeout(resolve, 1500)),
]);
status = selfHostedServerMonitor.getSnapshot().status;
}
if (status === 'offline') {
// Server offline: allow through if local backend port is known.
// operationRouter will route to local for supported endpoints.
// Suppress the toast — SelfHostedOfflineBanner communicates the outage.
return !!tauriBackendService.getBackendUrl();
}
// Server online: allow through — the operation targets the remote server.
return true;
}
// Trigger a health check so we get the freshest status
// SaaS mode: check local bundled backend
if (tauriBackendService.isOnline) {
return true;
}
// Trigger a fresh check so we get the latest status
await tauriBackendService.checkBackendHealth();
if (tauriBackendService.isBackendHealthy()) {
if (tauriBackendService.isOnline) {
return true;
}