import React, { useState, useEffect, useCallback } from 'react'; import { Box, Container, Typography, Paper, Tabs, Tab, FormControl, FormControlLabel, InputLabel, Select, MenuItem, Button, Snackbar, Alert, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Card, CardContent, Divider, Switch, SelectChangeEvent, Chip, LinearProgress, CircularProgress, Tooltip, } from '@mui/material'; import Grid from '@mui/material/GridLegacy'; import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon, CloudSync as CloudSyncIcon, Folder as FolderIcon, Assessment as AssessmentIcon, PlayArrow as PlayArrowIcon, Pause as PauseIcon, Stop as StopIcon, CheckCircle as CheckCircleIcon, Error as ErrorIcon, Visibility as VisibilityIcon, CreateNewFolder as CreateNewFolderIcon, RemoveCircle as RemoveCircleIcon, Warning as WarningIcon } from '@mui/icons-material'; import { useAuth } from '../contexts/AuthContext'; import api, { queueService, ErrorHelper, ErrorCodes, userWatchService, UserWatchDirectoryResponse } from '../services/api'; import OcrLanguageSelector from '../components/OcrLanguageSelector'; import LanguageSelector from '../components/LanguageSelector'; import { usePWA } from '../hooks/usePWA'; import { useTranslation } from 'react-i18next'; interface User { id: string; username: string; email: string; created_at: string; } interface Settings { ocrLanguage: string; preferredLanguages: string[]; primaryLanguage: string; autoDetectLanguageCombination: boolean; concurrentOcrJobs: number; ocrTimeoutSeconds: number; maxFileSizeMb: number; allowedFileTypes: string[]; autoRotateImages: boolean; enableImagePreprocessing: boolean; searchResultsPerPage: number; searchSnippetLength: number; fuzzySearchThreshold: number; retentionDays: number | null; enableAutoCleanup: boolean; enableCompression: boolean; memoryLimitMb: number; cpuPriority: string; enableBackgroundOcr: boolean; ocrPageSegmentationMode: number; ocrEngineMode: number; ocrMinConfidence: number; ocrDpi: number; ocrEnhanceContrast: boolean; ocrRemoveNoise: boolean; ocrDetectOrientation: boolean; ocrWhitelistChars: string; ocrBlacklistChars: string; ocrBrightnessBoost: number; ocrContrastMultiplier: number; ocrNoiseReductionLevel: number; ocrSharpeningStrength: number; ocrMorphologicalOperations: boolean; ocrAdaptiveThresholdWindowSize: number; ocrHistogramEqualization: boolean; ocrUpscaleFactor: number; ocrMaxImageWidth: number; ocrMaxImageHeight: number; saveProcessedImages: boolean; ocrQualityThresholdBrightness: number; ocrQualityThresholdContrast: number; ocrQualityThresholdNoise: number; ocrQualityThresholdSharpness: number; ocrSkipEnhancement: boolean; } interface SnackbarState { open: boolean; message: string; severity: 'success' | 'error' | 'warning' | 'info'; } interface UserDialogState { open: boolean; mode: 'create' | 'edit'; user: User | null; } interface UserFormData { username: string; email: string; password: string; } interface WebDAVFolderInfo { path: string; total_files: number; supported_files: number; estimated_time_hours: number; total_size_mb: number; } interface WebDAVCrawlEstimate { folders: WebDAVFolderInfo[]; total_files: number; total_supported_files: number; total_estimated_time_hours: number; total_size_mb: number; } interface WebDAVConnectionResult { success: boolean; message: string; server_version?: string; server_type?: string; } interface ServerConfiguration { max_file_size_mb: number; concurrent_ocr_jobs: number; ocr_timeout_seconds: number; memory_limit_mb: number; cpu_priority: string; server_host: string; server_port: number; jwt_secret_set: boolean; upload_path: string; watch_folder?: string; ocr_language: string; allowed_file_types: string[]; watch_interval_seconds?: number; file_stability_check_ms?: number; max_file_age_hours?: number; enable_background_ocr: boolean; version: string; build_info?: string; } // Debounce utility function function useDebounce any>(func: T, delay: number): T { const timeoutRef = React.useRef(null); const debouncedFunc = useCallback((...args: Parameters) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => func(...args), delay); }, [func, delay]) as T; // Cleanup on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return debouncedFunc; } const SettingsPage: React.FC = () => { const { t } = useTranslation(); const { user: currentUser } = useAuth(); const isPWA = usePWA(); const [tabValue, setTabValue] = useState(0); const [settings, setSettings] = useState({ ocrLanguage: 'eng', preferredLanguages: ['eng'], primaryLanguage: 'eng', autoDetectLanguageCombination: false, concurrentOcrJobs: 4, ocrTimeoutSeconds: 300, maxFileSizeMb: 50, allowedFileTypes: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], autoRotateImages: true, enableImagePreprocessing: false, searchResultsPerPage: 25, searchSnippetLength: 200, fuzzySearchThreshold: 0.8, retentionDays: null, enableAutoCleanup: false, enableCompression: false, memoryLimitMb: 512, cpuPriority: 'normal', enableBackgroundOcr: true, ocrPageSegmentationMode: 3, ocrEngineMode: 3, ocrMinConfidence: 30.0, ocrDpi: 300, ocrEnhanceContrast: true, ocrRemoveNoise: true, ocrDetectOrientation: true, ocrWhitelistChars: '', ocrBlacklistChars: '', ocrBrightnessBoost: 0.0, ocrContrastMultiplier: 1.0, ocrNoiseReductionLevel: 1, ocrSharpeningStrength: 0.0, ocrMorphologicalOperations: true, ocrAdaptiveThresholdWindowSize: 15, ocrHistogramEqualization: false, ocrUpscaleFactor: 1.0, ocrMaxImageWidth: 10000, ocrMaxImageHeight: 10000, saveProcessedImages: false, ocrQualityThresholdBrightness: 40.0, ocrQualityThresholdContrast: 0.15, ocrQualityThresholdNoise: 0.3, ocrQualityThresholdSharpness: 0.15, ocrSkipEnhancement: false, }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' }); const [userDialog, setUserDialog] = useState({ open: false, mode: 'create', user: null }); const [userForm, setUserForm] = useState({ username: '', email: '', password: '' }); // OCR Admin Controls State const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null); const [ocrActionLoading, setOcrActionLoading] = useState(false); // Server Configuration State const [serverConfig, setServerConfig] = useState(null); const [configLoading, setConfigLoading] = useState(false); // Watch Directory State const [userWatchDirectories, setUserWatchDirectories] = useState>(new Map()); const [watchDirLoading, setWatchDirLoading] = useState>(new Map()); const [confirmDialog, setConfirmDialog] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void; }>({ open: false, title: '', message: '', onConfirm: () => {}, }); useEffect(() => { fetchSettings(); fetchUsers(); fetchOcrStatus(); fetchServerConfiguration(); }, []); // Fetch watch directory information after users are loaded useEffect(() => { if (users.length > 0) { fetchUserWatchDirectories(); } }, [users]); const fetchSettings = async (): Promise => { try { const response = await api.get('/settings'); setSettings({ ocrLanguage: response.data.ocr_language || 'eng', preferredLanguages: response.data.preferred_languages || ['eng'], primaryLanguage: response.data.primary_language || 'eng', autoDetectLanguageCombination: response.data.auto_detect_language_combination || false, concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4, ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300, maxFileSizeMb: response.data.max_file_size_mb || 50, allowedFileTypes: response.data.allowed_file_types || ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], autoRotateImages: response.data.auto_rotate_images !== undefined ? response.data.auto_rotate_images : true, enableImagePreprocessing: response.data.enable_image_preprocessing !== undefined ? response.data.enable_image_preprocessing : false, searchResultsPerPage: response.data.search_results_per_page || 25, searchSnippetLength: response.data.search_snippet_length || 200, fuzzySearchThreshold: response.data.fuzzy_search_threshold || 0.8, retentionDays: response.data.retention_days, enableAutoCleanup: response.data.enable_auto_cleanup || false, enableCompression: response.data.enable_compression || false, memoryLimitMb: response.data.memory_limit_mb || 512, cpuPriority: response.data.cpu_priority || 'normal', enableBackgroundOcr: response.data.enable_background_ocr !== undefined ? response.data.enable_background_ocr : true, ocrPageSegmentationMode: response.data.ocr_page_segmentation_mode || 3, ocrEngineMode: response.data.ocr_engine_mode || 3, ocrMinConfidence: response.data.ocr_min_confidence || 30.0, ocrDpi: response.data.ocr_dpi || 300, ocrEnhanceContrast: response.data.ocr_enhance_contrast !== undefined ? response.data.ocr_enhance_contrast : true, ocrRemoveNoise: response.data.ocr_remove_noise !== undefined ? response.data.ocr_remove_noise : true, ocrDetectOrientation: response.data.ocr_detect_orientation !== undefined ? response.data.ocr_detect_orientation : true, ocrWhitelistChars: response.data.ocr_whitelist_chars || '', ocrBlacklistChars: response.data.ocr_blacklist_chars || '', ocrBrightnessBoost: response.data.ocr_brightness_boost || 0.0, ocrContrastMultiplier: response.data.ocr_contrast_multiplier || 1.0, ocrNoiseReductionLevel: response.data.ocr_noise_reduction_level || 1, ocrSharpeningStrength: response.data.ocr_sharpening_strength || 0.0, ocrMorphologicalOperations: response.data.ocr_morphological_operations !== undefined ? response.data.ocr_morphological_operations : true, ocrAdaptiveThresholdWindowSize: response.data.ocr_adaptive_threshold_window_size || 15, ocrHistogramEqualization: response.data.ocr_histogram_equalization || false, ocrUpscaleFactor: response.data.ocr_upscale_factor || 1.0, ocrMaxImageWidth: response.data.ocr_max_image_width || 10000, ocrMaxImageHeight: response.data.ocr_max_image_height || 10000, saveProcessedImages: response.data.save_processed_images || false, ocrQualityThresholdBrightness: response.data.ocr_quality_threshold_brightness || 40.0, ocrQualityThresholdContrast: response.data.ocr_quality_threshold_contrast || 0.15, ocrQualityThresholdNoise: response.data.ocr_quality_threshold_noise || 0.3, ocrQualityThresholdSharpness: response.data.ocr_quality_threshold_sharpness || 0.15, ocrSkipEnhancement: response.data.ocr_skip_enhancement || false, }); } catch (error: any) { console.error('Error fetching settings:', error); if (error.response?.status !== 404) { showSnackbar(t('settings.messages.settingsUpdateFailed'), 'error'); } } }; const fetchUsers = async (): Promise => { try { const response = await api.get('/users'); setUsers(response.data); } catch (error: any) { console.error('Error fetching users:', error); if (error.response?.status !== 404) { showSnackbar(t('settings.messages.settingsUpdateFailed'), 'error'); } } }; const handleSettingsChange = async (key: keyof Settings, value: any): Promise => { try { // Convert camelCase to snake_case for API const snakeCase = (str: string): string => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); const apiKey = snakeCase(key); // Build the update payload with only the changed field const updatePayload = { [apiKey]: value }; await api.put('/settings', updatePayload); // Only update state after successful API call setSettings(prevSettings => ({ ...prevSettings, [key]: value })); // Only show success message for non-text inputs to reduce noise if (typeof value !== 'string') { showSnackbar(t('settings.messages.settingsUpdated'), 'success'); } } catch (error) { console.error('Error updating settings:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); // Handle specific settings errors if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_INVALID_LANGUAGE)) { showSnackbar(t('settings.messages.invalidLanguage'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_VALUE_OUT_OF_RANGE)) { showSnackbar(t('settings.messages.valueOutOfRange', { message: errorInfo.message, suggestedAction: errorInfo.suggestedAction || '' }), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.SETTINGS_CONFLICTING_SETTINGS)) { showSnackbar(t('settings.messages.conflictingSettings'), 'warning'); } else { showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } }; const handleUserSubmit = async (): Promise => { setLoading(true); try { if (userDialog.mode === 'create') { await api.post('/users', userForm); showSnackbar(t('settings.messages.userCreated'), 'success'); } else { const { password, ...updateData } = userForm; const payload: any = updateData; if (password) { payload.password = password; } await api.put(`/users/${userDialog.user?.id}`, payload); showSnackbar(t('settings.messages.userUpdated'), 'success'); } fetchUsers(); handleCloseUserDialog(); } catch (error: any) { console.error('Error saving user:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); // Handle specific user errors with better messages if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DUPLICATE_USERNAME)) { showSnackbar(t('settings.messages.duplicateUsername'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DUPLICATE_EMAIL)) { showSnackbar(t('settings.messages.duplicateEmail'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_PASSWORD)) { showSnackbar(t('settings.messages.invalidPassword'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_EMAIL)) { showSnackbar(t('settings.messages.invalidEmail'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_INVALID_USERNAME)) { showSnackbar(t('settings.messages.invalidUsername'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else { showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } finally { setLoading(false); } }; const handleDeleteUser = async (userId: string): Promise => { if (userId === currentUser?.id) { showSnackbar(t('settings.messages.cannotDeleteSelf'), 'error'); return; } if (window.confirm(t('settings.messages.confirmDeleteUser'))) { setLoading(true); try { await api.delete(`/users/${userId}`); showSnackbar(t('settings.messages.userDeleted'), 'success'); fetchUsers(); } catch (error) { console.error('Error deleting user:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); // Handle specific delete errors if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_DELETE_RESTRICTED)) { showSnackbar(t('settings.messages.cannotDeleteUser'), 'error'); } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_NOT_FOUND)) { showSnackbar(t('settings.messages.userNotFound'), 'warning'); fetchUsers(); // Refresh the list } else if (ErrorHelper.isErrorCode(error, ErrorCodes.USER_PERMISSION_DENIED)) { showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else { showSnackbar(errorInfo.message || t('settings.messages.settingsUpdateFailed'), 'error'); } } finally { setLoading(false); } } }; const handleOpenUserDialog = (mode: 'create' | 'edit', user: User | null = null): void => { setUserDialog({ open: true, mode, user }); if (mode === 'edit' && user) { setUserForm({ username: user.username, email: user.email, password: '' }); } else { setUserForm({ username: '', email: '', password: '' }); } }; const handleCloseUserDialog = (): void => { setUserDialog({ open: false, mode: 'create', user: null }); setUserForm({ username: '', email: '', password: '' }); }; const showSnackbar = (message: string, severity: SnackbarState['severity']): void => { setSnackbar({ open: true, message, severity }); }; const handleTabChange = (event: React.SyntheticEvent, newValue: number): void => { setTabValue(newValue); }; const handleCpuPriorityChange = (event: SelectChangeEvent): void => { handleSettingsChange('cpuPriority', event.target.value); }; const handleResultsPerPageChange = (event: SelectChangeEvent): void => { handleSettingsChange('searchResultsPerPage', event.target.value); }; const handleLanguagesChange = (languages: string[], primary?: string) => { // Update multiple fields at once const updates = { preferredLanguages: languages, primaryLanguage: primary || languages[0] || 'eng', ocrLanguage: primary || languages[0] || 'eng', // Backward compatibility }; // Update all language-related settings Object.entries(updates).forEach(([key, value]) => { handleSettingsChange(key as keyof Settings, value); }); }; const fetchOcrStatus = async (): Promise => { try { const response = await queueService.getOcrStatus(); setOcrStatus(response.data); } catch (error: any) { console.error('Error fetching OCR status:', error); // Don't show error for OCR status since it might not be available for non-admin users } }; const handlePauseOcr = async (): Promise => { setOcrActionLoading(true); try { await queueService.pauseOcr(); showSnackbar(t('settings.messages.ocrPaused'), 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error pausing OCR:', error); if (error.response?.status === 403) { showSnackbar(t('settings.messages.ocrPauseFailed'), 'error'); } else { showSnackbar(t('settings.messages.ocrPauseFailedGeneric'), 'error'); } } finally { setOcrActionLoading(false); } }; const handleResumeOcr = async (): Promise => { setOcrActionLoading(true); try { await queueService.resumeOcr(); showSnackbar(t('settings.messages.ocrResumed'), 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error resuming OCR:', error); if (error.response?.status === 403) { showSnackbar(t('settings.messages.ocrResumeFailed'), 'error'); } else { showSnackbar(t('settings.messages.ocrResumeFailedGeneric'), 'error'); } } finally { setOcrActionLoading(false); } }; const fetchServerConfiguration = async (): Promise => { setConfigLoading(true); try { const response = await api.get('/settings/config'); setServerConfig(response.data); } catch (error: any) { console.error('Error fetching server configuration:', error); if (error.response?.status === 403) { showSnackbar(t('settings.messages.serverConfigLoadFailed'), 'error'); } else if (error.response?.status !== 404) { showSnackbar(t('settings.messages.serverConfigLoadFailedGeneric'), 'error'); } } finally { setConfigLoading(false); } }; // Watch Directory Functions const fetchUserWatchDirectories = async (): Promise => { try { const watchDirMap = new Map(); // Fetch watch directory info for each user await Promise.all( users.map(async (user) => { try { const response = await userWatchService.getUserWatchDirectory(user.id); watchDirMap.set(user.id, response.data); } catch (error: any) { // If watch directory doesn't exist or user doesn't have one, that's okay if (error.response?.status === 404) { watchDirMap.set(user.id, { user_id: user.id, username: user.username, watch_directory_path: `./user_watch/${user.username}`, exists: false, enabled: false, }); } else { console.error(`Error fetching watch directory for user ${user.username}:`, error); } } }) ); setUserWatchDirectories(watchDirMap); } catch (error: any) { console.error('Error fetching user watch directories:', error); // Don't show error message as this might not be available for all users } }; const setUserWatchDirLoading = (userId: string, loading: boolean): void => { setWatchDirLoading(prev => { const newMap = new Map(prev); if (loading) { newMap.set(userId, true); } else { newMap.delete(userId); } return newMap; }); }; const handleCreateWatchDirectory = async (userId: string): Promise => { setUserWatchDirLoading(userId, true); try { const response = await userWatchService.createUserWatchDirectory(userId); if (response.data.success) { showSnackbar(t('settings.messages.watchDirectoryCreated'), 'success'); // Refresh the watch directory info for this user try { const updatedResponse = await userWatchService.getUserWatchDirectory(userId); setUserWatchDirectories(prev => { const newMap = new Map(prev); newMap.set(userId, updatedResponse.data); return newMap; }); } catch (fetchError) { console.error('Error refreshing watch directory info:', fetchError); } } else { showSnackbar(response.data.message || t('settings.messages.watchDirectoryCreatedFailed'), 'error'); } } catch (error: any) { console.error('Error creating watch directory:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); if (error.response?.status === 403) { showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else if (error.response?.status === 409) { showSnackbar(t('settings.messages.watchDirectoryAlreadyExists'), 'warning'); } else { showSnackbar(errorInfo.message || t('settings.messages.watchDirectoryCreatedFailed'), 'error'); } } finally { setUserWatchDirLoading(userId, false); } }; const handleViewWatchDirectory = (directoryPath: string): void => { // For now, just show the path in a snackbar // In a real implementation, this could open a file explorer or navigate to a directory view showSnackbar(t('settings.messages.watchDirectoryPath', { path: directoryPath }), 'info'); }; const handleRemoveWatchDirectory = (userId: string, username: string): void => { setConfirmDialog({ open: true, title: t('settings.userManagement.confirmRemoveDirectory.title'), message: t('settings.userManagement.confirmRemoveDirectory.message', { username }), onConfirm: () => confirmRemoveWatchDirectory(userId), }); }; const confirmRemoveWatchDirectory = async (userId: string): Promise => { setUserWatchDirLoading(userId, true); try { const response = await userWatchService.deleteUserWatchDirectory(userId); if (response.data.success) { showSnackbar(t('settings.messages.watchDirectoryRemoved'), 'success'); // Update the watch directory info to reflect removal setUserWatchDirectories(prev => { const newMap = new Map(prev); const current = newMap.get(userId); if (current) { newMap.set(userId, { ...current, exists: false, enabled: false, }); } return newMap; }); } else { showSnackbar(response.data.message || t('settings.messages.watchDirectoryRemoveFailed'), 'error'); } } catch (error: any) { console.error('Error removing watch directory:', error); const errorInfo = ErrorHelper.formatErrorForDisplay(error, true); if (error.response?.status === 403) { showSnackbar(t('settings.messages.permissionDenied'), 'error'); } else if (error.response?.status === 404) { showSnackbar(t('settings.messages.watchDirectoryNotFound'), 'warning'); // Update state to reflect that it doesn't exist setUserWatchDirectories(prev => { const newMap = new Map(prev); const current = newMap.get(userId); if (current) { newMap.set(userId, { ...current, exists: false, enabled: false, }); } return newMap; }); } else { showSnackbar(errorInfo.message || t('settings.messages.watchDirectoryRemoveFailed'), 'error'); } } finally { setUserWatchDirLoading(userId, false); setConfirmDialog(prev => ({ ...prev, open: false })); } }; const handleCloseConfirmDialog = (): void => { setConfirmDialog(prev => ({ ...prev, open: false })); }; // Helper function to render watch directory status const renderWatchDirectoryStatus = (userId: string, username: string) => { const watchDirInfo = userWatchDirectories.get(userId); const isLoading = watchDirLoading.get(userId) || false; if (isLoading) { return ( {t('settings.userManagement.watchDirectory.loading')} ); } if (!watchDirInfo) { return ( {t('settings.userManagement.watchDirectory.statusUnknown')} ); } const getStatusIcon = () => { if (watchDirInfo.exists && watchDirInfo.enabled) { return ; } else if (watchDirInfo.exists && !watchDirInfo.enabled) { return ; } else { return ; } }; const getStatusText = () => { if (watchDirInfo.exists && watchDirInfo.enabled) { return t('settings.userManagement.watchDirectory.statusActive'); } else if (watchDirInfo.exists && !watchDirInfo.enabled) { return t('settings.userManagement.watchDirectory.statusDisabled'); } else { return t('settings.userManagement.watchDirectory.statusNotCreated'); } }; const getStatusColor = (): "success" | "warning" | "error" => { if (watchDirInfo.exists && watchDirInfo.enabled) { return 'success'; } else if (watchDirInfo.exists && !watchDirInfo.enabled) { return 'warning'; } else { return 'error'; } }; return ( {getStatusIcon()} {watchDirInfo.watch_directory_path} {/* Show truncated path on mobile */} .../user_watch/{username} ); }; return ( {t('settings.title')} {tabValue === 0 && ( {t('settings.general.title')} {t('settings.general.ocrConfiguration.title')} {t('settings.general.ocrConfiguration.description')} div': { width: '100%' } }}> handleSettingsChange('autoDetectLanguageCombination', e.target.checked)} disabled={loading} /> } label={t('settings.general.ocrConfiguration.autoDetectLanguageCombination')} /> {t('settings.general.ocrConfiguration.autoDetectLanguageCombinationHelper')} handleSettingsChange('concurrentOcrJobs', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 16 }} helperText={t('settings.general.ocrConfiguration.concurrentOcrJobsHelper')} /> handleSettingsChange('ocrTimeoutSeconds', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 30, max: 3600 }} helperText={t('settings.general.ocrConfiguration.ocrTimeoutHelper')} /> {t('settings.general.ocrConfiguration.cpuPriority')} {/* Admin OCR Controls */} {t('settings.general.ocrControls.title')} {t('settings.general.ocrControls.description')} {ocrStatus && ( : } size="medium" /> {ocrStatus.is_paused ? t('settings.general.ocrControls.ocrPausedMessage') : t('settings.general.ocrControls.ocrActiveMessage')} )} {ocrStatus?.is_paused && ( {t('settings.general.ocrControls.pausedAlertTitle')}
{t('settings.general.ocrControls.pausedAlertMessage')}
)}
{t('settings.general.fileProcessing.title')} handleSettingsChange('maxFileSizeMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 500 }} helperText={t('settings.general.fileProcessing.maxFileSizeHelper')} /> handleSettingsChange('memoryLimitMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 128, max: 4096 }} helperText={t('settings.general.fileProcessing.memoryLimitHelper')} /> handleSettingsChange('autoRotateImages', e.target.checked)} disabled={loading} /> } label={t('settings.general.fileProcessing.autoRotateImages')} /> {t('settings.general.fileProcessing.autoRotateImagesHelper')} handleSettingsChange('enableImagePreprocessing', e.target.checked)} disabled={loading} /> } label={t('settings.general.fileProcessing.enableImagePreprocessing')} /> {t('settings.general.fileProcessing.enableImagePreprocessingHelper')} {t('settings.general.fileProcessing.preprocessingWarning')} handleSettingsChange('enableBackgroundOcr', e.target.checked)} disabled={loading} /> } label={t('settings.general.fileProcessing.enableBackgroundOcr')} /> {t('settings.general.fileProcessing.enableBackgroundOcrHelper')} {t('settings.general.searchConfiguration.title')} {t('settings.general.searchConfiguration.resultsPerPage')} handleSettingsChange('searchSnippetLength', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 50, max: 500 }} helperText={t('settings.general.searchConfiguration.snippetLengthHelper')} /> handleSettingsChange('fuzzySearchThreshold', parseFloat(e.target.value))} disabled={loading} inputProps={{ min: 0, max: 1, step: 0.1 }} helperText={t('settings.general.searchConfiguration.fuzzySearchThresholdHelper')} /> {t('settings.general.storageManagement.title')} handleSettingsChange('retentionDays', e.target.value ? parseInt(e.target.value) : null)} disabled={loading} inputProps={{ min: 1 }} helperText={t('settings.general.storageManagement.retentionDaysHelper')} /> handleSettingsChange('enableAutoCleanup', e.target.checked)} disabled={loading} /> } label={t('settings.general.storageManagement.enableAutoCleanup')} /> {t('settings.general.storageManagement.enableAutoCleanupHelper')} handleSettingsChange('enableCompression', e.target.checked)} disabled={loading} /> } label={t('settings.general.storageManagement.enableCompression')} /> {t('settings.general.storageManagement.enableCompressionHelper')}
)} {tabValue === 1 && ( {t('settings.ocrSettings.title')} {t('settings.ocrSettings.enhancementControls.title')} handleSettingsChange('ocrSkipEnhancement', e.target.checked)} /> } label={t('settings.ocrSettings.enhancementControls.skipEnhancement')} sx={{ mb: 2 }} /> handleSettingsChange('ocrBrightnessBoost', parseFloat(e.target.value) || 0)} helperText={t('settings.ocrSettings.enhancementControls.brightnessBoostHelper')} inputProps={{ step: 0.1, min: 0, max: 100 }} /> handleSettingsChange('ocrContrastMultiplier', parseFloat(e.target.value) || 1)} helperText={t('settings.ocrSettings.enhancementControls.contrastMultiplierHelper')} inputProps={{ step: 0.1, min: 0.1, max: 5 }} /> {t('settings.ocrSettings.enhancementControls.noiseReductionLevel')} handleSettingsChange('ocrSharpeningStrength', parseFloat(e.target.value) || 0)} helperText={t('settings.ocrSettings.enhancementControls.sharpeningStrengthHelper')} inputProps={{ step: 0.1, min: 0, max: 2 }} /> {t('settings.ocrSettings.qualityThresholds.title')} handleSettingsChange('ocrQualityThresholdBrightness', parseFloat(e.target.value) || 40)} helperText={t('settings.ocrSettings.qualityThresholds.brightnessThresholdHelper')} inputProps={{ step: 1, min: 0, max: 255 }} /> handleSettingsChange('ocrQualityThresholdContrast', parseFloat(e.target.value) || 0.15)} helperText={t('settings.ocrSettings.qualityThresholds.contrastThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdNoise', parseFloat(e.target.value) || 0.3)} helperText={t('settings.ocrSettings.qualityThresholds.noiseThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdSharpness', parseFloat(e.target.value) || 0.15)} helperText={t('settings.ocrSettings.qualityThresholds.sharpnessThresholdHelper')} inputProps={{ step: 0.01, min: 0, max: 1 }} /> {t('settings.ocrSettings.advancedProcessing.title')} handleSettingsChange('ocrMorphologicalOperations', e.target.checked)} /> } label={t('settings.ocrSettings.advancedProcessing.morphologicalOperations')} /> handleSettingsChange('ocrHistogramEqualization', e.target.checked)} /> } label={t('settings.ocrSettings.advancedProcessing.histogramEqualization')} /> handleSettingsChange('saveProcessedImages', e.target.checked)} /> } label={t('settings.ocrSettings.advancedProcessing.saveProcessedImages')} /> handleSettingsChange('ocrAdaptiveThresholdWindowSize', parseInt(e.target.value) || 15)} helperText={t('settings.ocrSettings.advancedProcessing.adaptiveThresholdWindowSizeHelper')} inputProps={{ step: 2, min: 3, max: 101 }} /> {t('settings.ocrSettings.imageSizeScaling.title')} handleSettingsChange('ocrMaxImageWidth', parseInt(e.target.value) || 10000)} helperText={t('settings.ocrSettings.imageSizeScaling.maxImageWidthHelper')} inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrMaxImageHeight', parseInt(e.target.value) || 10000)} helperText={t('settings.ocrSettings.imageSizeScaling.maxImageHeightHelper')} inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrUpscaleFactor', parseFloat(e.target.value) || 1.0)} helperText={t('settings.ocrSettings.imageSizeScaling.upscaleFactorHelper')} inputProps={{ step: 0.1, min: 0.1, max: 5 }} /> )} {tabValue === 2 && ( {t('settings.userManagement.title')} {t('settings.userManagement.tableHeaders.username')} {t('settings.userManagement.tableHeaders.email')} {t('settings.userManagement.tableHeaders.createdAt')} {t('settings.userManagement.tableHeaders.watchDirectory')} {t('settings.userManagement.tableHeaders.actions')} {users.map((user) => ( {user.username} {/* Show email on mobile */} {user.email} {/* Show created date on mobile */} Created: {new Date(user.created_at).toLocaleDateString()} {user.email} {new Date(user.created_at).toLocaleDateString()} {renderWatchDirectoryStatus(user.id, user.username)} {/* Watch Directory Actions */} {(() => { const watchDirInfo = userWatchDirectories.get(user.id); const isWatchDirLoading = watchDirLoading.get(user.id) || false; if (!watchDirInfo || !watchDirInfo.exists) { // Show Create Directory button return ( handleCreateWatchDirectory(user.id)} disabled={loading || isWatchDirLoading} color="primary" size="small" > {isWatchDirLoading ? ( ) : ( )} ); } else { // Show View and Remove buttons return ( <> handleViewWatchDirectory(watchDirInfo.watch_directory_path)} disabled={loading || isWatchDirLoading} color="info" size="small" > handleRemoveWatchDirectory(user.id, user.username)} disabled={loading || isWatchDirLoading} color="error" size="small" > {isWatchDirLoading ? ( ) : ( )} ); } })()} {/* User Management Actions */} handleOpenUserDialog('edit', user)} disabled={loading} size="small" > handleDeleteUser(user.id)} disabled={loading || user.id === currentUser?.id} color="error" size="small" > ))}
)} {tabValue === 3 && ( {t('settings.serverConfiguration.title')} {configLoading ? ( ) : serverConfig ? ( <> {t('settings.serverConfiguration.fileUpload.title')} {t('settings.serverConfiguration.fileUpload.maxFileSize')} {serverConfig.max_file_size_mb} MB {t('settings.serverConfiguration.fileUpload.uploadPath')} {serverConfig.upload_path} {t('settings.serverConfiguration.fileUpload.allowedFileTypes')} {serverConfig.allowed_file_types.map((type) => ( ))} {serverConfig.watch_folder && ( {t('settings.serverConfiguration.fileUpload.watchFolder')} {serverConfig.watch_folder} )} {t('settings.serverConfiguration.ocrProcessing.title')} {t('settings.serverConfiguration.ocrProcessing.concurrentOcrJobs')} {serverConfig.concurrent_ocr_jobs} {t('settings.serverConfiguration.ocrProcessing.ocrTimeout')} {serverConfig.ocr_timeout_seconds}s {t('settings.serverConfiguration.ocrProcessing.memoryLimit')} {serverConfig.memory_limit_mb} MB {t('settings.serverConfiguration.ocrProcessing.ocrLanguage')} {serverConfig.ocr_language} {t('settings.serverConfiguration.ocrProcessing.cpuPriority')} {serverConfig.cpu_priority} {t('settings.serverConfiguration.ocrProcessing.backgroundOcr')} {t('settings.serverConfiguration.serverInformation.title')} {t('settings.serverConfiguration.serverInformation.serverHost')} {serverConfig.server_host} {t('settings.serverConfiguration.serverInformation.serverPort')} {serverConfig.server_port} {t('settings.serverConfiguration.serverInformation.jwtSecret')} {t('settings.serverConfiguration.serverInformation.version')} {serverConfig.version} {serverConfig.build_info && ( {t('settings.serverConfiguration.serverInformation.buildInformation')} {serverConfig.build_info} )} {serverConfig.watch_interval_seconds && ( {t('settings.serverConfiguration.watchFolderConfiguration.title')} {t('settings.serverConfiguration.watchFolderConfiguration.watchInterval')} {serverConfig.watch_interval_seconds}s {serverConfig.file_stability_check_ms && ( {t('settings.serverConfiguration.watchFolderConfiguration.fileStabilityCheck')} {serverConfig.file_stability_check_ms}ms )} {serverConfig.max_file_age_hours && ( {t('settings.serverConfiguration.watchFolderConfiguration.maxFileAge')} {serverConfig.max_file_age_hours}h )} )} ) : ( {t('settings.serverConfiguration.loadFailed')} )} )}
{userDialog.mode === 'create' ? t('settings.userManagement.dialogs.createUser') : t('settings.userManagement.dialogs.editUser')} setUserForm({ ...userForm, username: e.target.value })} required /> setUserForm({ ...userForm, email: e.target.value })} required /> setUserForm({ ...userForm, password: e.target.value })} required={userDialog.mode === 'create'} /> {/* Confirmation Dialog for Watch Directory Actions */} {confirmDialog.title} {confirmDialog.message} setSnackbar({ ...snackbar, open: false })} > setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity} sx={{ width: '100%' }} > {snackbar.message}
); }; export default SettingsPage;