From bcd756ed20b866551b26e354caebd387da43646e Mon Sep 17 00:00:00 2001 From: perf3ct Date: Tue, 17 Jun 2025 01:57:56 +0000 Subject: [PATCH] feat(server/client): remove webdav feature from user's settings as it's in sources now --- frontend/src/contexts/AuthContext.tsx | 1 + frontend/src/pages/SettingsPage.tsx | 697 -------------------------- frontend/src/pages/SourcesPage.tsx | 111 +++- src/models.rs | 2 + 4 files changed, 112 insertions(+), 699 deletions(-) diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 9d671f8..ce879b4 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -5,6 +5,7 @@ interface User { id: string username: string email: string + role: 'Admin' | 'User' } interface AuthContextType { diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index bbc6128..591e70c 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -92,14 +92,6 @@ interface Settings { ocrQualityThresholdNoise: number; ocrQualityThresholdSharpness: number; ocrSkipEnhancement: boolean; - webdavEnabled: boolean; - webdavServerUrl: string; - webdavUsername: string; - webdavPassword: string; - webdavWatchFolders: string[]; - webdavFileExtensions: string[]; - webdavAutoSync: boolean; - webdavSyncIntervalMinutes: number; } interface SnackbarState { @@ -148,12 +140,6 @@ interface WebDAVConnectionResult { server_type?: string; } -interface WebDAVTabContentProps { - settings: Settings; - loading: boolean; - onSettingsChange: (key: keyof Settings, value: any) => Promise; - onShowSnackbar: (message: string, severity: 'success' | 'error' | 'warning' | 'info') => void; -} // Debounce utility function function useDebounce any>(func: T, delay: number): T { @@ -178,663 +164,6 @@ function useDebounce any>(func: T, delay: number): return debouncedFunc; } -const WebDAVTabContent: React.FC = ({ - settings, - loading, - onSettingsChange, - onShowSnackbar -}) => { - const [connectionResult, setConnectionResult] = useState(null); - const [testingConnection, setTestingConnection] = useState(false); - const [crawlEstimate, setCrawlEstimate] = useState(null); - const [estimatingCrawl, setEstimatingCrawl] = useState(false); - const [newFolder, setNewFolder] = useState(''); - - // WebDAV sync state - const [syncStatus, setSyncStatus] = useState(null); - const [startingSync, setStartingSync] = useState(false); - const [cancellingSync, setCancellingSync] = useState(false); - const [pollingSyncStatus, setPollingSyncStatus] = useState(false); - - // Local state for input fields to prevent focus loss - const [localWebdavServerUrl, setLocalWebdavServerUrl] = useState(settings.webdavServerUrl); - const [localWebdavUsername, setLocalWebdavUsername] = useState(settings.webdavUsername); - const [localWebdavPassword, setLocalWebdavPassword] = useState(settings.webdavPassword); - const [localSyncInterval, setLocalSyncInterval] = useState(settings.webdavSyncIntervalMinutes); - - // Update local state when settings change from outside (like initial load) - useEffect(() => { - setLocalWebdavServerUrl(settings.webdavServerUrl); - setLocalWebdavUsername(settings.webdavUsername); - setLocalWebdavPassword(settings.webdavPassword); - setLocalSyncInterval(settings.webdavSyncIntervalMinutes); - }, [settings.webdavServerUrl, settings.webdavUsername, settings.webdavPassword, settings.webdavSyncIntervalMinutes]); - - // Debounced update functions - const debouncedUpdateServerUrl = useDebounce((value: string) => { - onSettingsChange('webdavServerUrl', value); - }, 500); - - const debouncedUpdateUsername = useDebounce((value: string) => { - onSettingsChange('webdavUsername', value); - }, 500); - - const debouncedUpdatePassword = useDebounce((value: string) => { - onSettingsChange('webdavPassword', value); - }, 500); - - const debouncedUpdateSyncInterval = useDebounce((value: number) => { - onSettingsChange('webdavSyncIntervalMinutes', value); - }, 500); - - // Input change handlers - const handleServerUrlChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setLocalWebdavServerUrl(value); - debouncedUpdateServerUrl(value); - }; - - const handleUsernameChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setLocalWebdavUsername(value); - debouncedUpdateUsername(value); - }; - - const handlePasswordChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setLocalWebdavPassword(value); - debouncedUpdatePassword(value); - }; - - const handleSyncIntervalChange = (e: React.ChangeEvent) => { - const value = parseInt(e.target.value); - setLocalSyncInterval(value); - debouncedUpdateSyncInterval(value); - }; - - const testConnection = async () => { - if (!localWebdavServerUrl || !localWebdavUsername || !localWebdavPassword) { - onShowSnackbar('Please fill in all WebDAV connection details', 'warning'); - return; - } - - setTestingConnection(true); - try { - const response = await api.post('/webdav/test-connection', { - server_url: localWebdavServerUrl, - username: localWebdavUsername, - password: localWebdavPassword, - server_type: 'nextcloud' - }); - setConnectionResult(response.data); - onShowSnackbar(response.data.message, response.data.success ? 'success' : 'error'); - } catch (error: any) { - console.error('Connection test failed:', error); - setConnectionResult({ - success: false, - message: 'Connection test failed' - }); - onShowSnackbar('Connection test failed', 'error'); - } finally { - setTestingConnection(false); - } - }; - - const estimateCrawl = async () => { - if (!settings.webdavEnabled || settings.webdavWatchFolders.length === 0) { - onShowSnackbar('Please enable WebDAV and configure folders first', 'warning'); - return; - } - - setEstimatingCrawl(true); - try { - const response = await api.post('/webdav/estimate-crawl', { - folders: settings.webdavWatchFolders - }); - setCrawlEstimate(response.data); - onShowSnackbar('Crawl estimation completed', 'success'); - } catch (error: any) { - console.error('Crawl estimation failed:', error); - onShowSnackbar('Failed to estimate crawl', 'error'); - } finally { - setEstimatingCrawl(false); - } - }; - - const addFolder = () => { - if (newFolder && !settings.webdavWatchFolders.includes(newFolder)) { - onSettingsChange('webdavWatchFolders', [...settings.webdavWatchFolders, newFolder]); - setNewFolder(''); - } - }; - - const removeFolder = (folderToRemove: string) => { - onSettingsChange('webdavWatchFolders', settings.webdavWatchFolders.filter(f => f !== folderToRemove)); - }; - - const serverTypes = [ - { value: 'nextcloud', label: 'Nextcloud' }, - { value: 'owncloud', label: 'ownCloud' }, - { value: 'generic', label: 'Generic WebDAV' }, - ]; - - // WebDAV sync functions - const fetchSyncStatus = async () => { - try { - const response = await api.get('/webdav/sync-status'); - setSyncStatus(response.data); - } catch (error) { - console.error('Failed to fetch sync status:', error); - } - }; - - const startManualSync = async () => { - setStartingSync(true); - try { - const response = await api.post('/webdav/start-sync'); - if (response.data.success) { - onShowSnackbar('WebDAV sync started successfully', 'success'); - setPollingSyncStatus(true); - fetchSyncStatus(); // Get initial status - } else if (response.data.error === 'sync_already_running') { - onShowSnackbar('A WebDAV sync is already in progress', 'warning'); - } else { - onShowSnackbar(response.data.message || 'Failed to start sync', 'error'); - } - } catch (error: any) { - console.error('Failed to start sync:', error); - onShowSnackbar('Failed to start WebDAV sync', 'error'); - } finally { - setStartingSync(false); - } - }; - - const cancelManualSync = async () => { - setCancellingSync(true); - try { - const response = await api.post('/webdav/cancel-sync'); - if (response.data.success) { - onShowSnackbar('WebDAV sync cancelled successfully', 'info'); - fetchSyncStatus(); // Update status - } else { - onShowSnackbar(response.data.message || 'Failed to cancel sync', 'error'); - } - } catch (error: any) { - console.error('Failed to cancel sync:', error); - onShowSnackbar('Failed to cancel WebDAV sync', 'error'); - } finally { - setCancellingSync(false); - } - }; - - // Poll sync status when enabled - useEffect(() => { - if (!settings.webdavEnabled) { - setSyncStatus(null); - setPollingSyncStatus(false); - return; - } - - // Initial fetch - fetchSyncStatus(); - - // Set up polling interval - const interval = setInterval(() => { - fetchSyncStatus(); - }, 3000); // Poll every 3 seconds - - return () => clearInterval(interval); - }, [settings.webdavEnabled]); - - // Stop polling when sync is not running - useEffect(() => { - if (syncStatus && !syncStatus.is_running && pollingSyncStatus) { - setPollingSyncStatus(false); - } - }, [syncStatus, pollingSyncStatus]); - - // Auto-restart sync when folder list changes (if sync was running) - const [previousFolders, setPreviousFolders] = useState([]); - useEffect(() => { - if (previousFolders.length > 0 && - JSON.stringify(previousFolders.sort()) !== JSON.stringify([...settings.webdavWatchFolders].sort()) && - syncStatus?.is_running) { - - onShowSnackbar('Folder list changed - restarting WebDAV sync', 'info'); - - // Cancel current sync and start a new one - const restartSync = async () => { - try { - await api.post('/webdav/cancel-sync'); - // Small delay to ensure cancellation is processed - setTimeout(() => { - startManualSync(); - }, 1000); - } catch (error) { - console.error('Failed to restart sync after folder change:', error); - } - }; - - restartSync(); - } - - setPreviousFolders([...settings.webdavWatchFolders]); - }, [settings.webdavWatchFolders, syncStatus?.is_running]); - - return ( - - - WebDAV Integration - - - Connect to your WebDAV server (Nextcloud, ownCloud, etc.) to automatically discover and OCR files. - - - {/* Connection Configuration */} - - - - - Connection Settings - - - - - - - onSettingsChange('webdavEnabled', e.target.checked)} - disabled={loading} - /> - } - label="Enable WebDAV Integration" - /> - - Enable automatic file discovery and synchronization from WebDAV server - - - - - {settings.webdavEnabled && ( - <> - - - - - - Server Type - - - - - - - - - - - - {connectionResult && ( - - {connectionResult.message} - {connectionResult.server_version && ( - - Server: {connectionResult.server_type} v{connectionResult.server_version} - - )} - - )} - - - )} - - - - - {/* Folder Configuration */} - {settings.webdavEnabled && ( - - - - - Folders to Monitor - - - - - Specify which folders to scan for files. Use absolute paths starting with "/". - - - - setNewFolder(e.target.value)} - placeholder="/Documents" - disabled={loading} - sx={{ mr: 1, minWidth: 200 }} - /> - - - - - {settings.webdavWatchFolders.map((folder, index) => ( - removeFolder(folder)} - disabled={loading} - sx={{ mr: 1, mb: 1 }} - /> - ))} - - - - - - - - - onSettingsChange('webdavAutoSync', e.target.checked)} - disabled={loading} - /> - } - label="Enable Automatic Sync" - /> - - Automatically sync files on the configured interval - - - - - - - )} - - {/* Crawl Estimation */} - {settings.webdavEnabled && settings.webdavServerUrl && settings.webdavUsername && settings.webdavWatchFolders.length > 0 && ( - - - - - Crawl Estimation - - - - - Estimate how many files will be processed and how long it will take. - - - - - {estimatingCrawl && ( - - - - Analyzing folders and counting files... - - - )} - - {crawlEstimate && ( - - - Estimation Results - - - - - - {crawlEstimate.total_files.toLocaleString()} - - Total Files - - - - - - {crawlEstimate.total_supported_files.toLocaleString()} - - Supported Files - - - - - - {crawlEstimate.total_estimated_time_hours.toFixed(1)}h - - Estimated Time - - - - - - {(crawlEstimate.total_size_mb / 1024).toFixed(1)}GB - - Total Size - - - - - - - - - Folder - Total Files - Supported - Est. Time - Size (MB) - - - - {crawlEstimate.folders.map((folder) => ( - - {folder.path} - {folder.total_files.toLocaleString()} - {folder.supported_files.toLocaleString()} - {folder.estimated_time_hours.toFixed(1)}h - {folder.total_size_mb.toFixed(1)} - - ))} - -
-
-
- )} -
-
- )} - - {/* Manual Sync & Status */} - {settings.webdavEnabled && settings.webdavServerUrl && settings.webdavUsername && ( - - - - - Manual Sync & Status - - - - - {/* Sync Controls */} - - - - Start a manual WebDAV sync to immediately pull new or changed files from your configured folders. - - - - - {syncStatus?.is_running && ( - - )} - - {syncStatus?.is_running && ( - } - sx={{ ml: 1 }} - /> - )} - - - - {/* Sync Status */} - - {syncStatus && ( - - - Sync Status - - - - - - - {syncStatus.files_processed || 0} - - - Files Processed - - - - - - - {syncStatus.files_remaining || 0} - - - Files Remaining - - - - - - {syncStatus.current_folder && ( - - - Currently syncing: {syncStatus.current_folder} - - - )} - - {syncStatus.last_sync && ( - - Last sync: {new Date(syncStatus.last_sync).toLocaleString()} - - )} - - {syncStatus.errors && Array.isArray(syncStatus.errors) && syncStatus.errors.length > 0 && ( - - - Recent Errors: - - {syncStatus.errors.slice(0, 3).map((error: string, index: number) => ( - - • {error} - - ))} - {syncStatus.errors.length > 3 && ( - - ... and {syncStatus.errors.length - 3} more errors - - )} - - )} - - )} - - - - - )} -
- ); -}; const SettingsPage: React.FC = () => { const { user: currentUser } = useAuth(); @@ -881,14 +210,6 @@ const SettingsPage: React.FC = () => { ocrQualityThresholdNoise: 0.3, ocrQualityThresholdSharpness: 0.15, ocrSkipEnhancement: false, - webdavEnabled: false, - webdavServerUrl: '', - webdavUsername: '', - webdavPassword: '', - webdavWatchFolders: ['/Documents'], - webdavFileExtensions: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], - webdavAutoSync: false, - webdavSyncIntervalMinutes: 60, }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); @@ -981,14 +302,6 @@ const SettingsPage: React.FC = () => { 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, - webdavEnabled: response.data.webdav_enabled || false, - webdavServerUrl: response.data.webdav_server_url || '', - webdavUsername: response.data.webdav_username || '', - webdavPassword: response.data.webdav_password || '', - webdavWatchFolders: response.data.webdav_watch_folders || ['/Documents'], - webdavFileExtensions: response.data.webdav_file_extensions || ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], - webdavAutoSync: response.data.webdav_auto_sync || false, - webdavSyncIntervalMinutes: response.data.webdav_sync_interval_minutes || 60, }); } catch (error: any) { console.error('Error fetching settings:', error); @@ -1170,7 +483,6 @@ const SettingsPage: React.FC = () => { - @@ -1744,15 +1056,6 @@ const SettingsPage: React.FC = () => { )} {tabValue === 2 && ( - - )} - - {tabValue === 3 && ( diff --git a/frontend/src/pages/SourcesPage.tsx b/frontend/src/pages/SourcesPage.tsx index d7b0a0c..13bb088 100644 --- a/frontend/src/pages/SourcesPage.tsx +++ b/frontend/src/pages/SourcesPage.tsx @@ -63,10 +63,13 @@ import { Assessment as AssessmentIcon, Extension as ExtensionIcon, Storage as ServerIcon, + Pause as PauseIcon, + PlayArrow as ResumeIcon, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; -import api from '../services/api'; +import api, { queueService } from '../services/api'; import { formatDistanceToNow } from 'date-fns'; +import { useAuth } from '../contexts/AuthContext'; interface Source { id: string; @@ -94,8 +97,11 @@ interface SnackbarState { const SourcesPage: React.FC = () => { const theme = useTheme(); const navigate = useNavigate(); + const { user } = useAuth(); const [sources, setSources] = useState([]); const [loading, setLoading] = useState(true); + const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: string }>({ is_paused: false, status: 'running' }); + const [ocrLoading, setOcrLoading] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editingSource, setEditingSource] = useState(null); const [snackbar, setSnackbar] = useState({ @@ -143,7 +149,10 @@ const SourcesPage: React.FC = () => { useEffect(() => { loadSources(); - }, []); + if (user?.role === 'Admin') { + loadOcrStatus(); + } + }, [user]); // Update default folders when source type changes useEffect(() => { @@ -181,6 +190,47 @@ const SourcesPage: React.FC = () => { setSnackbar({ open: true, message, severity }); }; + // OCR Control Functions (Admin only) + const loadOcrStatus = async () => { + if (user?.role !== 'Admin') return; + try { + const response = await queueService.getOcrStatus(); + setOcrStatus(response.data); + } catch (error) { + console.error('Failed to load OCR status:', error); + } + }; + + const handlePauseOcr = async () => { + if (user?.role !== 'Admin') return; + setOcrLoading(true); + try { + await queueService.pauseOcr(); + await loadOcrStatus(); + showSnackbar('OCR processing paused successfully', 'success'); + } catch (error) { + console.error('Failed to pause OCR:', error); + showSnackbar('Failed to pause OCR processing', 'error'); + } finally { + setOcrLoading(false); + } + }; + + const handleResumeOcr = async () => { + if (user?.role !== 'Admin') return; + setOcrLoading(true); + try { + await queueService.resumeOcr(); + await loadOcrStatus(); + showSnackbar('OCR processing resumed successfully', 'success'); + } catch (error) { + console.error('Failed to resume OCR:', error); + showSnackbar('Failed to resume OCR processing', 'error'); + } finally { + setOcrLoading(false); + } + }; + const handleCreateSource = () => { setEditingSource(null); setFormData({ @@ -858,6 +908,63 @@ const SourcesPage: React.FC = () => { > Refresh + + {/* OCR Controls for Admin Users */} + {user?.role === 'Admin' && ( + <> + {ocrLoading ? ( + + ) : ocrStatus.is_paused ? ( + + ) : ( + + )} + + )} diff --git a/src/models.rs b/src/models.rs index c07d8e7..ef9c32e 100644 --- a/src/models.rs +++ b/src/models.rs @@ -75,6 +75,7 @@ pub struct UserResponse { pub id: Uuid, pub username: String, pub email: String, + pub role: UserRole, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] @@ -258,6 +259,7 @@ impl From for UserResponse { id: user.id, username: user.username, email: user.email, + role: user.role, } } }