From 895c43fc61749da562a65dfb858ca4cf8a8d4527 Mon Sep 17 00:00:00 2001 From: perfectra1n Date: Sat, 12 Jul 2025 14:06:09 -0700 Subject: [PATCH] feat(client): add new frontend page for admins to view running config settings --- frontend/src/pages/SettingsPage.tsx | 223 ++++++++++++++++++++++++++++ src/routes/settings.rs | 88 ++++++++++- 2 files changed, 310 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index b3be669..e6402d9 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -140,6 +140,27 @@ interface WebDAVConnectionResult { server_type?: string; } +interface ServerConfiguration { + max_file_size_mb: number; + concurrent_ocr_jobs: number; + ocr_timeout_seconds: number; + memory_limit_mb: number; + cpu_priority: string; + server_host: string; + server_port: number; + jwt_secret_set: boolean; + upload_path: string; + watch_folder?: string; + ocr_language: string; + allowed_file_types: string[]; + watch_interval_seconds?: number; + file_stability_check_ms?: number; + max_file_age_hours?: number; + enable_background_ocr: boolean; + version: string; + build_info?: string; +} + // Debounce utility function function useDebounce any>(func: T, delay: number): T { @@ -232,6 +253,10 @@ const SettingsPage: React.FC = () => { // OCR Admin Controls State const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null); const [ocrActionLoading, setOcrActionLoading] = useState(false); + + // Server Configuration State + const [serverConfig, setServerConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(false); const ocrLanguages: OcrLanguage[] = [ { code: 'eng', name: 'English' }, @@ -255,6 +280,7 @@ const SettingsPage: React.FC = () => { fetchSettings(); fetchUsers(); fetchOcrStatus(); + fetchServerConfiguration(); }, []); const fetchSettings = async (): Promise => { @@ -473,6 +499,23 @@ const SettingsPage: React.FC = () => { } }; + const fetchServerConfiguration = async (): Promise => { + setConfigLoading(true); + try { + const response = await api.get('/settings/config'); + setServerConfig(response.data); + } catch (error: any) { + console.error('Error fetching server configuration:', error); + if (error.response?.status === 403) { + showSnackbar('Admin access required to view server configuration', 'error'); + } else if (error.response?.status !== 404) { + showSnackbar('Failed to load server configuration', 'error'); + } + } finally { + setConfigLoading(false); + } + }; + return ( @@ -484,6 +527,7 @@ const SettingsPage: React.FC = () => { + @@ -1111,6 +1155,185 @@ const SettingsPage: React.FC = () => { )} + + {tabValue === 3 && ( + + + Server Configuration (Admin Only) + + + {configLoading ? ( + + + + ) : serverConfig ? ( + <> + + + + File Upload Configuration + + + + + Max File Size + {serverConfig.max_file_size_mb} MB + + + Upload Path + + {serverConfig.upload_path} + + + + Allowed File Types + + {serverConfig.allowed_file_types.map((type) => ( + + ))} + + + {serverConfig.watch_folder && ( + + Watch Folder + + {serverConfig.watch_folder} + + + )} + + + + + + + + OCR Processing Configuration + + + + + Concurrent OCR Jobs + {serverConfig.concurrent_ocr_jobs} + + + OCR Timeout + {serverConfig.ocr_timeout_seconds}s + + + Memory Limit + {serverConfig.memory_limit_mb} MB + + + OCR Language + {serverConfig.ocr_language} + + + CPU Priority + + {serverConfig.cpu_priority} + + + + Background OCR + + + + + + + + + + Server Information + + + + + Server Host + + {serverConfig.server_host} + + + + Server Port + {serverConfig.server_port} + + + JWT Secret + + + + Version + {serverConfig.version} + + {serverConfig.build_info && ( + + Build Information + + {serverConfig.build_info} + + + )} + + + + + {serverConfig.watch_interval_seconds && ( + + + + Watch Folder Configuration + + + + + Watch Interval + {serverConfig.watch_interval_seconds}s + + {serverConfig.file_stability_check_ms && ( + + File Stability Check + {serverConfig.file_stability_check_ms}ms + + )} + {serverConfig.max_file_age_hours && ( + + Max File Age + {serverConfig.max_file_age_hours}h + + )} + + + + )} + + + + + + ) : ( + + Failed to load server configuration. Admin access may be required. + + )} + + )} diff --git a/src/routes/settings.rs b/src/routes/settings.rs index 4ca1bec..8e6d42d 100644 --- a/src/routes/settings.rs +++ b/src/routes/settings.rs @@ -9,13 +9,15 @@ use std::sync::Arc; use crate::{ auth::AuthUser, - models::{SettingsResponse, UpdateSettings}, + models::{SettingsResponse, UpdateSettings, UserRole}, AppState, }; +use serde::Serialize; pub fn router() -> Router> { Router::new() .route("/", get(get_settings).put(update_settings)) + .route("/config", get(get_server_configuration)) } #[utoipa::path( @@ -129,4 +131,88 @@ async fn update_settings( .map_err(|_| StatusCode::BAD_REQUEST)?; Ok(Json(settings.into())) +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "snake_case")] +struct ServerConfiguration { + max_file_size_mb: u64, + concurrent_ocr_jobs: i32, + ocr_timeout_seconds: i32, + memory_limit_mb: u64, + cpu_priority: String, + server_host: String, + server_port: u16, + jwt_secret_set: bool, + upload_path: String, + watch_folder: Option, + ocr_language: String, + allowed_file_types: Vec, + watch_interval_seconds: Option, + file_stability_check_ms: Option, + max_file_age_hours: Option, + enable_background_ocr: bool, + version: String, + build_info: Option, +} + +#[utoipa::path( + get, + path = "/api/settings/config", + tag = "settings", + security( + ("bearer_auth" = []) + ), + responses( + (status = 200, description = "Server configuration", body = ServerConfiguration), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Admin access required"), + (status = 500, description = "Internal server error") + ) +)] +async fn get_server_configuration( + auth_user: AuthUser, + State(state): State>, +) -> Result, StatusCode> { + // Only allow admin users to view server configuration + if auth_user.user.role != UserRole::Admin { + return Err(StatusCode::FORBIDDEN); + } + + let config = &state.config; + + // Get default settings for reference + let default_settings = crate::models::Settings::default(); + + // Parse server_address to get host and port + let (server_host, server_port) = if let Some(colon_pos) = config.server_address.rfind(':') { + let host = config.server_address[..colon_pos].to_string(); + let port = config.server_address[colon_pos + 1..].parse::().unwrap_or(8000); + (host, port) + } else { + (config.server_address.clone(), 8000) + }; + + let server_config = ServerConfiguration { + max_file_size_mb: config.max_file_size_mb, + concurrent_ocr_jobs: default_settings.concurrent_ocr_jobs, + ocr_timeout_seconds: default_settings.ocr_timeout_seconds, + memory_limit_mb: default_settings.memory_limit_mb as u64, + cpu_priority: default_settings.cpu_priority, + server_host, + server_port, + jwt_secret_set: !config.jwt_secret.is_empty(), + upload_path: config.upload_path.clone(), + watch_folder: Some(config.watch_folder.clone()), + ocr_language: default_settings.ocr_language, + allowed_file_types: default_settings.allowed_file_types, + watch_interval_seconds: config.watch_interval_seconds, + file_stability_check_ms: config.file_stability_check_ms, + max_file_age_hours: config.max_file_age_hours, + enable_background_ocr: default_settings.enable_background_ocr, + version: env!("CARGO_PKG_VERSION").to_string(), + build_info: option_env!("BUILD_INFO").map(|s| s.to_string()), + }; + + Ok(Json(server_config)) } \ No newline at end of file