Files
Stirling-PDF/frontend/src/desktop/components/BackendHealthIndicator.tsx
T
ConnorYohandGitHub 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

81 lines
2.3 KiB
TypeScript

import React, { useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Tooltip, useMantineTheme, useComputedColorScheme, rem } from '@mantine/core';
import { useBackendHealth } from '@app/hooks/useBackendHealth';
interface BackendHealthIndicatorProps {
className?: string;
}
export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
className = ''
}) => {
const { t } = useTranslation();
const theme = useMantineTheme();
const colorScheme = useComputedColorScheme('light');
const { status, isOnline, checkHealth } = useBackendHealth();
const label = useMemo(() => {
if (status === 'starting') {
return t('backendHealth.checking', 'Checking backend status...');
}
if (isOnline) {
return t('backendHealth.online', 'Backend Online');
}
return t('backendHealth.offline', 'Backend Offline');
}, [status, isOnline, t]);
const dotColor = useMemo(() => {
if (status === 'starting') {
return theme.colors.yellow?.[5] ?? '#fcc419';
}
if (isOnline) {
return theme.colors.green?.[5] ?? '#37b24d';
}
return theme.colors.red?.[6] ?? '#e03131';
}, [status, isOnline, theme.colors.green, theme.colors.red, theme.colors.yellow]);
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLSpanElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
checkHealth();
}
}, [checkHealth]);
return (
<Tooltip
label={label}
position="left"
offset={12}
withArrow
withinPortal
color={colorScheme === 'dark' ? undefined : 'dark'}
>
<Box
component="span"
className={className ? `${className}` : undefined}
role="status"
aria-live="polite"
aria-label={label}
tabIndex={0}
onClick={checkHealth}
onKeyDown={handleKeyDown}
style={{
width: rem(12),
height: rem(12),
borderRadius: '50%',
backgroundColor: dotColor,
boxShadow: colorScheme === 'dark'
? '0 0 0 2px rgba(255, 255, 255, 0.18)'
: '0 0 0 2px rgba(0, 0, 0, 0.08)',
cursor: 'pointer',
display: 'inline-block',
outline: 'none',
}}
/>
</Tooltip>
);
};