feat(server/client): remove webdav feature from user's settings as it's in sources now
This commit is contained in:
parent
fad6756c8c
commit
bcd756ed20
|
|
@ -5,6 +5,7 @@ interface User {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
|
role: 'Admin' | 'User'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,6 @@ interface Settings {
|
||||||
ocrQualityThresholdNoise: number;
|
ocrQualityThresholdNoise: number;
|
||||||
ocrQualityThresholdSharpness: number;
|
ocrQualityThresholdSharpness: number;
|
||||||
ocrSkipEnhancement: boolean;
|
ocrSkipEnhancement: boolean;
|
||||||
webdavEnabled: boolean;
|
|
||||||
webdavServerUrl: string;
|
|
||||||
webdavUsername: string;
|
|
||||||
webdavPassword: string;
|
|
||||||
webdavWatchFolders: string[];
|
|
||||||
webdavFileExtensions: string[];
|
|
||||||
webdavAutoSync: boolean;
|
|
||||||
webdavSyncIntervalMinutes: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnackbarState {
|
interface SnackbarState {
|
||||||
|
|
@ -148,12 +140,6 @@ interface WebDAVConnectionResult {
|
||||||
server_type?: string;
|
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
|
// Debounce utility function
|
||||||
function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number): T {
|
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;
|
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 SettingsPage: React.FC = () => {
|
||||||
const { user: currentUser } = useAuth();
|
const { user: currentUser } = useAuth();
|
||||||
|
|
@ -881,14 +210,6 @@ const SettingsPage: React.FC = () => {
|
||||||
ocrQualityThresholdNoise: 0.3,
|
ocrQualityThresholdNoise: 0.3,
|
||||||
ocrQualityThresholdSharpness: 0.15,
|
ocrQualityThresholdSharpness: 0.15,
|
||||||
ocrSkipEnhancement: false,
|
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 [users, setUsers] = useState<User[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
@ -981,14 +302,6 @@ const SettingsPage: React.FC = () => {
|
||||||
ocrQualityThresholdNoise: response.data.ocr_quality_threshold_noise || 0.3,
|
ocrQualityThresholdNoise: response.data.ocr_quality_threshold_noise || 0.3,
|
||||||
ocrQualityThresholdSharpness: response.data.ocr_quality_threshold_sharpness || 0.15,
|
ocrQualityThresholdSharpness: response.data.ocr_quality_threshold_sharpness || 0.15,
|
||||||
ocrSkipEnhancement: response.data.ocr_skip_enhancement || false,
|
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) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching settings:', error);
|
console.error('Error fetching settings:', error);
|
||||||
|
|
@ -1170,7 +483,6 @@ const SettingsPage: React.FC = () => {
|
||||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
|
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
|
||||||
<Tab label="General" />
|
<Tab label="General" />
|
||||||
<Tab label="OCR Settings" />
|
<Tab label="OCR Settings" />
|
||||||
<Tab label="WebDAV Integration" />
|
|
||||||
<Tab label="User Management" />
|
<Tab label="User Management" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
@ -1744,15 +1056,6 @@ const SettingsPage: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tabValue === 2 && (
|
{tabValue === 2 && (
|
||||||
<WebDAVTabContent
|
|
||||||
settings={settings}
|
|
||||||
loading={loading}
|
|
||||||
onSettingsChange={handleSettingsChange}
|
|
||||||
onShowSnackbar={showSnackbar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tabValue === 3 && (
|
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,13 @@ import {
|
||||||
Assessment as AssessmentIcon,
|
Assessment as AssessmentIcon,
|
||||||
Extension as ExtensionIcon,
|
Extension as ExtensionIcon,
|
||||||
Storage as ServerIcon,
|
Storage as ServerIcon,
|
||||||
|
Pause as PauseIcon,
|
||||||
|
PlayArrow as ResumeIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import api from '../services/api';
|
import api, { queueService } from '../services/api';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
interface Source {
|
interface Source {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -94,8 +97,11 @@ interface SnackbarState {
|
||||||
const SourcesPage: React.FC = () => {
|
const SourcesPage: React.FC = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
const [sources, setSources] = useState<Source[]>([]);
|
const [sources, setSources] = useState<Source[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
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 [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingSource, setEditingSource] = useState<Source | null>(null);
|
const [editingSource, setEditingSource] = useState<Source | null>(null);
|
||||||
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
const [snackbar, setSnackbar] = useState<SnackbarState>({
|
||||||
|
|
@ -143,7 +149,10 @@ const SourcesPage: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSources();
|
loadSources();
|
||||||
}, []);
|
if (user?.role === 'Admin') {
|
||||||
|
loadOcrStatus();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
// Update default folders when source type changes
|
// Update default folders when source type changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -181,6 +190,47 @@ const SourcesPage: React.FC = () => {
|
||||||
setSnackbar({ open: true, message, severity });
|
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 = () => {
|
const handleCreateSource = () => {
|
||||||
setEditingSource(null);
|
setEditingSource(null);
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -858,6 +908,63 @@ const SourcesPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</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>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ pub struct UserResponse {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub role: UserRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
|
@ -258,6 +259,7 @@ impl From<User> for UserResponse {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue