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:
@@ -358,3 +358,52 @@ pub async fn get_system_error_logs() -> Result<Json<serde_json::Value>, StatusCo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Endpoint para obtener apps eliminadas (soft delete history)
|
||||
pub async fn get_deleted_apps_handler() -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
use crate::config::get_config_manager;
|
||||
|
||||
let config_manager = get_config_manager();
|
||||
let deleted_apps = config_manager.get_deleted_apps();
|
||||
|
||||
// Formatear respuesta con información de cada app eliminada
|
||||
let apps_info: Vec<serde_json::Value> = deleted_apps.iter().map(|app| {
|
||||
serde_json::json!({
|
||||
"name": app.name,
|
||||
"port": app.port,
|
||||
"path": app.path,
|
||||
"entry_point": app.entry_point,
|
||||
"mode": app.mode,
|
||||
"registered_at": app.registered_at,
|
||||
"deleted_at": app.deleted_at,
|
||||
"deleted_reason": app.deleted_reason,
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"apps": apps_info,
|
||||
"total": apps_info.len()
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Endpoint para restaurar una app eliminada (soft delete)
|
||||
pub async fn restore_app_handler(
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
use crate::config::get_config_manager;
|
||||
|
||||
let config_manager = get_config_manager();
|
||||
|
||||
match config_manager.restore_app(&app_name) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: app_name.clone(),
|
||||
operation: "restore".to_string(),
|
||||
success: true,
|
||||
message: format!("Aplicación '{}' restaurada exitosamente. Nota: el servicio systemd debe ser recreado manualmente.", app_name),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -257,6 +257,9 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
|
||||
mode: service.node_env,
|
||||
service_file_path: service.service_file.clone(),
|
||||
registered_at,
|
||||
deleted: false,
|
||||
deleted_at: None,
|
||||
deleted_reason: None,
|
||||
systemd_service: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
@@ -70,11 +70,13 @@ async fn main() {
|
||||
.route("/api/monitored", get(api::get_monitored_apps_handler))
|
||||
.route("/api/logs/errors", get(api::get_system_error_logs))
|
||||
.route("/api/apps", get(api::list_apps_handler).post(api::register_app_handler))
|
||||
.route("/api/apps/deleted", get(api::get_deleted_apps_handler))
|
||||
.route("/api/apps/:name", delete(api::unregister_app_handler))
|
||||
.route("/api/apps/:name/status", get(api::get_app_status_handler))
|
||||
.route("/api/apps/:name/start", post(api::start_app_handler))
|
||||
.route("/api/apps/:name/stop", post(api::stop_app_handler))
|
||||
.route("/api/apps/:name/restart", post(api::restart_app_handler))
|
||||
.route("/api/apps/:name/restore", post(api::restore_app_handler))
|
||||
.route("/api/scan", get(api::scan_processes_handler))
|
||||
.with_state(api_state);
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ impl AppManager {
|
||||
mode,
|
||||
service_file_path,
|
||||
registered_at,
|
||||
deleted: false,
|
||||
deleted_at: None,
|
||||
deleted_reason: None,
|
||||
systemd_service: None,
|
||||
created_at: None,
|
||||
};
|
||||
@@ -104,7 +107,7 @@ impl AppManager {
|
||||
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
logger.info("AppManager", &format!("Desregistrando aplicación: {}", app_name));
|
||||
logger.info("AppManager", &format!("Desregistrando aplicación (soft delete): {}", app_name));
|
||||
|
||||
// Obtener configuración
|
||||
let config = self.apps.get(app_name)
|
||||
@@ -119,7 +122,7 @@ impl AppManager {
|
||||
// Deshabilitar el servicio
|
||||
let _ = SystemCtl::disable(&service_name);
|
||||
|
||||
// Eliminar archivo de servicio
|
||||
// Eliminar archivo de servicio (físicamente)
|
||||
ServiceGenerator::delete_service_file(&service_name)?;
|
||||
|
||||
// Recargar daemon
|
||||
@@ -128,13 +131,14 @@ impl AppManager {
|
||||
// Eliminar de memoria
|
||||
self.apps.remove(app_name);
|
||||
|
||||
// Eliminar de monitored_apps.json
|
||||
// SOFT DELETE en monitored_apps.json (mantener historial)
|
||||
let config_manager = get_config_manager();
|
||||
if let Err(e) = config_manager.remove_app(app_name) {
|
||||
logger.warning("AppManager", "No se pudo eliminar de monitored_apps.json", Some(&e));
|
||||
let delete_reason = Some("Eliminada desde el panel de control".to_string());
|
||||
if let Err(e) = config_manager.soft_delete_app(app_name, delete_reason) {
|
||||
logger.warning("AppManager", "No se pudo hacer soft delete en monitored_apps.json", Some(&e));
|
||||
}
|
||||
|
||||
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente", app_name));
|
||||
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente (soft delete)", app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user