Compare commits
3 Commits
e850a081f4
...
13b36dda5f
| Author | SHA1 | Date | |
|---|---|---|---|
| 13b36dda5f | |||
| 60f38be957 | |||
| 6ab43980aa |
1914
logs/errors.log
1914
logs/errors.log
File diff suppressed because it is too large
Load Diff
@@ -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")]
|
#[serde(default, skip_serializing_if = "String::is_empty", rename = "reg")]
|
||||||
pub registered_at: String,
|
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
|
// DEPRECATED: Mantener por compatibilidad con versiones antiguas
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub systemd_service: Option<String>,
|
pub systemd_service: Option<String>,
|
||||||
@@ -131,11 +144,30 @@ impl ConfigManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtiene las apps activas (no eliminadas)
|
||||||
pub fn get_apps(&self) -> Vec<MonitoredApp> {
|
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();
|
let config = self.config.read().unwrap();
|
||||||
config.apps.clone()
|
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
|
/// Agrega una app con información completa
|
||||||
pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> {
|
pub fn add_app_full(&self, app: MonitoredApp) -> Result<(), String> {
|
||||||
let mut config = self.config.write().unwrap();
|
let mut config = self.config.write().unwrap();
|
||||||
@@ -170,6 +202,9 @@ impl ConfigManager {
|
|||||||
mode: "production".to_string(),
|
mode: "production".to_string(),
|
||||||
service_file_path: String::new(),
|
service_file_path: String::new(),
|
||||||
registered_at,
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
systemd_service: None,
|
systemd_service: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
};
|
};
|
||||||
@@ -177,6 +212,55 @@ impl ConfigManager {
|
|||||||
self.add_app_full(app)
|
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> {
|
pub fn remove_app(&self, name: &str) -> Result<(), String> {
|
||||||
let mut config = self.config.write().unwrap();
|
let mut config = self.config.write().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
|
|||||||
mode: service.node_env,
|
mode: service.node_env,
|
||||||
service_file_path: service.service_file.clone(),
|
service_file_path: service.service_file.clone(),
|
||||||
registered_at,
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
systemd_service: None,
|
systemd_service: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,11 +70,13 @@ async fn main() {
|
|||||||
.route("/api/monitored", get(api::get_monitored_apps_handler))
|
.route("/api/monitored", get(api::get_monitored_apps_handler))
|
||||||
.route("/api/logs/errors", get(api::get_system_error_logs))
|
.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", 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", delete(api::unregister_app_handler))
|
||||||
.route("/api/apps/:name/status", get(api::get_app_status_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/start", post(api::start_app_handler))
|
||||||
.route("/api/apps/:name/stop", post(api::stop_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/restart", post(api::restart_app_handler))
|
||||||
|
.route("/api/apps/:name/restore", post(api::restore_app_handler))
|
||||||
.route("/api/scan", get(api::scan_processes_handler))
|
.route("/api/scan", get(api::scan_processes_handler))
|
||||||
.with_state(api_state);
|
.with_state(api_state);
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ impl AppManager {
|
|||||||
mode,
|
mode,
|
||||||
service_file_path,
|
service_file_path,
|
||||||
registered_at,
|
registered_at,
|
||||||
|
deleted: false,
|
||||||
|
deleted_at: None,
|
||||||
|
deleted_reason: None,
|
||||||
systemd_service: None,
|
systemd_service: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
};
|
};
|
||||||
@@ -104,7 +107,7 @@ impl AppManager {
|
|||||||
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
||||||
let logger = get_logger();
|
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
|
// Obtener configuración
|
||||||
let config = self.apps.get(app_name)
|
let config = self.apps.get(app_name)
|
||||||
@@ -119,7 +122,7 @@ impl AppManager {
|
|||||||
// Deshabilitar el servicio
|
// Deshabilitar el servicio
|
||||||
let _ = SystemCtl::disable(&service_name);
|
let _ = SystemCtl::disable(&service_name);
|
||||||
|
|
||||||
// Eliminar archivo de servicio
|
// Eliminar archivo de servicio (físicamente)
|
||||||
ServiceGenerator::delete_service_file(&service_name)?;
|
ServiceGenerator::delete_service_file(&service_name)?;
|
||||||
|
|
||||||
// Recargar daemon
|
// Recargar daemon
|
||||||
@@ -128,13 +131,14 @@ impl AppManager {
|
|||||||
// Eliminar de memoria
|
// Eliminar de memoria
|
||||||
self.apps.remove(app_name);
|
self.apps.remove(app_name);
|
||||||
|
|
||||||
// Eliminar de monitored_apps.json
|
// SOFT DELETE en monitored_apps.json (mantener historial)
|
||||||
let config_manager = get_config_manager();
|
let config_manager = get_config_manager();
|
||||||
if let Err(e) = config_manager.remove_app(app_name) {
|
let delete_reason = Some("Eliminada desde el panel de control".to_string());
|
||||||
logger.warning("AppManager", "No se pudo eliminar de monitored_apps.json", Some(&e));
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
384
web/index.html
384
web/index.html
@@ -397,6 +397,100 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Historial de Apps Eliminadas Section -->
|
||||||
|
<div
|
||||||
|
id="deleted-apps-section"
|
||||||
|
class="mt-10 bg-white dark:bg-slate-800/30 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="p-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="size-10 rounded-full bg-red-500/10 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-red-500"
|
||||||
|
>history</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="text-slate-900 dark:text-white text-xl font-bold"
|
||||||
|
>
|
||||||
|
Historial de Apps Eliminadas
|
||||||
|
</h2>
|
||||||
|
<p class="text-slate-500 text-sm">
|
||||||
|
Aplicaciones eliminadas que pueden ser
|
||||||
|
restauradas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick="toggleDeletedApps()"
|
||||||
|
class="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>expand_more</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="deleted-apps-content" class="hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-slate-50 dark:bg-slate-800/50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Aplicación
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Puerto
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Eliminada
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Razón
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-right text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Acciones
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
id="deleted-apps-list"
|
||||||
|
class="divide-y divide-slate-200 dark:divide-slate-800"
|
||||||
|
>
|
||||||
|
<!-- Deleted apps will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="deleted-apps-empty"
|
||||||
|
class="hidden p-8 text-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-6xl text-slate-300 dark:text-slate-700 mb-3"
|
||||||
|
>check_circle</span
|
||||||
|
>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400">
|
||||||
|
No hay apps eliminadas en el historial
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Action Links -->
|
<!-- Quick Action Links -->
|
||||||
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div
|
<div
|
||||||
@@ -471,6 +565,117 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Confirmación para Eliminar -->
|
||||||
|
<div
|
||||||
|
id="delete-modal"
|
||||||
|
class="hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full border border-slate-200 dark:border-slate-700 animate-in fade-in zoom-in duration-200"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 p-6 border-b border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="size-12 rounded-full bg-red-500/10 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-red-500 text-2xl"
|
||||||
|
>delete_forever</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-bold text-slate-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Eliminar Aplicación
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-slate-500">
|
||||||
|
Esta acción no se puede deshacer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<div
|
||||||
|
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-sm text-slate-700 dark:text-slate-300 mb-3"
|
||||||
|
>
|
||||||
|
¿Estás seguro de eliminar
|
||||||
|
<strong
|
||||||
|
id="delete-app-name"
|
||||||
|
class="text-red-600 dark:text-red-400"
|
||||||
|
></strong
|
||||||
|
>?
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-xs text-slate-600 dark:text-slate-400 font-medium mb-2"
|
||||||
|
>
|
||||||
|
Esta acción eliminará:
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
class="text-xs text-slate-600 dark:text-slate-400 space-y-1.5"
|
||||||
|
>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Servicio systemd (siax-app-*.service)
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Archivo de configuración en /etc/systemd/system/
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Registro en monitored_apps.json
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined text-[14px] text-red-500"
|
||||||
|
>close</span
|
||||||
|
>
|
||||||
|
Historial de monitoreo
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div
|
||||||
|
class="flex gap-3 p-6 border-t border-slate-200 dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick="closeDeleteModal()"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-lg border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 font-medium hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="confirmDelete()"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-[18px]"
|
||||||
|
>delete</span
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function loadApps() {
|
async function loadApps() {
|
||||||
try {
|
try {
|
||||||
@@ -585,6 +790,17 @@
|
|||||||
title="Ver logs">
|
title="Ver logs">
|
||||||
<span class="material-symbols-outlined text-[20px]">visibility</span>
|
<span class="material-symbols-outlined text-[20px]">visibility</span>
|
||||||
</button>
|
</button>
|
||||||
|
${
|
||||||
|
app.status !== "Running"
|
||||||
|
? `
|
||||||
|
<button class="text-red-500 hover:text-red-400 transition-colors p-1.5 rounded hover:bg-red-900/20"
|
||||||
|
onclick="openDeleteModal('${app.name}')"
|
||||||
|
title="Eliminar">
|
||||||
|
<span class="material-symbols-outlined text-[20px]">delete</span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -643,12 +859,178 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modal de confirmación para eliminar
|
||||||
|
let appToDelete = null;
|
||||||
|
|
||||||
|
function openDeleteModal(appName) {
|
||||||
|
appToDelete = appName;
|
||||||
|
document.getElementById("delete-app-name").textContent =
|
||||||
|
appName;
|
||||||
|
document
|
||||||
|
.getElementById("delete-modal")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
appToDelete = null;
|
||||||
|
document.getElementById("delete-modal").classList.add("hidden");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!appToDelete) return;
|
||||||
|
|
||||||
|
const appName = appToDelete;
|
||||||
|
closeDeleteModal();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/apps/${appName}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.data.message}`);
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps(); // Recargar historial de eliminadas
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
alert("❌ Error al eliminar la aplicación");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleMenu() {
|
function toggleMenu() {
|
||||||
const menu = document.getElementById("mobile-menu");
|
const menu = document.getElementById("mobile-menu");
|
||||||
menu.classList.toggle("hidden");
|
menu.classList.toggle("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", loadApps);
|
// Funciones para apps eliminadas (soft delete)
|
||||||
|
async function loadDeletedApps() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/apps/deleted");
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!result.success ||
|
||||||
|
!result.data ||
|
||||||
|
!result.data.apps ||
|
||||||
|
result.data.apps.length === 0
|
||||||
|
) {
|
||||||
|
// No hay apps eliminadas, ocultar sección
|
||||||
|
document
|
||||||
|
.getElementById("deleted-apps-section")
|
||||||
|
.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar sección si hay apps eliminadas
|
||||||
|
document
|
||||||
|
.getElementById("deleted-apps-section")
|
||||||
|
.classList.remove("hidden");
|
||||||
|
|
||||||
|
const deletedAppsList =
|
||||||
|
document.getElementById("deleted-apps-list");
|
||||||
|
const emptyMessage =
|
||||||
|
document.getElementById("deleted-apps-empty");
|
||||||
|
|
||||||
|
deletedAppsList.innerHTML = result.data.apps
|
||||||
|
.map((app) => {
|
||||||
|
const deletedDate = app.deleted_at
|
||||||
|
? new Date(app.deleted_at).toLocaleString(
|
||||||
|
"es-ES",
|
||||||
|
)
|
||||||
|
: "Desconocida";
|
||||||
|
const reason =
|
||||||
|
app.deleted_reason || "Sin razón especificada";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-slate-500 text-xl">deployed_code_history</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-slate-900 dark:text-white font-semibold text-sm">${app.name}</p>
|
||||||
|
<p class="text-slate-500 text-xs">${app.path || "Sin ruta"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${app.port}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${deletedDate}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<span class="text-slate-600 dark:text-slate-400 text-sm">${reason}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
<button
|
||||||
|
onclick="restoreApp('${app.name}')"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-500 hover:bg-green-600 text-white text-xs font-medium rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-sm">restore</span>
|
||||||
|
Restaurar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
emptyMessage.classList.add("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading deleted apps:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDeletedApps() {
|
||||||
|
const content = document.getElementById("deleted-apps-content");
|
||||||
|
content.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreApp(appName) {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`¿Estás seguro de restaurar la aplicación "${appName}"?\n\nNota: Solo se restaurará el registro en el JSON. El servicio systemd debe ser recreado manualmente.`,
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/apps/${appName}/restore`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`✅ ${result.data.message}`);
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps();
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
alert("❌ Error al restaurar la aplicación");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
loadApps();
|
||||||
|
loadDeletedApps();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -284,17 +284,22 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/apps");
|
const response = await fetch("/api/apps");
|
||||||
const data = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
loading.classList.add("hidden");
|
loading.classList.add("hidden");
|
||||||
|
|
||||||
if (!data.apps || data.apps.length === 0) {
|
if (
|
||||||
|
!result.success ||
|
||||||
|
!result.data ||
|
||||||
|
!result.data.apps ||
|
||||||
|
result.data.apps.length === 0
|
||||||
|
) {
|
||||||
empty.classList.remove("hidden");
|
empty.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appList.classList.remove("hidden");
|
appList.classList.remove("hidden");
|
||||||
appList.innerHTML = data.apps
|
appList.innerHTML = result.data.apps
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
const statusColor =
|
const statusColor =
|
||||||
app.status === "Running"
|
app.status === "Running"
|
||||||
|
|||||||
Reference in New Issue
Block a user