= ({ id, tool, isSelected, onSelect,
// Get navigation props for URL support (only if navigation is not disabled)
const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null;
+ const { key: disabledKey, fallback: disabledFallback } = getDisabledLabel(disabledReason);
+ const disabledMessage = t(disabledKey, disabledFallback);
+
const tooltipContent = isUnavailable
- ? (Coming soon: {tool.description})
+ ? ({disabledMessage} {tool.description})
: (
{tool.description}
diff --git a/frontend/src/core/contexts/ToolWorkflowContext.tsx b/frontend/src/core/contexts/ToolWorkflowContext.tsx
index 7c657506e..f60c28785 100644
--- a/frontend/src/core/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/core/contexts/ToolWorkflowContext.tsx
@@ -4,7 +4,7 @@
*/
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react';
-import { useToolManagement } from '@app/hooks/useToolManagement';
+import { useToolManagement, type ToolAvailabilityMap } from '@app/hooks/useToolManagement';
import { PageEditorFunctions } from '@app/types/pageEditor';
import { ToolRegistryEntry, ToolRegistry } from '@app/data/toolsTaxonomy';
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
@@ -44,6 +44,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
selectedTool: ToolRegistryEntry | null;
toolRegistry: Partial;
getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null;
+ toolAvailability: ToolAvailabilityMap;
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
@@ -112,7 +113,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const navigationState = useNavigationState();
// Tool management hook
- const { toolRegistry, getSelectedTool } = useToolManagement();
+ const { toolRegistry, getSelectedTool, toolAvailability } = useToolManagement();
const { allTools } = useToolRegistry();
// Tool history hook
@@ -258,6 +259,11 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: ToolId) => {
+ const availabilityInfo = toolAvailability[toolId];
+ const isExplicitlyDisabled = availabilityInfo ? availabilityInfo.available === false : false;
+ if (toolId !== 'read' && toolId !== 'multiTool' && isExplicitlyDisabled) {
+ return;
+ }
// If we're currently on a custom workbench (e.g., Validate Signature report),
// selecting any tool should take the user back to the default file manager view.
const wasInCustomWorkbench = !isBaseWorkbench(navigationState.workbench);
@@ -299,7 +305,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setSearchQuery('');
setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools
- }, [actions, getSelectedTool, navigationState.workbench, setLeftPanelView, setReaderMode, setSearchQuery]);
+ }, [actions, getSelectedTool, navigationState.workbench, setLeftPanelView, setReaderMode, setSearchQuery, toolAvailability]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
@@ -354,6 +360,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
toolResetFunctions,
registerToolReset,
resetTool,
+ toolAvailability,
// Workflow Actions
handleToolSelect,
@@ -381,6 +388,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
selectedTool,
toolRegistry,
getSelectedTool,
+ toolAvailability,
setSidebarsVisible,
setLeftPanelView,
setReaderMode,
diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx
index e9ecf0c08..827247261 100644
--- a/frontend/src/core/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx
@@ -82,6 +82,7 @@ import { adjustPageScaleOperationConfig } from "@app/hooks/tools/adjustPageScale
import { scannerImageSplitOperationConfig } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
import { addPageNumbersOperationConfig } from "@app/components/tools/addPageNumbers/useAddPageNumbersOperation";
import { extractPagesOperationConfig } from "@app/hooks/tools/extractPages/useExtractPagesOperation";
+import { ENDPOINTS as SPLIT_ENDPOINT_NAMES } from '@app/constants/splitConstants';
import CompressSettings from "@app/components/tools/compress/CompressSettings";
import AddPasswordSettings from "@app/components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from "@app/components/tools/removePassword/RemovePasswordSettings";
@@ -300,6 +301,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
+ endpoints: ["get-info-on-pdf"],
synonyms: getSynonyms(t, "getPdfInfo"),
supportsAutomate: false,
automationSettings: null
@@ -398,6 +400,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
+ endpoints: Array.from(new Set(Object.values(SPLIT_ENDPOINT_NAMES))),
operationConfig: splitOperationConfig,
automationSettings: SplitAutomationSettings,
synonyms: getSynonyms(t, "split")
@@ -465,6 +468,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
+ endpoints: ["booklet-imposition"],
},
pdfToSinglePage: {
@@ -559,6 +563,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1,
+ endpoints: ["remove-annotations"],
operationConfig: removeAnnotationsOperationConfig,
automationSettings: null,
synonyms: getSynonyms(t, "removeAnnotations")
@@ -597,7 +602,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.REMOVAL,
maxFiles: -1,
- endpoints: ["remove-certificate-sign"],
+ endpoints: ["remove-cert-sign"],
operationConfig: removeCertificateSignOperationConfig,
synonyms: getSynonyms(t, "removeCertSign"),
automationSettings: null,
@@ -626,7 +631,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
name: t("home.autoRename.title", "Auto Rename PDF File"),
component: AutoRename,
maxFiles: -1,
- endpoints: ["remove-certificate-sign"],
+ endpoints: ["auto-rename"],
operationConfig: autoRenameOperationConfig,
description: t("home.autoRename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
@@ -681,6 +686,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
+ endpoints: ["overlay-pdf"],
operationConfig: overlayPdfsOperationConfig,
synonyms: getSynonyms(t, "overlay-pdfs"),
automationSettings: OverlayPdfsSettings
@@ -705,6 +711,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.addImage.desc", "Add images to PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
+ endpoints: ["add-image"],
synonyms: getSynonyms(t, "addImage"),
automationSettings: null
},
@@ -715,6 +722,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.scannerEffect.desc", "Create a PDF that looks like it was scanned"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
+ endpoints: ["scanner-effect"],
synonyms: getSynonyms(t, "scannerEffect"),
automationSettings: null
},
@@ -805,6 +813,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
+ endpoints: ["compress-pdf"],
operationConfig: compressOperationConfig,
automationSettings: CompressSettings,
synonyms: getSynonyms(t, "compress")
@@ -848,6 +857,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
+ endpoints: ["ocr-pdf"],
operationConfig: ocrOperationConfig,
automationSettings: OCRSettings,
synonyms: getSynonyms(t, "ocr")
diff --git a/frontend/src/core/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts b/frontend/src/core/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts
index 67476734a..838d672e5 100644
--- a/frontend/src/core/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts
+++ b/frontend/src/core/hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters.ts
@@ -14,6 +14,6 @@ export type RemoveCertificateSignParametersHook = BaseParametersHook {
return useBaseParameters({
defaultParameters,
- endpointName: 'remove-certificate-sign',
+ endpointName: 'remove-cert-sign',
});
};
\ No newline at end of file
diff --git a/frontend/src/core/hooks/useEndpointConfig.ts b/frontend/src/core/hooks/useEndpointConfig.ts
index 83bb34b57..587fb064d 100644
--- a/frontend/src/core/hooks/useEndpointConfig.ts
+++ b/frontend/src/core/hooks/useEndpointConfig.ts
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import apiClient from '@app/services/apiClient';
+import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
// Track globally fetched endpoint sets to prevent duplicate fetches across components
const globalFetchedSets = new Set();
-const globalEndpointCache: Record = {};
+const globalEndpointCache: Record = {};
/**
* Hook to check if a specific endpoint is enabled
@@ -59,11 +60,13 @@ export function useEndpointEnabled(endpoint: string): {
*/
export function useMultipleEndpointsEnabled(endpoints: string[]): {
endpointStatus: Record;
+ endpointDetails: Record;
loading: boolean;
error: string | null;
refetch: () => Promise;
} {
const [endpointStatus, setEndpointStatus] = useState>({});
+ const [endpointDetails, setEndpointDetails] = useState>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -73,31 +76,25 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
// Skip if we already fetched these exact endpoints globally
if (!force && globalFetchedSets.has(endpointsKey)) {
console.debug('[useEndpointConfig] Already fetched these endpoints globally, using cache');
- const cachedStatus = endpoints.reduce((acc, endpoint) => {
- if (endpoint in globalEndpointCache) {
- acc[endpoint] = globalEndpointCache[endpoint];
- }
- return acc;
- }, {} as Record);
- setEndpointStatus(cachedStatus);
+ const cached = endpoints.reduce(
+ (acc, endpoint) => {
+ const cachedDetails = globalEndpointCache[endpoint];
+ if (cachedDetails) {
+ acc.status[endpoint] = cachedDetails.enabled;
+ acc.details[endpoint] = cachedDetails;
+ }
+ return acc;
+ },
+ { status: {} as Record, details: {} as Record }
+ );
+ setEndpointStatus(cached.status);
+ setEndpointDetails(prev => ({ ...prev, ...cached.details }));
setLoading(false);
return;
}
if (!endpoints || endpoints.length === 0) {
setEndpointStatus({});
- setLoading(false);
- return;
- }
-
- // Check if JWT exists - if not, optimistically enable all endpoints
- const hasJwt = !!localStorage.getItem('stirling_jwt');
- if (!hasJwt) {
- console.debug('[useEndpointConfig] No JWT found - optimistically enabling all endpoints');
- const optimisticStatus = endpoints.reduce((acc, endpoint) => {
- acc[endpoint] = true;
- return acc;
- }, {} as Record);
- setEndpointStatus(optimisticStatus);
+ setEndpointDetails({});
setLoading(false);
return;
}
@@ -110,11 +107,19 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
const newEndpoints = endpoints.filter(ep => !(ep in globalEndpointCache));
if (newEndpoints.length === 0) {
console.debug('[useEndpointConfig] All endpoints already in global cache');
- const cachedStatus = endpoints.reduce((acc, endpoint) => {
- acc[endpoint] = globalEndpointCache[endpoint];
- return acc;
- }, {} as Record);
- setEndpointStatus(cachedStatus);
+ const cached = endpoints.reduce(
+ (acc, endpoint) => {
+ const cachedDetails = globalEndpointCache[endpoint];
+ if (cachedDetails) {
+ acc.status[endpoint] = cachedDetails.enabled;
+ acc.details[endpoint] = cachedDetails;
+ }
+ return acc;
+ },
+ { status: {} as Record, details: {} as Record }
+ );
+ setEndpointStatus(cached.status);
+ setEndpointDetails(prev => ({ ...prev, ...cached.details }));
globalFetchedSets.add(endpointsKey);
setLoading(false);
return;
@@ -123,30 +128,51 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
// Use batch API for efficiency - only fetch new endpoints
const endpointsParam = newEndpoints.join(',');
- const response = await apiClient.get>(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`);
+ const response = await apiClient.get>(`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`);
const statusMap = response.data;
// Update global cache with new results
- Object.assign(globalEndpointCache, statusMap);
+ Object.entries(statusMap).forEach(([endpoint, details]) => {
+ globalEndpointCache[endpoint] = {
+ enabled: details?.enabled ?? true,
+ reason: details?.reason ?? null,
+ };
+ });
// Get all requested endpoints from cache (including previously cached ones)
- const fullStatus = endpoints.reduce((acc, endpoint) => {
- acc[endpoint] = globalEndpointCache[endpoint] ?? true; // Default to true if not in cache
- return acc;
- }, {} as Record);
+ const fullStatus = endpoints.reduce(
+ (acc, endpoint) => {
+ const cachedDetails = globalEndpointCache[endpoint];
+ if (cachedDetails) {
+ acc.status[endpoint] = cachedDetails.enabled;
+ acc.details[endpoint] = cachedDetails;
+ } else {
+ acc.status[endpoint] = true;
+ }
+ return acc;
+ },
+ { status: {} as Record, details: {} as Record }
+ );
- setEndpointStatus(fullStatus);
+ setEndpointStatus(fullStatus.status);
+ setEndpointDetails(prev => ({ ...prev, ...fullStatus.details }));
globalFetchedSets.add(endpointsKey);
} catch (err: any) {
// On 401 (auth error), use optimistic fallback instead of disabling
if (err.response?.status === 401) {
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
- const optimisticStatus = endpoints.reduce((acc, endpoint) => {
- acc[endpoint] = true;
- globalEndpointCache[endpoint] = true; // Cache the optimistic value
- return acc;
- }, {} as Record);
- setEndpointStatus(optimisticStatus);
+ const optimisticStatus = endpoints.reduce(
+ (acc, endpoint) => {
+ const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null };
+ acc.status[endpoint] = true;
+ acc.details[endpoint] = optimisticDetails;
+ globalEndpointCache[endpoint] = optimisticDetails;
+ return acc;
+ },
+ { status: {} as Record, details: {} as Record }
+ );
+ setEndpointStatus(optimisticStatus.status);
+ setEndpointDetails(prev => ({ ...prev, ...optimisticStatus.details }));
setLoading(false);
return;
}
@@ -156,11 +182,17 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
console.error('[EndpointConfig] Failed to check multiple endpoints:', err);
// Fallback: assume all endpoints are enabled on error (optimistic)
- const optimisticStatus = endpoints.reduce((acc, endpoint) => {
- acc[endpoint] = true;
- return acc;
- }, {} as Record);
- setEndpointStatus(optimisticStatus);
+ const optimisticStatus = endpoints.reduce(
+ (acc, endpoint) => {
+ const optimisticDetails: EndpointAvailabilityDetails = { enabled: true, reason: null };
+ acc.status[endpoint] = true;
+ acc.details[endpoint] = optimisticDetails;
+ return acc;
+ },
+ { status: {} as Record, details: {} as Record }
+ );
+ setEndpointStatus(optimisticStatus.status);
+ setEndpointDetails(prev => ({ ...prev, ...optimisticStatus.details }));
} finally {
setLoading(false);
}
@@ -186,6 +218,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
return {
endpointStatus,
+ endpointDetails,
loading,
error,
refetch: () => fetchAllEndpointStatuses(true),
diff --git a/frontend/src/core/hooks/useToolManagement.tsx b/frontend/src/core/hooks/useToolManagement.tsx
index 3f5676824..a53fe6735 100644
--- a/frontend/src/core/hooks/useToolManagement.tsx
+++ b/frontend/src/core/hooks/useToolManagement.tsx
@@ -1,9 +1,20 @@
import { useState, useCallback, useMemo } from 'react';
import { useToolRegistry } from "@app/contexts/ToolRegistryContext";
+import { usePreferences } from '@app/contexts/PreferencesContext';
import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "@app/data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "@app/hooks/useEndpointConfig";
import { FileId } from '@app/types/file';
import { ToolId } from "@app/types/toolId";
+import type { EndpointDisableReason } from '@app/types/endpointAvailability';
+
+export type ToolDisableCause = 'disabledByAdmin' | 'missingDependency' | 'unknown';
+
+export interface ToolAvailabilityInfo {
+ available: boolean;
+ reason?: ToolDisableCause;
+}
+
+export type ToolAvailabilityMap = Partial>;
interface ToolManagementResult {
selectedTool: ToolRegistryEntry | null;
@@ -11,6 +22,7 @@ interface ToolManagementResult {
toolRegistry: Partial;
setToolSelectedFileIds: (fileIds: FileId[]) => void;
getSelectedTool: (toolKey: ToolId | null) => ToolRegistryEntry | null;
+ toolAvailability: ToolAvailabilityMap;
}
export const useToolManagement = (): ToolManagementResult => {
@@ -19,9 +31,10 @@ export const useToolManagement = (): ToolManagementResult => {
// Build endpoints list from registry entries with fallback to legacy mapping
const { allTools } = useToolRegistry();
const baseRegistry = allTools;
+ const { preferences } = usePreferences();
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
- const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
+ const { endpointStatus, endpointDetails, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
const isToolAvailable = useCallback((toolKey: string): boolean => {
// Keep tools enabled during loading (optimistic UX)
@@ -38,22 +51,64 @@ export const useToolManagement = (): ToolManagementResult => {
return endpoints.some((endpoint: string) => endpointStatus[endpoint] !== false);
}, [endpointsLoading, endpointStatus, baseRegistry]);
+ const deriveToolDisableReason = useCallback((toolKey: ToolId): ToolDisableCause => {
+ const tool = baseRegistry[toolKey];
+ if (!tool) {
+ return 'unknown';
+ }
+ const endpoints = tool.endpoints || [];
+ const disabledReasons: EndpointDisableReason[] = endpoints
+ .filter(endpoint => endpointStatus[endpoint] === false)
+ .map(endpoint => endpointDetails[endpoint]?.reason ?? 'CONFIG');
+
+ if (disabledReasons.some(reason => reason === 'DEPENDENCY')) {
+ return 'missingDependency';
+ }
+ if (disabledReasons.some(reason => reason === 'CONFIG')) {
+ return 'disabledByAdmin';
+ }
+ if (disabledReasons.length > 0) {
+ return 'unknown';
+ }
+ return 'unknown';
+ }, [baseRegistry, endpointDetails, endpointStatus]);
+
+ const toolAvailability = useMemo(() => {
+ if (endpointsLoading) {
+ return {};
+ }
+ const availability: ToolAvailabilityMap = {};
+ (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
+ const available = isToolAvailable(toolKey);
+ availability[toolKey] = available
+ ? { available: true }
+ : { available: false, reason: deriveToolDisableReason(toolKey) };
+ });
+ return availability;
+ }, [baseRegistry, deriveToolDisableReason, endpointsLoading, isToolAvailable]);
+
const toolRegistry: Partial = useMemo(() => {
const availableToolRegistry: Partial = {};
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
- if (isToolAvailable(toolKey)) {
- const baseTool = baseRegistry[toolKey];
- if (baseTool) {
- availableToolRegistry[toolKey] = {
- ...baseTool,
- name: baseTool.name,
- description: baseTool.description,
- };
- }
+ const baseTool = baseRegistry[toolKey];
+ if (!baseTool) return;
+ const availabilityInfo = toolAvailability[toolKey];
+ const isAvailable = availabilityInfo ? availabilityInfo.available !== false : true;
+
+ // Check if tool is "coming soon" (has no component and no link)
+ const isComingSoon = !baseTool.component && !baseTool.link && toolKey !== 'read' && toolKey !== 'multiTool';
+
+ if (preferences.hideUnavailableTools && (!isAvailable || isComingSoon)) {
+ return;
}
+ availableToolRegistry[toolKey] = {
+ ...baseTool,
+ name: baseTool.name,
+ description: baseTool.description,
+ };
});
return availableToolRegistry;
- }, [isToolAvailable, baseRegistry]);
+ }, [baseRegistry, preferences.hideUnavailableTools, toolAvailability]);
const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
return toolKey ? toolRegistry[toolKey] || null : null;
@@ -65,5 +120,6 @@ export const useToolManagement = (): ToolManagementResult => {
toolRegistry,
setToolSelectedFileIds,
getSelectedTool,
+ toolAvailability,
};
};
diff --git a/frontend/src/core/services/preferencesService.ts b/frontend/src/core/services/preferencesService.ts
index 5894eae50..bc30f453c 100644
--- a/frontend/src/core/services/preferencesService.ts
+++ b/frontend/src/core/services/preferencesService.ts
@@ -9,6 +9,8 @@ export interface UserPreferences {
toolPanelModePromptSeen: boolean;
showLegacyToolDescriptions: boolean;
hasCompletedOnboarding: boolean;
+ hideUnavailableTools: boolean;
+ hideUnavailableConversions: boolean;
}
export const DEFAULT_PREFERENCES: UserPreferences = {
@@ -19,6 +21,8 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
toolPanelModePromptSeen: false,
showLegacyToolDescriptions: false,
hasCompletedOnboarding: false,
+ hideUnavailableTools: false,
+ hideUnavailableConversions: false,
};
const STORAGE_KEY = 'stirlingpdf_preferences';
diff --git a/frontend/src/core/services/updateService.ts b/frontend/src/core/services/updateService.ts
index 1cba4838f..f134e403e 100644
--- a/frontend/src/core/services/updateService.ts
+++ b/frontend/src/core/services/updateService.ts
@@ -1,6 +1,6 @@
export interface UpdateSummary {
- latest_version: string;
- latest_stable_version?: string;
+ latest_version: string | null;
+ latest_stable_version?: string | null;
max_priority: 'urgent' | 'normal' | 'minor' | 'low';
recommended_action?: string;
any_breaking: boolean;
diff --git a/frontend/src/core/types/endpointAvailability.ts b/frontend/src/core/types/endpointAvailability.ts
new file mode 100644
index 000000000..c0f37d831
--- /dev/null
+++ b/frontend/src/core/types/endpointAvailability.ts
@@ -0,0 +1,6 @@
+export type EndpointDisableReason = 'CONFIG' | 'DEPENDENCY' | 'UNKNOWN' | null;
+
+export interface EndpointAvailabilityDetails {
+ enabled: boolean;
+ reason?: EndpointDisableReason;
+}
diff --git a/frontend/src/desktop/hooks/useEndpointConfig.ts b/frontend/src/desktop/hooks/useEndpointConfig.ts
index 8ccdc66e2..f0156b6dc 100644
--- a/frontend/src/desktop/hooks/useEndpointConfig.ts
+++ b/frontend/src/desktop/hooks/useEndpointConfig.ts
@@ -4,8 +4,10 @@ import { useTranslation } from 'react-i18next';
import apiClient from '@app/services/apiClient';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { isBackendNotReadyError } from '@app/constants/backendErrors';
+import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
import { connectionModeService } from '@desktop/services/connectionModeService';
+
interface EndpointConfig {
backendUrl: string;
}
@@ -128,6 +130,7 @@ export function useEndpointEnabled(endpoint: string): {
export function useMultipleEndpointsEnabled(endpoints: string[]): {
endpointStatus: Record;
+ endpointDetails: Record;
loading: boolean;
error: string | null;
refetch: () => Promise;
@@ -140,6 +143,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
return acc;
}, {} as Record);
});
+ const [endpointDetails, setEndpointDetails] = useState>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const isMountedRef = useRef(true);
@@ -174,13 +178,27 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
const endpointsParam = endpoints.join(',');
- const response = await apiClient.get>('/api/v1/config/endpoints-enabled', {
+ const response = await apiClient.get>('/api/v1/config/endpoints-availability', {
params: { endpoints: endpointsParam },
suppressErrorToast: true,
});
if (!isMountedRef.current) return;
- setEndpointStatus(response.data);
+ const details = Object.entries(response.data).reduce((acc, [endpointName, detail]) => {
+ acc[endpointName] = {
+ enabled: detail?.enabled ?? true,
+ reason: detail?.reason ?? null,
+ };
+ return acc;
+ }, {} as Record);
+
+ const statusMap = Object.keys(details).reduce((acc, key) => {
+ acc[key] = details[key].enabled;
+ return acc;
+ }, {} as Record);
+
+ setEndpointDetails(prev => ({ ...prev, ...details }));
+ setEndpointStatus(statusMap);
} catch (err: unknown) {
const isBackendStarting = isBackendNotReadyError(err);
const message = getErrorMessage(err);
@@ -188,10 +206,13 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
setError(isBackendStarting ? t('backendHealth.starting', 'Backend starting up...') : message);
const fallbackStatus = endpoints.reduce((acc, endpointName) => {
- acc[endpointName] = true;
+ const fallbackDetail: EndpointAvailabilityDetails = { enabled: true, reason: null };
+ acc.status[endpointName] = true;
+ acc.details[endpointName] = fallbackDetail;
return acc;
- }, {} as Record);
- setEndpointStatus(fallbackStatus);
+ }, { status: {} as Record, details: {} as Record });
+ setEndpointStatus(fallbackStatus.status);
+ setEndpointDetails(prev => ({ ...prev, ...fallbackStatus.details }));
if (!retryTimeoutRef.current) {
retryTimeoutRef.current = setTimeout(() => {
@@ -209,6 +230,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
useEffect(() => {
if (!endpoints || endpoints.length === 0) {
setEndpointStatus({});
+ setEndpointDetails({});
setLoading(false);
return;
}
@@ -230,6 +252,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
return {
endpointStatus,
+ endpointDetails,
loading,
error,
refetch: fetchAllEndpointStatuses,