Readur/frontend/src/pages/WatchFolderPage.tsx

409 lines
15 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Typography,
Paper,
Card,
CardContent,
Grid,
Chip,
LinearProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Alert,
Button,
IconButton,
CircularProgress,
} from '@mui/material';
import {
Refresh as RefreshIcon,
Folder as FolderIcon,
CheckCircleOutline as CheckCircleIcon,
Error as ErrorIcon,
Schedule as ScheduleIcon,
Visibility as VisibilityIcon,
CloudUpload as CloudUploadIcon,
Description as DescriptionIcon,
} from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import { queueService, QueueStats } from '../services/api';
interface WatchConfig {
watchFolder: string;
watchInterval: number;
maxFileAge: number;
allowedTypes: string[];
isActive: boolean;
strategy: string;
}
const WatchFolderPage: React.FC = () => {
const theme = useTheme();
const [queueStats, setQueueStats] = useState<QueueStats | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
const [requeuingFailed, setRequeuingFailed] = useState<boolean>(false);
// Mock configuration data (would typically come from API)
const watchConfig: WatchConfig = {
watchFolder: import.meta.env.VITE_WATCH_FOLDER || './watch',
watchInterval: 30,
maxFileAge: 24,
allowedTypes: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt', 'doc', 'docx'],
isActive: true,
strategy: 'hybrid'
};
useEffect(() => {
fetchQueueStats();
const interval = setInterval(fetchQueueStats, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
const fetchQueueStats = async (): Promise<void> => {
try {
setLoading(true);
const response = await queueService.getStats();
setQueueStats(response.data);
setLastRefresh(new Date());
setError(null);
} catch (err) {
console.error('Error fetching queue stats:', err);
setError('Failed to fetch queue statistics');
} finally {
setLoading(false);
}
};
const requeueFailedJobs = async (): Promise<void> => {
try {
setRequeuingFailed(true);
const response = await queueService.requeueFailedItems();
const requeued = response.data.requeued_count || 0;
if (requeued > 0) {
// Show success message
setError(null);
// Refresh stats to see updated counts
await fetchQueueStats();
}
} catch (err) {
console.error('Error requeuing failed jobs:', err);
setError('Failed to requeue failed jobs');
} finally {
setRequeuingFailed(false);
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDuration = (minutes: number | null | undefined): string => {
if (!minutes) return 'N/A';
if (minutes < 60) return `${Math.round(minutes)}m`;
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
return `${hours}h ${mins}m`;
};
const getStatusColor = (status: string): 'success' | 'error' | 'warning' | 'default' => {
switch (status) {
case 'active': return 'success';
case 'error': return 'error';
case 'pending': return 'warning';
default: return 'default';
}
};
const getStatusIcon = (status: string): JSX.Element => {
switch (status) {
case 'active': return <CheckCircleIcon />;
case 'error': return <ErrorIcon />;
case 'pending': return <ScheduleIcon />;
default: return <VisibilityIcon />;
}
};
return (
<Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Typography variant="h4" sx={{
fontWeight: 600,
background: theme.palette.mode === 'light'
? 'linear-gradient(135deg, #1e293b 0%, #6366f1 100%)'
: 'linear-gradient(135deg, #f8fafc 0%, #a855f7 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
Watch Folder
</Typography>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={fetchQueueStats}
disabled={loading}
sx={{ mr: 2 }}
>
Refresh
</Button>
{queueStats && queueStats.failed_count > 0 && (
<Button
variant="contained"
color="warning"
startIcon={requeuingFailed ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={requeueFailedJobs}
disabled={requeuingFailed || loading}
>
{requeuingFailed ? 'Requeuing...' : `Retry ${queueStats.failed_count} Failed Jobs`}
</Button>
)}
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{/* Watch Folder Configuration */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<FolderIcon color="primary" />
Watch Folder Configuration
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Watched Directory
</Typography>
<Typography variant="body1" sx={{
fontFamily: 'monospace',
bgcolor: theme.palette.mode === 'light' ? 'grey.100' : 'grey.800',
p: 1,
borderRadius: 1,
color: 'text.primary',
}}>
{watchConfig.watchFolder}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Status
</Typography>
<Chip
icon={getStatusIcon(watchConfig.isActive ? 'active' : 'error')}
label={watchConfig.isActive ? 'Active' : 'Inactive'}
color={getStatusColor(watchConfig.isActive ? 'active' : 'error')}
variant="filled"
/>
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Watch Strategy
</Typography>
<Typography variant="body1" sx={{ textTransform: 'capitalize' }}>
{watchConfig.strategy}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Scan Interval
</Typography>
<Typography variant="body1">
{watchConfig.watchInterval} seconds
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Max File Age
</Typography>
<Typography variant="body1">
{watchConfig.maxFileAge} hours
</Typography>
</Box>
</Grid>
<Grid item xs={12}>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Supported File Types
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{watchConfig.allowedTypes.map((type) => (
<Chip
key={type}
label={`.${type}`}
size="small"
variant="outlined"
color="primary"
/>
))}
</Box>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Queue Statistics */}
{queueStats && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<CloudUploadIcon color="primary" />
Processing Queue
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'info.light', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'info.dark' }}>
{queueStats.pending_count}
</Typography>
<Typography variant="body2" color="text.secondary">
Pending
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'warning.light', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'warning.dark' }}>
{queueStats.processing_count}
</Typography>
<Typography variant="body2" color="text.secondary">
Processing
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'error.light', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'error.dark' }}>
{queueStats.failed_count}
</Typography>
<Typography variant="body2" color="text.secondary">
Failed
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'success.light', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'success.dark' }}>
{queueStats.completed_today}
</Typography>
<Typography variant="body2" color="text.secondary">
Completed Today
</Typography>
</Box>
</Grid>
</Grid>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid item xs={12} md={6}>
<Box sx={{
p: 2,
bgcolor: theme.palette.mode === 'light' ? 'grey.50' : 'grey.800',
borderRadius: 2,
border: theme.palette.mode === 'dark' ? '1px solid rgba(255,255,255,0.1)' : 'none',
}}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Average Wait Time
</Typography>
<Typography variant="h6">
{formatDuration(queueStats.avg_wait_time_minutes)}
</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{
p: 2,
bgcolor: theme.palette.mode === 'light' ? 'grey.50' : 'grey.800',
borderRadius: 2,
border: theme.palette.mode === 'dark' ? '1px solid rgba(255,255,255,0.1)' : 'none',
}}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Oldest Pending Item
</Typography>
<Typography variant="h6">
{formatDuration(queueStats.oldest_pending_minutes)}
</Typography>
</Box>
</Grid>
</Grid>
{lastRefresh && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
Last updated: {lastRefresh.toLocaleTimeString()}
</Typography>
)}
</CardContent>
</Card>
)}
{/* Processing Information */}
<Card>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<DescriptionIcon color="primary" />
How Watch Folder Works
</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
The watch folder system automatically monitors the configured directory for new files and processes them for OCR.
</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 1, color: 'primary.main' }}>
Processing Pipeline:
</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 0.5 }}>
1. <strong>File Detection:</strong> New files are detected using hybrid watching (inotify + polling)
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
2. <strong>Validation:</strong> Files are checked for supported format and size limits
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
3. <strong>Deduplication:</strong> System prevents processing of duplicate files
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
4. <strong>Storage:</strong> Files are moved to the document storage system
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
5. <strong>OCR Queue:</strong> Documents are queued for OCR processing with priority
</Typography>
</Box>
</Box>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
The system uses a hybrid watching strategy that automatically detects filesystem type and chooses
the optimal monitoring approach (inotify for local filesystems, polling for network mounts).
</Typography>
</Alert>
</CardContent>
</Card>
</Container>
);
};
export default WatchFolderPage;