feat(server/client): fix thumbnails and quick search
This commit is contained in:
parent
4aa4359064
commit
af7129da0a
|
|
@ -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,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -907,6 +908,10 @@ const SettingsPage: React.FC = () => {
|
|||
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' },
|
||||
{ code: 'spa', name: 'Spanish' },
|
||||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
},
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -76,3 +87,84 @@ async fn requeue_failed(
|
|||
"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" }
|
||||
})))
|
||||
}
|
||||
Loading…
Reference in New Issue