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:
218
web/index.html
218
web/index.html
@@ -397,6 +397,100 @@
|
||||
>
|
||||
</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 -->
|
||||
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div
|
||||
@@ -800,6 +894,7 @@
|
||||
if (result.success) {
|
||||
alert(`✅ ${result.data.message}`);
|
||||
loadApps();
|
||||
loadDeletedApps(); // Recargar historial de eliminadas
|
||||
} else {
|
||||
alert(`❌ Error: ${result.error}`);
|
||||
}
|
||||
@@ -814,7 +909,128 @@
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user