feat: Implementación completa de Soft Delete para apps

- Agregados campos deleted, deleted_at, deleted_reason a MonitoredApp
- Implementado soft_delete_app() y restore_app() en ConfigManager
- Modificado get_apps() para filtrar apps eliminadas por defecto
- Agregados métodos get_all_apps() y get_deleted_apps()
- Actualizado unregister_app() para usar soft delete en lugar de hard delete
- Creados endpoints:
  * GET /api/apps/deleted - Ver historial de apps eliminadas
  * POST /api/apps/:name/restore - Restaurar app eliminada
- Agregada sección de Historial en index.html con UI completa
- Botón de restaurar para cada app eliminada
- El servicio systemd se elimina físicamente, solo el JSON mantiene historial
- Permite auditoría y recuperación de apps eliminadas accidentalmente
This commit is contained in:
2026-01-19 19:40:47 -05:00
parent 60f38be957
commit 13b36dda5f
6 changed files with 365 additions and 7 deletions

View File

@@ -40,6 +40,19 @@ pub struct MonitoredApp {
#[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<String>,
/// Razón de eliminación (opcional)
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_reason: Option<String>,
// DEPRECATED: Mantener por compatibilidad con versiones antiguas
#[serde(skip_serializing_if = "Option::is_none")]
pub systemd_service: Option<String>,
@@ -131,11 +144,30 @@ impl ConfigManager {
Ok(())
}
/// Obtiene las apps activas (no eliminadas)
pub fn get_apps(&self) -> Vec<MonitoredApp> {
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<MonitoredApp> {
let config = self.config.read().unwrap();
config.apps.clone()
}
/// Obtiene solo las apps eliminadas
pub fn get_deleted_apps(&self) -> Vec<MonitoredApp> {
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();
@@ -170,6 +202,9 @@ impl ConfigManager {
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,
};
@@ -177,6 +212,55 @@ impl ConfigManager {
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<String>) -> 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();