mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
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:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { endpointAvailabilityService } from '@app/services/endpointAvailabilityService';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor';
|
||||
import { EXTENSION_TO_ENDPOINT } from '@app/constants/convertConstants';
|
||||
import { getEndpointName } from '@app/utils/convertUtils';
|
||||
|
||||
@@ -28,16 +29,56 @@ export function useConversionCloudStatus(): ConversionStatus {
|
||||
|
||||
useEffect(() => {
|
||||
const checkConversions = async () => {
|
||||
// Don't check until backend is healthy
|
||||
// This prevents showing incorrect status during startup
|
||||
if (!tauriBackendService.isBackendHealthy()) {
|
||||
const mode = await connectionModeService.getCurrentMode();
|
||||
|
||||
// Self-hosted offline path: server is down but local backend is available.
|
||||
// Check each conversion against the local backend only (no cloud routing).
|
||||
if (mode === 'selfhosted') {
|
||||
const { status } = selfHostedServerMonitor.getSnapshot();
|
||||
const localUrl = tauriBackendService.getBackendUrl();
|
||||
if (status === 'offline' && localUrl) {
|
||||
const pairs: [string, string, string][] = [];
|
||||
for (const fromExt of Object.keys(EXTENSION_TO_ENDPOINT)) {
|
||||
for (const toExt of Object.keys(EXTENSION_TO_ENDPOINT[fromExt] || {})) {
|
||||
const endpointName = getEndpointName(fromExt, toExt);
|
||||
if (endpointName) pairs.push([fromExt, toExt, endpointName]);
|
||||
}
|
||||
}
|
||||
const availability: Record<string, boolean> = {};
|
||||
const cloudStatus: Record<string, boolean> = {};
|
||||
const localOnly: Record<string, boolean> = {};
|
||||
const results = await Promise.all(
|
||||
pairs.map(async ([fromExt, toExt, endpointName]) => {
|
||||
const key = `${fromExt}-${toExt}`;
|
||||
try {
|
||||
const supported = await endpointAvailabilityService.isEndpointSupportedLocally(endpointName, localUrl);
|
||||
return { key, supported };
|
||||
} catch {
|
||||
return { key, supported: false };
|
||||
}
|
||||
})
|
||||
);
|
||||
for (const { key, supported } of results) {
|
||||
availability[key] = supported;
|
||||
cloudStatus[key] = false;
|
||||
localOnly[key] = supported;
|
||||
}
|
||||
setStatus({ availability, cloudStatus, localOnly });
|
||||
return;
|
||||
}
|
||||
// Server online or local not ready: let normal endpoint checking handle it
|
||||
setStatus({ availability: {}, cloudStatus: {}, localOnly: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't check until backend is healthy (SaaS startup guard)
|
||||
if (!tauriBackendService.isOnline) {
|
||||
setStatus({ availability: {}, cloudStatus: {}, localOnly: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = await connectionModeService.getCurrentMode();
|
||||
if (mode !== 'saas') {
|
||||
// In non-SaaS modes, local endpoint checking handles everything
|
||||
// Non-SaaS, non-self-hosted: local endpoint checking handles everything
|
||||
setStatus({ availability: {}, cloudStatus: {}, localOnly: {} });
|
||||
return;
|
||||
}
|
||||
@@ -83,14 +124,29 @@ export function useConversionCloudStatus(): ConversionStatus {
|
||||
// Initial check
|
||||
checkConversions();
|
||||
|
||||
// Subscribe to backend status changes to re-check when backend becomes healthy
|
||||
const unsubscribe = tauriBackendService.subscribeToStatus((status) => {
|
||||
// Re-check when SaaS local backend becomes healthy
|
||||
const unsubLocal = tauriBackendService.subscribeToStatus((status) => {
|
||||
if (status === 'healthy') {
|
||||
checkConversions();
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
// Re-check when self-hosted server goes offline or comes back online.
|
||||
// By the time the server is confirmed offline, the local port is already
|
||||
// discovered (waitForPort completes in ~500ms vs the 8s server poll timeout).
|
||||
// selfHostedServerMonitor.subscribe immediately invokes the listener with the
|
||||
// current state, which would cause a duplicate check alongside the one above.
|
||||
// Skip the first invocation since checkConversions() was already called above.
|
||||
let skipFirst = true;
|
||||
const unsubServer = selfHostedServerMonitor.subscribe(() => {
|
||||
if (skipFirst) { skipFirst = false; return; }
|
||||
void checkConversions();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubLocal();
|
||||
unsubServer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return status;
|
||||
|
||||
Reference in New Issue
Block a user