feat(server/client): remove webdav feature from user's settings as it's in sources now

This commit is contained in:
perf3ct 2025-06-17 01:57:56 +00:00
parent 8de1e153a1
commit 98a4b7479b
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
4 changed files with 112 additions and 699 deletions

View File

@ -5,6 +5,7 @@ interface User {
id: string
username: string
email: string
role: 'Admin' | 'User'
}
interface AuthContextType {

View File

@ -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<void>;
onShowSnackbar: (message: string, severity: 'success' | 'error' | 'warning' | 'info') => void;
}
// Debounce utility function
function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number): T {
@ -178,663 +164,6 @@ function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number):
return debouncedFunc;
}
const WebDAVTabContent: React.FC<WebDAVTabContentProps> = ({
settings,
loading,
onSettingsChange,
onShowSnackbar
}) => {
const [connectionResult, setConnectionResult] = useState<WebDAVConnectionResult | null>(null);
const [testingConnection, setTestingConnection] = useState(false);
const [crawlEstimate, setCrawlEstimate] = useState<WebDAVCrawlEstimate | null>(null);
const [estimatingCrawl, setEstimatingCrawl] = useState(false);
const [newFolder, setNewFolder] = useState('');
// WebDAV sync state
const [syncStatus, setSyncStatus] = useState<any>(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<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavServerUrl(value);
debouncedUpdateServerUrl(value);
};
const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavUsername(value);
debouncedUpdateUsername(value);
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalWebdavPassword(value);
debouncedUpdatePassword(value);
};
const handleSyncIntervalChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<string[]>([]);
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 (
<Box>
<Typography variant="h6" sx={{ mb: 3 }}>
WebDAV Integration
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
Connect to your WebDAV server (Nextcloud, ownCloud, etc.) to automatically discover and OCR files.
</Typography>
{/* Connection Configuration */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
<CloudSyncIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Connection Settings
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.webdavEnabled}
onChange={(e) => onSettingsChange('webdavEnabled', e.target.checked)}
disabled={loading}
/>
}
label="Enable WebDAV Integration"
/>
<Typography variant="body2" color="text.secondary">
Enable automatic file discovery and synchronization from WebDAV server
</Typography>
</FormControl>
</Grid>
{settings.webdavEnabled && (
<>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Server URL"
value={localWebdavServerUrl}
onChange={handleServerUrlChange}
disabled={loading}
placeholder="https://cloud.example.com"
helperText="Full URL to your WebDAV server"
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>Server Type</InputLabel>
<Select
value="nextcloud"
label="Server Type"
disabled={loading}
>
{serverTypes.map((type) => (
<MenuItem key={type.value} value={type.value}>
{type.label}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Username"
value={localWebdavUsername}
onChange={handleUsernameChange}
disabled={loading}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Password / App Password"
type="password"
value={localWebdavPassword}
onChange={handlePasswordChange}
disabled={loading}
helperText="For Nextcloud/ownCloud, use an app password"
/>
</Grid>
<Grid item xs={12}>
<Button
variant="outlined"
onClick={testConnection}
disabled={testingConnection || loading}
sx={{ mr: 2 }}
>
{testingConnection ? 'Testing...' : 'Test Connection'}
</Button>
{connectionResult && (
<Alert severity={connectionResult.success ? 'success' : 'error'} sx={{ mt: 2 }}>
{connectionResult.message}
{connectionResult.server_version && (
<Typography variant="body2">
Server: {connectionResult.server_type} v{connectionResult.server_version}
</Typography>
)}
</Alert>
)}
</Grid>
</>
)}
</Grid>
</CardContent>
</Card>
{/* Folder Configuration */}
{settings.webdavEnabled && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
<FolderIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Folders to Monitor
</Typography>
<Divider sx={{ mb: 2 }} />
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Specify which folders to scan for files. Use absolute paths starting with "/".
</Typography>
<Box sx={{ mb: 2 }}>
<TextField
label="Add Folder Path"
value={newFolder}
onChange={(e) => setNewFolder(e.target.value)}
placeholder="/Documents"
disabled={loading}
sx={{ mr: 1, minWidth: 200 }}
/>
<Button variant="outlined" onClick={addFolder} disabled={!newFolder || loading}>
Add Folder
</Button>
</Box>
<Box sx={{ mb: 2 }}>
{settings.webdavWatchFolders.map((folder, index) => (
<Chip
key={index}
label={folder}
onDelete={() => removeFolder(folder)}
disabled={loading}
sx={{ mr: 1, mb: 1 }}
/>
))}
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Sync Interval (minutes)"
value={localSyncInterval}
onChange={handleSyncIntervalChange}
disabled={loading}
inputProps={{ min: 15, max: 1440 }}
helperText="How often to check for new files"
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.webdavAutoSync}
onChange={(e) => onSettingsChange('webdavAutoSync', e.target.checked)}
disabled={loading}
/>
}
label="Enable Automatic Sync"
/>
<Typography variant="body2" color="text.secondary">
Automatically sync files on the configured interval
</Typography>
</FormControl>
</Grid>
</Grid>
</CardContent>
</Card>
)}
{/* Crawl Estimation */}
{settings.webdavEnabled && settings.webdavServerUrl && settings.webdavUsername && settings.webdavWatchFolders.length > 0 && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Crawl Estimation
</Typography>
<Divider sx={{ mb: 2 }} />
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Estimate how many files will be processed and how long it will take.
</Typography>
<Button
variant="outlined"
onClick={estimateCrawl}
disabled={estimatingCrawl || loading}
sx={{ mb: 2 }}
>
{estimatingCrawl ? 'Estimating...' : 'Estimate Crawl'}
</Button>
{estimatingCrawl && (
<Box sx={{ mb: 2 }}>
<LinearProgress />
<Typography variant="body2" sx={{ mt: 1 }}>
Analyzing folders and counting files...
</Typography>
</Box>
)}
{crawlEstimate && (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
Estimation Results
</Typography>
<Grid container spacing={2} sx={{ mb: 2 }}>
<Grid item xs={12} md={3}>
<Paper sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="primary">
{crawlEstimate.total_files.toLocaleString()}
</Typography>
<Typography variant="body2">Total Files</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={3}>
<Paper sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="success.main">
{crawlEstimate.total_supported_files.toLocaleString()}
</Typography>
<Typography variant="body2">Supported Files</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={3}>
<Paper sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="warning.main">
{crawlEstimate.total_estimated_time_hours.toFixed(1)}h
</Typography>
<Typography variant="body2">Estimated Time</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={3}>
<Paper sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" color="info.main">
{(crawlEstimate.total_size_mb / 1024).toFixed(1)}GB
</Typography>
<Typography variant="body2">Total Size</Typography>
</Paper>
</Grid>
</Grid>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Folder</TableCell>
<TableCell align="right">Total Files</TableCell>
<TableCell align="right">Supported</TableCell>
<TableCell align="right">Est. Time</TableCell>
<TableCell align="right">Size (MB)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{crawlEstimate.folders.map((folder) => (
<TableRow key={folder.path}>
<TableCell>{folder.path}</TableCell>
<TableCell align="right">{folder.total_files.toLocaleString()}</TableCell>
<TableCell align="right">{folder.supported_files.toLocaleString()}</TableCell>
<TableCell align="right">{folder.estimated_time_hours.toFixed(1)}h</TableCell>
<TableCell align="right">{folder.total_size_mb.toFixed(1)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
)}
</CardContent>
</Card>
)}
{/* Manual Sync & Status */}
{settings.webdavEnabled && settings.webdavServerUrl && settings.webdavUsername && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
<PlayArrowIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Manual Sync & Status
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={3}>
{/* Sync Controls */}
<Grid item xs={12} md={6}>
<Box>
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Start a manual WebDAV sync to immediately pull new or changed files from your configured folders.
</Typography>
<Button
variant="contained"
startIcon={startingSync ? <CircularProgress size={16} /> : <PlayArrowIcon />}
onClick={startManualSync}
disabled={startingSync || loading || syncStatus?.is_running}
sx={{ mr: 2 }}
>
{startingSync ? 'Starting...' : syncStatus?.is_running ? 'Sync Running...' : 'Start Sync Now'}
</Button>
{syncStatus?.is_running && (
<Button
variant="outlined"
color="error"
startIcon={cancellingSync ? <CircularProgress size={16} /> : undefined}
onClick={cancelManualSync}
disabled={cancellingSync || loading}
sx={{ mr: 2 }}
>
{cancellingSync ? 'Cancelling...' : 'Cancel Sync'}
</Button>
)}
{syncStatus?.is_running && (
<Chip
label="Sync Active"
color="primary"
variant="outlined"
icon={<CircularProgress size={12} />}
sx={{ ml: 1 }}
/>
)}
</Box>
</Grid>
{/* Sync Status */}
<Grid item xs={12} md={6}>
{syncStatus && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Sync Status
</Typography>
<Grid container spacing={1}>
<Grid item xs={6}>
<Paper sx={{ p: 1.5, textAlign: 'center' }}>
<Typography variant="h6" color="primary">
{syncStatus.files_processed || 0}
</Typography>
<Typography variant="caption" color="text.secondary">
Files Processed
</Typography>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper sx={{ p: 1.5, textAlign: 'center' }}>
<Typography variant="h6" color="secondary">
{syncStatus.files_remaining || 0}
</Typography>
<Typography variant="caption" color="text.secondary">
Files Remaining
</Typography>
</Paper>
</Grid>
</Grid>
{syncStatus.current_folder && (
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
<strong>Currently syncing:</strong> {syncStatus.current_folder}
</Typography>
</Alert>
)}
{syncStatus.last_sync && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Last sync: {new Date(syncStatus.last_sync).toLocaleString()}
</Typography>
)}
{syncStatus.errors && Array.isArray(syncStatus.errors) && syncStatus.errors.length > 0 && (
<Alert severity="error" sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Recent Errors:</strong>
</Typography>
{syncStatus.errors.slice(0, 3).map((error: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
{error}
</Typography>
))}
{syncStatus.errors.length > 3 && (
<Typography variant="caption" color="text.secondary">
... and {syncStatus.errors.length - 3} more errors
</Typography>
)}
</Alert>
)}
</Box>
)}
</Grid>
</Grid>
</CardContent>
</Card>
)}
</Box>
);
};
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<User[]>([]);
const [loading, setLoading] = useState<boolean>(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 = () => {
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
<Tab label="General" />
<Tab label="OCR Settings" />
<Tab label="WebDAV Integration" />
<Tab label="User Management" />
</Tabs>
@ -1744,15 +1056,6 @@ const SettingsPage: React.FC = () => {
)}
{tabValue === 2 && (
<WebDAVTabContent
settings={settings}
loading={loading}
onSettingsChange={handleSettingsChange}
onShowSnackbar={showSnackbar}
/>
)}
{tabValue === 3 && (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h6">

View File

@ -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<Source[]>([]);
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<Source | null>(null);
const [snackbar, setSnackbar] = useState<SnackbarState>({
@ -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
</Button>
{/* OCR Controls for Admin Users */}
{user?.role === 'Admin' && (
<>
{ocrLoading ? (
<CircularProgress size={24} />
) : ocrStatus.is_paused ? (
<Button
variant="outlined"
size="large"
startIcon={<ResumeIcon />}
onClick={handleResumeOcr}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
borderWidth: 2,
borderColor: 'success.main',
color: 'success.main',
'&:hover': {
borderWidth: 2,
borderColor: 'success.dark',
backgroundColor: alpha(theme.palette.success.main, 0.1),
transform: 'translateY(-1px)',
},
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
Resume OCR
</Button>
) : (
<Button
variant="outlined"
size="large"
startIcon={<PauseIcon />}
onClick={handlePauseOcr}
sx={{
borderRadius: 3,
px: 4,
py: 1.5,
borderWidth: 2,
borderColor: 'warning.main',
color: 'warning.main',
'&:hover': {
borderWidth: 2,
borderColor: 'warning.dark',
backgroundColor: alpha(theme.palette.warning.main, 0.1),
transform: 'translateY(-1px)',
},
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
Pause OCR
</Button>
)}
</>
)}
</Stack>
</Box>

View File

@ -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<User> for UserResponse {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
}
}
}