use serde::{Serialize, Deserialize}; use std::fs::{self, create_dir_all}; use std::path::Path; use std::sync::{Arc, RwLock, OnceLock}; use crate::logger::get_logger; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MonitoredApp { /// Nombre de la aplicación pub name: String, /// Nombre del servicio systemd (ej: siax-app-TAREAS.service) #[serde(default)] pub service_name: String, /// Ruta completa al directorio de la aplicación (WorkingDirectory) #[serde(default)] pub path: String, /// Puerto donde escucha la aplicación pub port: i32, /// Archivo de entrada (ej: server.js, app.js) #[serde(default)] pub entry_point: String, /// Ruta completa al binario de node/python #[serde(default)] pub node_bin: String, /// Modo de ejecución (production, development, test) #[serde(default = "default_mode")] pub mode: String, /// Ruta completa al archivo .service de systemd #[serde(default)] pub service_file_path: String, /// Fecha de registro (ISO 8601) #[serde(default, skip_serializing_if = "String::is_empty", rename = "reg")] pub registered_at: String, // --- SOFT DELETE FIELDS --- /// Indica si la app fue eliminada (soft delete) #[serde(default)] pub deleted: bool, /// Fecha de eliminación (ISO 8601) #[serde(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, /// Razón de eliminación (opcional) #[serde(skip_serializing_if = "Option::is_none")] pub deleted_reason: Option, // DEPRECATED: Mantener por compatibilidad con versiones antiguas #[serde(skip_serializing_if = "Option::is_none")] pub systemd_service: Option, #[serde(skip_serializing_if = "Option::is_none")] pub created_at: Option, } fn default_mode() -> String { "production".to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub apps: Vec, } impl Default for AppConfig { fn default() -> Self { AppConfig { apps: vec![] } } } pub struct ConfigManager { config_path: String, config: Arc>, } impl ConfigManager { pub fn new(config_path: &str) -> Self { let logger = get_logger(); // Crear directorio config si no existe if let Some(parent) = Path::new(config_path).parent() { if !parent.exists() { logger.info("Config", &format!("Creando directorio: {}", parent.display())); match create_dir_all(parent) { Ok(_) => logger.info("Config", &format!("✅ Directorio creado: {}", parent.display())), Err(e) => logger.error("Config", "Error creando directorio", Some(&e.to_string())), } } } // Cargar o crear configuración let config = Self::load_config(config_path); ConfigManager { config_path: config_path.to_string(), config: Arc::new(RwLock::new(config)), } } fn load_config(path: &str) -> AppConfig { let logger = get_logger(); match fs::read_to_string(path) { Ok(content) => { match serde_json::from_str(&content) { Ok(config) => { let app_count = if let AppConfig { apps } = &config { apps.len() } else { 0 }; logger.info("Config", &format!("✅ Configuración cargada: {} apps desde {}", app_count, path)); config } Err(e) => { logger.error("Config", "Error parseando JSON, creando vacío", Some(&e.to_string())); let default_config = AppConfig::default(); let _ = Self::save_config_to_file(path, &default_config); default_config } } } Err(e) => { logger.warning("Config", &format!("Archivo no encontrado ({}), creando vacío en: {}", e.kind(), path), None); let default_config = AppConfig::default(); match Self::save_config_to_file(path, &default_config) { Ok(_) => logger.info("Config", &format!("✅ Archivo de configuración creado: {}", path)), Err(save_err) => logger.error("Config", "Error al crear archivo", Some(&save_err.to_string())), } default_config } } } fn save_config_to_file(path: &str, config: &AppConfig) -> std::io::Result<()> { let json = serde_json::to_string_pretty(config)?; fs::write(path, json)?; println!("💾 Configuración guardada en: {}", path); Ok(()) } /// Obtiene las apps activas (no eliminadas) pub fn get_apps(&self) -> Vec { let config = self.config.read().unwrap(); config.apps.iter() .filter(|app| !app.deleted) .cloned() .collect() } /// Obtiene TODAS las apps, incluyendo las eliminadas pub fn get_all_apps(&self) -> Vec { let config = self.config.read().unwrap(); config.apps.clone() } /// Obtiene solo las apps eliminadas pub fn get_deleted_apps(&self) -> Vec { let config = self.config.read().unwrap(); config.apps.iter() .filter(|app| app.deleted) .cloned() .collect() } /// Agrega una app con información completa pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> { let mut config = self.config.write().unwrap(); // Verificar si ya existe if config.apps.iter().any(|a| a.name == app.name) { return Err(format!("La app '{}' ya está siendo monitoreada", app.name)); } config.apps.push(app); // Guardar en disco match Self::save_config_to_file(&self.config_path, &config) { Ok(_) => Ok(()), Err(e) => Err(format!("Error al guardar configuración: {}", e)) } } /// Método simplificado para compatibilidad (DEPRECATED) #[deprecated(note = "Usar add_app_full() con MonitoredApp completo")] pub fn add_app(&self, name: String, port: i32) -> Result<(), String> { let service_name = format!("siax-app-{}.service", name); let registered_at = chrono::Local::now().to_rfc3339(); let app = MonitoredApp { name, service_name, path: String::new(), port, entry_point: String::new(), node_bin: String::new(), mode: "production".to_string(), service_file_path: String::new(), registered_at, deleted: false, deleted_at: None, deleted_reason: None, systemd_service: None, created_at: None, }; self.add_app_full(app) } /// Realiza un soft delete: marca la app como eliminada pero mantiene el registro pub fn soft_delete_app(&self, name: &str, reason: Option) -> Result<(), String> { let mut config = self.config.write().unwrap(); // Buscar la app let app = config.apps.iter_mut().find(|a| a.name == name && !a.deleted); match app { Some(app) => { // Marcar como eliminada app.deleted = true; app.deleted_at = Some(chrono::Local::now().to_rfc3339()); app.deleted_reason = reason; // Guardar en disco match Self::save_config_to_file(&self.config_path, &config) { Ok(_) => Ok(()), Err(e) => Err(format!("Error al guardar configuración: {}", e)) } } None => Err(format!("La app '{}' no se encontró o ya está eliminada", name)) } } /// Restaura una app previamente eliminada (soft delete) pub fn restore_app(&self, name: &str) -> Result<(), String> { let mut config = self.config.write().unwrap(); // Buscar la app eliminada let app = config.apps.iter_mut().find(|a| a.name == name && a.deleted); match app { Some(app) => { // Restaurar app.deleted = false; app.deleted_at = None; app.deleted_reason = None; // Guardar en disco match Self::save_config_to_file(&self.config_path, &config) { Ok(_) => Ok(()), Err(e) => Err(format!("Error al guardar configuración: {}", e)) } } None => Err(format!("La app '{}' no se encontró en apps eliminadas", name)) } } /// HARD DELETE: Elimina permanentemente una app del JSON (usar con precaución) pub fn remove_app(&self, name: &str) -> Result<(), String> { let mut config = self.config.write().unwrap(); let original_len = config.apps.len(); config.apps.retain(|app| app.name != name); if config.apps.len() == original_len { return Err(format!("La app '{}' no se encontró", name)); } // Guardar en disco match Self::save_config_to_file(&self.config_path, &config) { Ok(_) => Ok(()), Err(e) => Err(format!("Error al guardar configuración: {}", e)) } } pub fn get_arc(&self) -> Arc> { Arc::clone(&self.config) } } // Singleton global del ConfigManager static CONFIG_MANAGER: OnceLock = OnceLock::new(); /// Determina la ruta del archivo de configuración fn get_config_path() -> String { // Prioridad de rutas: // 1. Variable de entorno SIAX_CONFIG_PATH // 2. /opt/siax-agent/config/monitored_apps.json (producción) // 3. ./config/monitored_apps.json (desarrollo) if let Ok(env_path) = std::env::var("SIAX_CONFIG_PATH") { return env_path; } let prod_path = "/opt/siax-agent/config/monitored_apps.json"; if Path::new("/opt/siax-agent").exists() { return prod_path.to_string(); } "config/monitored_apps.json".to_string() } // ⚠️ IMPORTANTE: Esta función DEBE ser pública pub fn get_config_manager() -> &'static ConfigManager { CONFIG_MANAGER.get_or_init(|| { let config_path = get_config_path(); let logger = get_logger(); logger.info("Config", &format!("Usando archivo de configuración: {}", config_path)); ConfigManager::new(&config_path) }) }