use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; const MAX_ALERTS: usize = 500; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Thresholds { pub cpu_percent: f64, pub ram_percent: f64, pub disk_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessConfig { pub name: String, pub pattern: String, pub memory_threshold_mb: f64, pub enabled: bool, pub alert_on_down: bool, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SmtpConfig { pub server: String, pub port: u16, pub use_tls: bool, pub username: String, pub password: String, pub from_email: String, pub to_emails: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserStatusThresholds { pub active_minutes: u64, pub inactive_minutes: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdminConfig { pub username: String, pub password_hash: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub secret_key: String, pub port: u16, pub check_interval_minutes: u64, pub alert_cooldown_minutes: u64, pub thresholds: Thresholds, pub processes: Vec, pub smtp: SmtpConfig, pub amadea_log_path: String, pub user_status_thresholds: UserStatusThresholds, pub admin: AdminConfig, } impl Default for Config { fn default() -> Self { Config { secret_key: generate_secret_key(), port: 5000, check_interval_minutes: 1, alert_cooldown_minutes: 30, thresholds: Thresholds { cpu_percent: 90.0, ram_percent: 85.0, disk_percent: 90.0, }, processes: vec![ ProcessConfig { name: "JVM".into(), pattern: "java".into(), memory_threshold_mb: 0.0, enabled: true, alert_on_down: true, }, ProcessConfig { name: "Nginx".into(), pattern: "nginx".into(), memory_threshold_mb: 0.0, enabled: false, alert_on_down: false, }, ProcessConfig { name: "Amadea Web 8 x64".into(), pattern: "amadea".into(), memory_threshold_mb: 0.0, enabled: true, alert_on_down: true, }, ], smtp: SmtpConfig { port: 587, use_tls: true, ..Default::default() }, amadea_log_path: r"C:\ProgramData\ISoft\Amadea Web 8 x64\data\logs".into(), user_status_thresholds: UserStatusThresholds { active_minutes: 5, inactive_minutes: 30, }, admin: AdminConfig { username: "admin".into(), password_hash: bcrypt::hash("admin", bcrypt::DEFAULT_COST) .unwrap_or_default(), }, } } } fn generate_secret_key() -> String { use rand::Rng; let mut rng = rand::thread_rng(); (0..32).map(|_| format!("{:02x}", rng.gen::())).collect() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Alert { pub timestamp: String, #[serde(rename = "type")] pub alert_type: String, pub key: String, pub message: String, pub value: f64, pub threshold: f64, pub hostname: String, } pub struct ConfigManager { config_file: PathBuf, alerts_file: PathBuf, pub config: Config, } impl ConfigManager { pub fn new() -> Self { let exe_dir = std::env::current_exe() .unwrap_or_default() .parent() .unwrap_or(Path::new(".")) .to_path_buf(); Self::new_with_dir(exe_dir.join("data")) } pub fn new_with_dir(data_dir: PathBuf) -> Self { fs::create_dir_all(&data_dir).ok(); let config_file = data_dir.join("config.json"); let alerts_file = data_dir.join("alerts.json"); let config = if config_file.exists() { fs::read_to_string(&config_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default() } else { let cfg = Config::default(); let json = serde_json::to_string_pretty(&cfg).unwrap_or_default(); fs::write(&config_file, &json).ok(); cfg }; ConfigManager { config_file, alerts_file, config, } } pub fn save(&self) { let json = serde_json::to_string_pretty(&self.config).unwrap_or_default(); fs::write(&self.config_file, json).ok(); } pub fn update(&mut self, config: Config) { self.config = config; self.save(); } pub fn load_alerts(&self) -> Vec { if !self.alerts_file.exists() { return vec![]; } fs::read_to_string(&self.alerts_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default() } pub fn save_alert(&self, alert: Alert) { let mut alerts = self.load_alerts(); alerts.insert(0, alert); alerts.truncate(MAX_ALERTS); let json = serde_json::to_string_pretty(&alerts).unwrap_or_default(); fs::write(&self.alerts_file, json).ok(); } pub fn clear_alerts(&self) { fs::write(&self.alerts_file, "[]").ok(); } } #[cfg(test)] mod tests { use super::*; #[test] fn default_config_has_expected_values() { let cfg = Config::default(); assert_eq!(cfg.port, 5000); assert_eq!(cfg.thresholds.cpu_percent, 90.0); assert_eq!(cfg.thresholds.ram_percent, 85.0); assert_eq!(cfg.thresholds.disk_percent, 90.0); assert_eq!(cfg.check_interval_minutes, 1); assert_eq!(cfg.alert_cooldown_minutes, 30); assert_eq!(cfg.processes.len(), 3); assert_eq!(cfg.admin.username, "admin"); } #[test] fn config_serializes_and_deserializes() { let cfg = Config::default(); let json = serde_json::to_string(&cfg).unwrap(); let cfg2: Config = serde_json::from_str(&json).unwrap(); assert_eq!(cfg.port, cfg2.port); assert_eq!(cfg.admin.username, cfg2.admin.username); } #[test] fn save_alert_truncates_at_500() { let dir = tempfile::tempdir().unwrap(); let cm = ConfigManager::new_with_dir(dir.path().to_path_buf()); for i in 0..510u32 { cm.save_alert(Alert { timestamp: format!("2026-01-01T00:00:{:02}", i % 60), alert_type: "threshold".into(), key: format!("cpu_{}", i), message: format!("msg {}", i), value: i as f64, threshold: 90.0, hostname: "test".into(), }); } let alerts = cm.load_alerts(); assert_eq!(alerts.len(), 500); } }