mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00: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:
@@ -1,10 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Stack, Text, NumberInput, Select, Divider, Checkbox, Slider, SegmentedControl } from "@mantine/core";
|
||||
import { Stack, Text, NumberInput, Select, Divider, Checkbox, Slider, SegmentedControl, Tooltip, Box } from "@mantine/core";
|
||||
import SliderWithInput from '@app/components/shared/sliderWithInput/SliderWithInput';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CompressParameters } from "@app/hooks/tools/compress/useCompressParameters";
|
||||
import ButtonSelector from "@app/components/shared/ButtonSelector";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import { useGroupEnabled } from "@app/hooks/useGroupEnabled";
|
||||
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||
|
||||
interface CompressSettingsProps {
|
||||
@@ -15,20 +14,7 @@ interface CompressSettingsProps {
|
||||
|
||||
const CompressSettings = ({ parameters, onParameterChange, disabled = false }: CompressSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [imageMagickAvailable, setImageMagickAvailable] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkImageMagick = async () => {
|
||||
try {
|
||||
const response = await apiClient.get<boolean>('/api/v1/config/group-enabled?group=ImageMagick');
|
||||
setImageMagickAvailable(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to check ImageMagick availability:', error);
|
||||
setImageMagickAvailable(true); // Optimistic fallback
|
||||
}
|
||||
};
|
||||
checkImageMagick();
|
||||
}, []);
|
||||
const { enabled: imageMagickAvailable, unavailableReason: imageMagickReason } = useGroupEnabled('ImageMagick');
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
@@ -122,17 +108,27 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Checkbox
|
||||
checked={parameters.lineArt}
|
||||
onChange={(event) => onParameterChange('lineArt', event.currentTarget.checked)}
|
||||
disabled={disabled || imageMagickAvailable === false}
|
||||
label={t("compress.lineArt.label", "Convert images to line art (bilevel)")}
|
||||
description={
|
||||
imageMagickAvailable === false
|
||||
? t("compress.lineArt.unavailable", "ImageMagick is not installed or enabled on this server")
|
||||
: t("compress.lineArt.description", "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction.")
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
label={imageMagickReason ?? t("compress.lineArt.unavailable", "ImageMagick is not installed or enabled on this server")}
|
||||
disabled={imageMagickAvailable !== false}
|
||||
multiline
|
||||
maw={280}
|
||||
>
|
||||
<Box style={{ cursor: imageMagickAvailable === false ? 'not-allowed' : undefined }}>
|
||||
<Checkbox
|
||||
checked={parameters.lineArt}
|
||||
onChange={(event) => onParameterChange('lineArt', event.currentTarget.checked)}
|
||||
disabled={disabled || imageMagickAvailable === false}
|
||||
label={t("compress.lineArt.label", "Convert images to line art (bilevel)")}
|
||||
description={
|
||||
imageMagickAvailable !== false
|
||||
? t("compress.lineArt.description", "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction.")
|
||||
: undefined
|
||||
}
|
||||
style={{ pointerEvents: imageMagickAvailable === false ? 'none' : undefined }}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{parameters.lineArt && (
|
||||
<Stack gap="xs" style={{ opacity: (disabled || imageMagickAvailable === false) ? 0.6 : 1 }}>
|
||||
<Text size="sm" fw={600}>{t('compress.lineArt.detailLevel', 'Detail level')}</Text>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const getIconStyle = (): Record<string, string> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export type ToolDisabledReason = 'comingSoon' | 'disabledByAdmin' | 'missingDependency' | 'unknownUnavailable' | 'requiresPremium' | null;
|
||||
export type ToolDisabledReason = 'comingSoon' | 'disabledByAdmin' | 'missingDependency' | 'unknownUnavailable' | 'requiresPremium' | 'selfHostedOffline' | null;
|
||||
|
||||
export const getToolDisabledReason = (
|
||||
id: string,
|
||||
@@ -43,6 +43,9 @@ export const getToolDisabledReason = (
|
||||
|
||||
const availabilityInfo = toolAvailability?.[id as ToolId];
|
||||
if (availabilityInfo && availabilityInfo.available === false) {
|
||||
if (availabilityInfo.reason === 'selfHostedOffline') {
|
||||
return 'selfHostedOffline';
|
||||
}
|
||||
if (availabilityInfo.reason === 'missingDependency') {
|
||||
return 'missingDependency';
|
||||
}
|
||||
@@ -64,6 +67,12 @@ export const getDisabledLabel = (
|
||||
fallback: 'Premium feature:'
|
||||
};
|
||||
}
|
||||
if (disabledReason === 'selfHostedOffline') {
|
||||
return {
|
||||
key: 'toolPanel.fullscreen.selfHostedOffline',
|
||||
fallback: 'Requires your Stirling-PDF server (currently offline):'
|
||||
};
|
||||
}
|
||||
if (disabledReason === 'missingDependency') {
|
||||
return {
|
||||
key: 'toolPanel.fullscreen.unavailableDependency',
|
||||
|
||||
@@ -36,8 +36,8 @@ const OperationButton = ({
|
||||
'data-tour': dataTour
|
||||
}: OperationButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isHealthy, message: backendMessage } = useBackendHealth();
|
||||
const blockedByBackend = !isHealthy;
|
||||
const { isOnline, message: backendMessage } = useBackendHealth();
|
||||
const blockedByBackend = !isOnline;
|
||||
const combinedDisabled = disabled || blockedByBackend;
|
||||
const tooltipLabel = blockedByBackend
|
||||
? (backendMessage ?? t('backendHealth.checking', 'Checking backend status...'))
|
||||
|
||||
Reference in New Issue
Block a user