feat(server/client): remove webdav feature from user's settings as it's in sources now
This commit is contained in:
parent
8de1e153a1
commit
98a4b7479b
|
|
@ -5,6 +5,7 @@ interface User {
|
|||
id: string
|
||||
username: string
|
||||
email: string
|
||||
role: 'Admin' | 'User'
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue