feat(server/client): fix thumbnails and quick search

This commit is contained in:
perf3ct 2025-06-16 17:40:53 +00:00
parent d51f2793e9
commit 6f3aa771c0
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
7 changed files with 276 additions and 10 deletions

View File

@ -643,8 +643,6 @@ const GlobalSearchBar: React.FC<GlobalSearchBarProps> = ({ sx, ...props }) => {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%',
width: 0,
minWidth: 0,
flex: 1,
}}
>
@ -707,8 +705,6 @@ const GlobalSearchBar: React.FC<GlobalSearchBarProps> = ({ sx, ...props }) => {
fontSize: '0.7rem',
fontStyle: 'italic',
maxWidth: '100%',
width: 0,
minWidth: 0,
flex: 1,
}}
>

View File

@ -38,9 +38,10 @@ import {
} from '@mui/material';
import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon,
CloudSync as CloudSyncIcon, Folder as FolderIcon,
Assessment as AssessmentIcon, PlayArrow as PlayArrowIcon } from '@mui/icons-material';
Assessment as AssessmentIcon, PlayArrow as PlayArrowIcon,
Pause as PauseIcon, Stop as StopIcon } from '@mui/icons-material';
import { useAuth } from '../contexts/AuthContext';
import api from '../services/api';
import api, { queueService } from '../services/api';
interface User {
id: string;
@ -906,6 +907,10 @@ const SettingsPage: React.FC = () => {
email: '',
password: ''
});
// OCR Admin Controls State
const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null);
const [ocrActionLoading, setOcrActionLoading] = useState(false);
const ocrLanguages: OcrLanguage[] = [
{ code: 'eng', name: 'English' },
@ -928,6 +933,7 @@ const SettingsPage: React.FC = () => {
useEffect(() => {
fetchSettings();
fetchUsers();
fetchOcrStatus();
}, []);
const fetchSettings = async (): Promise<void> => {
@ -1108,6 +1114,52 @@ const SettingsPage: React.FC = () => {
handleSettingsChange('searchResultsPerPage', event.target.value);
};
const fetchOcrStatus = async (): Promise<void> => {
try {
const response = await queueService.getOcrStatus();
setOcrStatus(response.data);
} catch (error: any) {
console.error('Error fetching OCR status:', error);
// Don't show error for OCR status since it might not be available for non-admin users
}
};
const handlePauseOcr = async (): Promise<void> => {
setOcrActionLoading(true);
try {
await queueService.pauseOcr();
showSnackbar('OCR processing paused successfully', 'success');
fetchOcrStatus(); // Refresh status
} catch (error: any) {
console.error('Error pausing OCR:', error);
if (error.response?.status === 403) {
showSnackbar('Admin access required to pause OCR processing', 'error');
} else {
showSnackbar('Failed to pause OCR processing', 'error');
}
} finally {
setOcrActionLoading(false);
}
};
const handleResumeOcr = async (): Promise<void> => {
setOcrActionLoading(true);
try {
await queueService.resumeOcr();
showSnackbar('OCR processing resumed successfully', 'success');
fetchOcrStatus(); // Refresh status
} catch (error: any) {
console.error('Error resuming OCR:', error);
if (error.response?.status === 403) {
showSnackbar('Admin access required to resume OCR processing', 'error');
} else {
showSnackbar('Failed to resume OCR processing', 'error');
}
} finally {
setOcrActionLoading(false);
}
};
return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Typography variant="h4" sx={{ mb: 4 }}>
@ -1196,6 +1248,69 @@ const SettingsPage: React.FC = () => {
</CardContent>
</Card>
{/* Admin OCR Controls */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
<StopIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
OCR Processing Controls (Admin Only)
</Typography>
<Divider sx={{ mb: 2 }} />
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
Control OCR processing to manage CPU usage and allow users to use the application without performance impact.
</Typography>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
variant={ocrStatus?.is_paused ? "outlined" : "contained"}
color={ocrStatus?.is_paused ? "success" : "warning"}
startIcon={ocrActionLoading ? <CircularProgress size={16} /> :
(ocrStatus?.is_paused ? <PlayArrowIcon /> : <PauseIcon />)}
onClick={ocrStatus?.is_paused ? handleResumeOcr : handlePauseOcr}
disabled={ocrActionLoading || loading}
size="large"
>
{ocrActionLoading ? 'Processing...' :
ocrStatus?.is_paused ? 'Resume OCR Processing' : 'Pause OCR Processing'}
</Button>
</Box>
</Grid>
<Grid item xs={12} md={6}>
{ocrStatus && (
<Box>
<Chip
label={`OCR Status: ${ocrStatus.status.toUpperCase()}`}
color={ocrStatus.is_paused ? "warning" : "success"}
variant="outlined"
icon={ocrStatus.is_paused ? <PauseIcon /> : <PlayArrowIcon />}
size="medium"
/>
<Typography variant="caption" sx={{ display: 'block', mt: 1, color: 'text.secondary' }}>
{ocrStatus.is_paused
? 'OCR processing is paused. No new jobs will be processed.'
: 'OCR processing is active. Documents will be processed automatically.'}
</Typography>
</Box>
)}
</Grid>
</Grid>
{ocrStatus?.is_paused && (
<Alert severity="warning" sx={{ mt: 2 }}>
<Typography variant="body2">
<strong>OCR Processing Paused</strong><br />
New documents will not be processed for OCR text extraction until processing is resumed.
Users can still upload and view documents, but search functionality may be limited.
</Typography>
</Alert>
)}
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>

View File

@ -184,6 +184,16 @@ export const documentService = {
},
}
export interface OcrStatusResponse {
is_paused: boolean
status: 'paused' | 'running'
}
export interface OcrActionResponse {
status: 'paused' | 'resumed'
message: string
}
export const queueService = {
getStats: () => {
return api.get<QueueStats>('/queue/stats')
@ -192,4 +202,16 @@ export const queueService = {
requeueFailed: () => {
return api.post('/queue/requeue-failed')
},
getOcrStatus: () => {
return api.get<OcrStatusResponse>('/queue/status')
},
pauseOcr: () => {
return api.post<OcrActionResponse>('/queue/pause')
},
resumeOcr: () => {
return api.post<OcrActionResponse>('/queue/resume')
},
}

View File

@ -152,9 +152,22 @@ impl FileService {
let img = image::load_from_memory(file_data)?;
let thumbnail = img.resize(200, 200, FilterType::Lanczos3);
// Convert to RGB if the image has an alpha channel (RGBA)
// JPEG doesn't support transparency, so we need to remove the alpha channel
let rgb_thumbnail = match thumbnail {
image::DynamicImage::ImageRgba8(_) => {
// Convert RGBA to RGB by compositing against a white background
let rgb_img = image::DynamicImage::ImageRgb8(
thumbnail.to_rgb8()
);
rgb_img
},
_ => thumbnail, // Already RGB or other compatible format
};
let mut buffer = Vec::new();
let mut cursor = std::io::Cursor::new(&mut buffer);
thumbnail.write_to(&mut cursor, ImageFormat::Jpeg)?;
rgb_thumbnail.write_to(&mut cursor, ImageFormat::Jpeg)?;
Ok(buffer)
}

View File

@ -3,6 +3,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool, Row};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Semaphore;
use tokio::time::{sleep, Duration};
use tracing::{error, info, warn};
@ -45,6 +46,7 @@ pub struct OcrQueueService {
worker_id: String,
transaction_manager: DocumentTransactionManager,
processing_throttler: Arc<RequestThrottler>,
is_paused: Arc<AtomicBool>,
}
impl OcrQueueService {
@ -67,6 +69,7 @@ impl OcrQueueService {
worker_id,
transaction_manager,
processing_throttler,
is_paused: Arc::new(AtomicBool::new(false)),
}
}
@ -401,6 +404,23 @@ impl OcrQueueService {
Ok(())
}
/// Pause OCR processing
pub fn pause(&self) {
self.is_paused.store(true, Ordering::SeqCst);
info!("OCR processing paused for worker {}", self.worker_id);
}
/// Resume OCR processing
pub fn resume(&self) {
self.is_paused.store(false, Ordering::SeqCst);
info!("OCR processing resumed for worker {}", self.worker_id);
}
/// Check if OCR processing is paused
pub fn is_paused(&self) -> bool {
self.is_paused.load(Ordering::SeqCst)
}
/// Start the worker loop
pub async fn start_worker(self: Arc<Self>) -> Result<()> {
let semaphore = Arc::new(Semaphore::new(self.max_concurrent_jobs));
@ -412,6 +432,13 @@ impl OcrQueueService {
);
loop {
// Check if processing is paused
if self.is_paused() {
info!("OCR processing is paused, waiting...");
sleep(Duration::from_secs(5)).await;
continue;
}
// Check for items to process
match self.dequeue().await {
Ok(Some(item)) => {

View File

@ -360,8 +360,9 @@ async fn get_document_thumbnail(
.body(thumbnail_data.into())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?)
}
Err(_) => {
// Return a placeholder thumbnail or 404
Err(e) => {
// Log the error for debugging
tracing::error!("Failed to generate thumbnail for document {}: {}", document_id, e);
Err(StatusCode::NOT_FOUND)
}
}

View File

@ -7,12 +7,23 @@ use axum::{
};
use std::sync::Arc;
use crate::{auth::AuthUser, ocr_queue::OcrQueueService, AppState};
use crate::{auth::AuthUser, ocr_queue::OcrQueueService, AppState, models::UserRole};
fn require_admin(auth_user: &AuthUser) -> Result<(), StatusCode> {
if auth_user.user.role != UserRole::Admin {
Err(StatusCode::FORBIDDEN)
} else {
Ok(())
}
}
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/stats", get(get_queue_stats))
.route("/requeue-failed", post(requeue_failed))
.route("/pause", post(pause_ocr_processing))
.route("/resume", post(resume_ocr_processing))
.route("/status", get(get_ocr_status))
}
#[utoipa::path(
@ -75,4 +86,85 @@ async fn requeue_failed(
Ok(Json(serde_json::json!({
"requeued_count": count,
})))
}
#[utoipa::path(
post,
path = "/api/queue/pause",
tag = "queue",
security(
("bearer_auth" = [])
),
responses(
(status = 200, description = "OCR processing paused successfully"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - Admin access required")
)
)]
async fn pause_ocr_processing(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
) -> Result<Json<serde_json::Value>, StatusCode> {
require_admin(&auth_user)?;
state.queue_service.pause();
Ok(Json(serde_json::json!({
"status": "paused",
"message": "OCR processing has been paused"
})))
}
#[utoipa::path(
post,
path = "/api/queue/resume",
tag = "queue",
security(
("bearer_auth" = [])
),
responses(
(status = 200, description = "OCR processing resumed successfully"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - Admin access required")
)
)]
async fn resume_ocr_processing(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
) -> Result<Json<serde_json::Value>, StatusCode> {
require_admin(&auth_user)?;
state.queue_service.resume();
Ok(Json(serde_json::json!({
"status": "resumed",
"message": "OCR processing has been resumed"
})))
}
#[utoipa::path(
get,
path = "/api/queue/status",
tag = "queue",
security(
("bearer_auth" = [])
),
responses(
(status = 200, description = "OCR processing status"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - Admin access required")
)
)]
async fn get_ocr_status(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
) -> Result<Json<serde_json::Value>, StatusCode> {
require_admin(&auth_user)?;
let is_paused = state.queue_service.is_paused();
Ok(Json(serde_json::json!({
"is_paused": is_paused,
"status": if is_paused { "paused" } else { "running" }
})))
}