feat(client): add new frontend page for admins to view running config settings

This commit is contained in:
perfectra1n 2025-07-12 14:06:09 -07:00
parent 83a55cbc32
commit 895c43fc61
2 changed files with 310 additions and 1 deletions

View File

@ -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<T extends (...args: any[]) => 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<ServerConfiguration | null>(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<void> => {
@ -473,6 +499,23 @@ const SettingsPage: React.FC = () => {
}
};
const fetchServerConfiguration = async (): Promise<void> => {
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 (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Typography variant="h4" sx={{ mb: 4 }}>
@ -484,6 +527,7 @@ const SettingsPage: React.FC = () => {
<Tab label="General" />
<Tab label="OCR Settings" />
<Tab label="User Management" />
<Tab label="Server Configuration" />
</Tabs>
<Box sx={{ p: 3 }}>
@ -1111,6 +1155,185 @@ const SettingsPage: React.FC = () => {
</TableContainer>
</Box>
)}
{tabValue === 3 && (
<Box>
<Typography variant="h6" sx={{ mb: 3 }}>
Server Configuration (Admin Only)
</Typography>
{configLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
</Box>
) : serverConfig ? (
<>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
File Upload Configuration
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Max File Size</Typography>
<Typography variant="h6">{serverConfig.max_file_size_mb} MB</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Upload Path</Typography>
<Typography variant="body1" sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{serverConfig.upload_path}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Allowed File Types</Typography>
<Box sx={{ mt: 1 }}>
{serverConfig.allowed_file_types.map((type) => (
<Chip key={type} label={type} size="small" sx={{ mr: 0.5, mb: 0.5 }} />
))}
</Box>
</Grid>
{serverConfig.watch_folder && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Watch Folder</Typography>
<Typography variant="body1" sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{serverConfig.watch_folder}
</Typography>
</Grid>
)}
</Grid>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
OCR Processing Configuration
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Concurrent OCR Jobs</Typography>
<Typography variant="h6">{serverConfig.concurrent_ocr_jobs}</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">OCR Timeout</Typography>
<Typography variant="h6">{serverConfig.ocr_timeout_seconds}s</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Memory Limit</Typography>
<Typography variant="h6">{serverConfig.memory_limit_mb} MB</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">OCR Language</Typography>
<Typography variant="h6">{serverConfig.ocr_language}</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">CPU Priority</Typography>
<Typography variant="h6" sx={{ textTransform: 'capitalize' }}>
{serverConfig.cpu_priority}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Background OCR</Typography>
<Chip
label={serverConfig.enable_background_ocr ? 'Enabled' : 'Disabled'}
color={serverConfig.enable_background_ocr ? 'success' : 'warning'}
size="small"
/>
</Grid>
</Grid>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
Server Information
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Server Host</Typography>
<Typography variant="body1" sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{serverConfig.server_host}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Server Port</Typography>
<Typography variant="h6">{serverConfig.server_port}</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">JWT Secret</Typography>
<Chip
label={serverConfig.jwt_secret_set ? 'Configured' : 'Not Set'}
color={serverConfig.jwt_secret_set ? 'success' : 'error'}
size="small"
/>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Version</Typography>
<Typography variant="h6">{serverConfig.version}</Typography>
</Grid>
{serverConfig.build_info && (
<Grid item xs={12}>
<Typography variant="body2" color="text.secondary">Build Information</Typography>
<Typography variant="body1" sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{serverConfig.build_info}
</Typography>
</Grid>
)}
</Grid>
</CardContent>
</Card>
{serverConfig.watch_interval_seconds && (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
Watch Folder Configuration
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Watch Interval</Typography>
<Typography variant="h6">{serverConfig.watch_interval_seconds}s</Typography>
</Grid>
{serverConfig.file_stability_check_ms && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">File Stability Check</Typography>
<Typography variant="h6">{serverConfig.file_stability_check_ms}ms</Typography>
</Grid>
)}
{serverConfig.max_file_age_hours && (
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">Max File Age</Typography>
<Typography variant="h6">{serverConfig.max_file_age_hours}h</Typography>
</Grid>
)}
</Grid>
</CardContent>
</Card>
)}
<Box sx={{ mt: 2 }}>
<Button
variant="outlined"
onClick={fetchServerConfiguration}
startIcon={<CloudSyncIcon />}
disabled={configLoading}
>
Refresh Configuration
</Button>
</Box>
</>
) : (
<Alert severity="error">
Failed to load server configuration. Admin access may be required.
</Alert>
)}
</Box>
)}
</Box>
</Paper>

View File

@ -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<Arc<AppState>> {
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<String>,
ocr_language: String,
allowed_file_types: Vec<String>,
watch_interval_seconds: Option<u64>,
file_stability_check_ms: Option<u64>,
max_file_age_hours: Option<u64>,
enable_background_ocr: bool,
version: String,
build_info: Option<String>,
}
#[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<Arc<AppState>>,
) -> Result<Json<ServerConfiguration>, 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::<u16>().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))
}