feat(client): add new frontend page for admins to view running config settings
This commit is contained in:
parent
83a55cbc32
commit
895c43fc61
|
|
@ -140,6 +140,27 @@ interface WebDAVConnectionResult {
|
||||||
server_type?: string;
|
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
|
// Debounce utility function
|
||||||
function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number): T {
|
function useDebounce<T extends (...args: any[]) => any>(func: T, delay: number): T {
|
||||||
|
|
@ -233,6 +254,10 @@ const SettingsPage: React.FC = () => {
|
||||||
const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null);
|
const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null);
|
||||||
const [ocrActionLoading, setOcrActionLoading] = useState(false);
|
const [ocrActionLoading, setOcrActionLoading] = useState(false);
|
||||||
|
|
||||||
|
// Server Configuration State
|
||||||
|
const [serverConfig, setServerConfig] = useState<ServerConfiguration | null>(null);
|
||||||
|
const [configLoading, setConfigLoading] = useState(false);
|
||||||
|
|
||||||
const ocrLanguages: OcrLanguage[] = [
|
const ocrLanguages: OcrLanguage[] = [
|
||||||
{ code: 'eng', name: 'English' },
|
{ code: 'eng', name: 'English' },
|
||||||
{ code: 'spa', name: 'Spanish' },
|
{ code: 'spa', name: 'Spanish' },
|
||||||
|
|
@ -255,6 +280,7 @@ const SettingsPage: React.FC = () => {
|
||||||
fetchSettings();
|
fetchSettings();
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchOcrStatus();
|
fetchOcrStatus();
|
||||||
|
fetchServerConfiguration();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchSettings = async (): Promise<void> => {
|
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 (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||||
<Typography variant="h4" sx={{ mb: 4 }}>
|
<Typography variant="h4" sx={{ mb: 4 }}>
|
||||||
|
|
@ -484,6 +527,7 @@ const SettingsPage: React.FC = () => {
|
||||||
<Tab label="General" />
|
<Tab label="General" />
|
||||||
<Tab label="OCR Settings" />
|
<Tab label="OCR Settings" />
|
||||||
<Tab label="User Management" />
|
<Tab label="User Management" />
|
||||||
|
<Tab label="Server Configuration" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
|
|
@ -1111,6 +1155,185 @@ const SettingsPage: React.FC = () => {
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthUser,
|
auth::AuthUser,
|
||||||
models::{SettingsResponse, UpdateSettings},
|
models::{SettingsResponse, UpdateSettings, UserRole},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(get_settings).put(update_settings))
|
.route("/", get(get_settings).put(update_settings))
|
||||||
|
.route("/config", get(get_server_configuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
|
|
@ -130,3 +132,87 @@ async fn update_settings(
|
||||||
|
|
||||||
Ok(Json(settings.into()))
|
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))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue