diff --git a/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx b/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx index 3946248..d622855 100644 --- a/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx +++ b/frontend/src/components/GlobalSearchBar/GlobalSearchBar.tsx @@ -643,8 +643,6 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '100%', - width: 0, - minWidth: 0, flex: 1, }} > @@ -707,8 +705,6 @@ const GlobalSearchBar: React.FC = ({ sx, ...props }) => { fontSize: '0.7rem', fontStyle: 'italic', maxWidth: '100%', - width: 0, - minWidth: 0, flex: 1, }} > diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 9db313c..bbc6128 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -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 => { @@ -1108,6 +1114,52 @@ const SettingsPage: React.FC = () => { handleSettingsChange('searchResultsPerPage', event.target.value); }; + const fetchOcrStatus = async (): Promise => { + 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 => { + 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 => { + 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 ( @@ -1196,6 +1248,69 @@ const SettingsPage: React.FC = () => { + {/* Admin OCR Controls */} + + + + + OCR Processing Controls (Admin Only) + + + + + Control OCR processing to manage CPU usage and allow users to use the application without performance impact. + + + + + + + + + + + {ocrStatus && ( + + : } + size="medium" + /> + + {ocrStatus.is_paused + ? 'OCR processing is paused. No new jobs will be processed.' + : 'OCR processing is active. Documents will be processed automatically.'} + + + )} + + + + {ocrStatus?.is_paused && ( + + + OCR Processing Paused
+ 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. +
+
+ )} +
+
+ diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index eeb3442..2785526 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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('/queue/stats') @@ -192,4 +202,16 @@ export const queueService = { requeueFailed: () => { return api.post('/queue/requeue-failed') }, + + getOcrStatus: () => { + return api.get('/queue/status') + }, + + pauseOcr: () => { + return api.post('/queue/pause') + }, + + resumeOcr: () => { + return api.post('/queue/resume') + }, } \ No newline at end of file diff --git a/src/file_service.rs b/src/file_service.rs index 41707b8..08f75cc 100644 --- a/src/file_service.rs +++ b/src/file_service.rs @@ -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) } diff --git a/src/ocr_queue.rs b/src/ocr_queue.rs index fd5c76d..8c73325 100644 --- a/src/ocr_queue.rs +++ b/src/ocr_queue.rs @@ -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, + is_paused: Arc, } 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) -> 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)) => { diff --git a/src/routes/documents.rs b/src/routes/documents.rs index 6a14cba..4216543 100644 --- a/src/routes/documents.rs +++ b/src/routes/documents.rs @@ -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) } } diff --git a/src/routes/queue.rs b/src/routes/queue.rs index a91cd5c..dc248ac 100644 --- a/src/routes/queue.rs +++ b/src/routes/queue.rs @@ -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> { 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>, + auth_user: AuthUser, +) -> Result, 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>, + auth_user: AuthUser, +) -> Result, 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>, + auth_user: AuthUser, +) -> Result, 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" } + }))) } \ No newline at end of file