- 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
1037 lines
49 KiB
HTML
1037 lines
49 KiB
HTML
<!doctype html>
|
|
<html class="dark" lang="es" dir="ltr">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
<title>Panel de Monitoreo</title>
|
|
<link rel="icon" type="image/svg+xml" href="/static/icon/favicon.svg" />
|
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<script id="tailwind-config">
|
|
tailwind.config = {
|
|
darkMode: "class",
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: "#137fec",
|
|
"background-light": "#f6f7f8",
|
|
"background-dark": "#101922",
|
|
},
|
|
fontFamily: {
|
|
display: ["Inter"],
|
|
},
|
|
borderRadius: {
|
|
DEFAULT: "0.25rem",
|
|
lg: "0.5rem",
|
|
xl: "0.75rem",
|
|
full: "9999px",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<style>
|
|
body {
|
|
font-family: "Inter", sans-serif;
|
|
}
|
|
.material-symbols-outlined {
|
|
font-variation-settings:
|
|
"FILL" 0,
|
|
"wght" 400,
|
|
"GRAD" 0,
|
|
"opsz" 24;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body
|
|
class="bg-background-light dark:bg-background-dark text-slate-900 dark:text-slate-100 min-h-screen"
|
|
>
|
|
<div class="flex h-full grow flex-col">
|
|
<!-- Sticky Top Navigation -->
|
|
<header class="border-b border-[#283039] bg-[#0a0f16]">
|
|
<div class="container mx-auto px-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div
|
|
class="rounded-full size-9 border-2 border-slate-700 overflow-hidden"
|
|
>
|
|
<img
|
|
src="/static/icon/logo.png"
|
|
alt="Logo"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xl font-bold">SIAX Monitor</h1>
|
|
<p class="text-xs text-slate-400">Dashboard</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop Navigation -->
|
|
<nav class="hidden md:flex items-center gap-2">
|
|
<a
|
|
href="/health"
|
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>monitor_heart</span
|
|
>
|
|
<span>Health</span>
|
|
</a>
|
|
<a
|
|
href="/scan"
|
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>search</span
|
|
>
|
|
<span>Escanear</span>
|
|
</a>
|
|
<a
|
|
href="/logs"
|
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>article</span
|
|
>
|
|
<span>Logs</span>
|
|
</a>
|
|
<a
|
|
href="/register"
|
|
class="px-4 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-2"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>app_registration</span
|
|
>
|
|
<span>Registrar</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<!-- Mobile Menu Button -->
|
|
<button
|
|
onclick="toggleMenu()"
|
|
class="md:hidden px-3 py-2 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors"
|
|
>
|
|
<span class="material-symbols-outlined text-2xl"
|
|
>menu</span
|
|
>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mobile Menu Dropdown -->
|
|
<div
|
|
id="mobile-menu"
|
|
class="hidden md:hidden mt-4 pb-4 space-y-2"
|
|
>
|
|
<a
|
|
href="/health"
|
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>monitor_heart</span
|
|
>
|
|
<span>Health</span>
|
|
</a>
|
|
<a
|
|
href="/scan"
|
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>search</span
|
|
>
|
|
<span>Escanear</span>
|
|
</a>
|
|
<a
|
|
href="/logs"
|
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>article</span
|
|
>
|
|
<span>Logs</span>
|
|
</a>
|
|
<a
|
|
href="/register"
|
|
class="block px-4 py-3 rounded-lg text-slate-300 hover:bg-[#161f2a] transition-colors flex items-center gap-3"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>app_registration</span
|
|
>
|
|
<span>Registrar</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main class="max-w-[1200px] mx-auto w-full px-4 lg:px-10 py-8">
|
|
<!-- Page Heading -->
|
|
<div
|
|
class="flex flex-wrap items-center justify-between gap-4 mb-8"
|
|
>
|
|
<div>
|
|
<h1
|
|
class="text-slate-900 dark:text-white text-3xl font-black tracking-tight"
|
|
>
|
|
Panel de Control
|
|
</h1>
|
|
<p class="text-slate-500 text-sm mt-1">
|
|
Monitoreo de salud del sistema y procesos en tiempo
|
|
real - Server: {{SERVER_NAME}}
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button
|
|
class="flex items-center gap-2 rounded-lg h-10 px-4 bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-white text-sm font-bold transition-colors hover:bg-slate-700"
|
|
onclick="window.location.href = '/scan'"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>refresh</span
|
|
>
|
|
<span>Escanear Sistema</span>
|
|
</button>
|
|
<button
|
|
class="flex items-center gap-2 rounded-lg h-10 px-4 bg-primary text-white text-sm font-bold transition-opacity hover:opacity-90 lg:hidden"
|
|
onclick="window.location.href = '/register'"
|
|
>
|
|
<span class="material-symbols-outlined text-lg"
|
|
>add</span
|
|
>
|
|
<span>Registrar</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Stats Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
|
<div
|
|
class="bg-white dark:bg-slate-800/50 rounded-xl p-6 border border-slate-200 dark:border-slate-800"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<p
|
|
class="text-slate-500 dark:text-slate-400 text-sm font-medium"
|
|
>
|
|
Uso CPU
|
|
</p>
|
|
<span class="material-symbols-outlined text-primary"
|
|
>speed</span
|
|
>
|
|
</div>
|
|
<div class="flex items-end gap-2">
|
|
<p
|
|
class="text-slate-900 dark:text-white text-3xl font-bold"
|
|
>
|
|
24.8%
|
|
</p>
|
|
<p
|
|
class="text-red-500 text-sm font-semibold mb-1 flex items-center"
|
|
>
|
|
<span class="material-symbols-outlined text-sm"
|
|
>trending_up</span
|
|
>+2.4%
|
|
</p>
|
|
</div>
|
|
<div
|
|
class="mt-4 w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5"
|
|
>
|
|
<div
|
|
class="bg-primary h-1.5 rounded-full"
|
|
style="width: 24.8%"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="bg-white dark:bg-slate-800/50 rounded-xl p-6 border border-slate-200 dark:border-slate-800"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<p
|
|
class="text-slate-500 dark:text-slate-400 text-sm font-medium"
|
|
>
|
|
Consumo de Memoria
|
|
</p>
|
|
<span class="material-symbols-outlined text-primary"
|
|
>memory</span
|
|
>
|
|
</div>
|
|
<div class="flex items-end gap-2">
|
|
<p
|
|
class="text-slate-900 dark:text-white text-3xl font-bold"
|
|
>
|
|
12.4 GB
|
|
</p>
|
|
<p
|
|
class="text-emerald-500 text-sm font-semibold mb-1 flex items-center"
|
|
>
|
|
<span class="material-symbols-outlined text-sm"
|
|
>trending_down</span
|
|
>-0.5%
|
|
</p>
|
|
</div>
|
|
<p class="text-slate-500 text-xs mt-1">
|
|
of 32 GB Total RAM
|
|
</p>
|
|
</div>
|
|
<div
|
|
class="bg-white dark:bg-slate-800/50 rounded-xl p-6 border border-slate-200 dark:border-slate-800"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<p
|
|
class="text-slate-500 dark:text-slate-400 text-sm font-medium"
|
|
>
|
|
Procesos Activos
|
|
</p>
|
|
<span class="material-symbols-outlined text-primary"
|
|
>apps</span
|
|
>
|
|
</div>
|
|
<div class="flex items-end gap-2">
|
|
<p
|
|
class="text-slate-900 dark:text-white text-3xl font-bold"
|
|
id="app-count"
|
|
>
|
|
0
|
|
</p>
|
|
<p
|
|
class="text-emerald-500 text-sm font-semibold mb-1 flex items-center"
|
|
>
|
|
<span class="material-symbols-outlined text-sm"
|
|
>add</span
|
|
>monitored
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-1 mt-4">
|
|
<span
|
|
class="size-2 rounded-full bg-emerald-500"
|
|
></span>
|
|
<span
|
|
class="size-2 rounded-full bg-emerald-500"
|
|
></span>
|
|
<span
|
|
class="size-2 rounded-full bg-emerald-500"
|
|
></span>
|
|
<span
|
|
class="size-2 rounded-full bg-amber-500"
|
|
></span>
|
|
<span
|
|
class="size-2 rounded-full bg-emerald-500"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Aplicaciones Recientes Section -->
|
|
<div
|
|
class="bg-white dark:bg-slate-800/30 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden"
|
|
>
|
|
<div
|
|
class="p-6 border-b border-slate-200 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4"
|
|
>
|
|
<h2
|
|
class="text-slate-900 dark:text-white text-xl font-bold"
|
|
>
|
|
Aplicaciones Recientes
|
|
</h2>
|
|
<div class="flex items-center gap-2">
|
|
<div class="relative flex-1 sm:w-64">
|
|
<span
|
|
class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-500"
|
|
>
|
|
<span
|
|
class="material-symbols-outlined text-sm"
|
|
>filter_list</span
|
|
>
|
|
</span>
|
|
<input
|
|
class="form-input w-full rounded-lg border border-slate-200 dark:border-slate-700 bg-transparent text-sm py-1.5 pl-10 pr-4 placeholder:text-slate-500 focus:ring-1 focus:ring-primary"
|
|
placeholder="Filtrar por estado o nombre..."
|
|
type="text"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto" id="apps-table-container">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr
|
|
class="bg-slate-50 dark:bg-slate-800/50 text-slate-500 dark:text-slate-400 text-xs font-semibold uppercase tracking-wider"
|
|
>
|
|
<th class="px-6 py-4">Nombre de App</th>
|
|
<th class="px-6 py-4">Estado</th>
|
|
<th class="px-6 py-4">CPU %</th>
|
|
<th class="px-6 py-4">Mem %</th>
|
|
<th class="px-6 py-4">Tiempo Activo</th>
|
|
<th class="px-6 py-4 text-right">
|
|
Acciones
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody
|
|
class="divide-y divide-slate-200 dark:divide-slate-800"
|
|
id="apps-tbody"
|
|
>
|
|
<tr>
|
|
<td
|
|
colspan="6"
|
|
class="px-6 py-8 text-center text-slate-500"
|
|
>
|
|
<span
|
|
class="material-symbols-outlined text-4xl mb-2"
|
|
>hourglass_empty</span
|
|
>
|
|
<p>Cargando aplicaciones...</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div
|
|
class="p-4 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-200 dark:border-slate-800 text-center"
|
|
>
|
|
<a
|
|
class="text-primary text-sm font-bold hover:underline cursor-pointer"
|
|
onclick="window.location.href = '/scan'"
|
|
>Ver Todas las Aplicaciones</a
|
|
>
|
|
</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
|
|
class="flex items-start gap-4 p-5 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-transparent hover:border-primary/50 transition-colors cursor-pointer group"
|
|
onclick="window.location.href = '/register'"
|
|
>
|
|
<div
|
|
class="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-all"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>add_to_queue</span
|
|
>
|
|
</div>
|
|
<div>
|
|
<h3
|
|
class="text-slate-900 dark:text-white font-bold text-lg leading-tight"
|
|
>
|
|
Register New Service
|
|
</h3>
|
|
<p class="text-slate-500 text-sm">
|
|
Manually add a binary or process to the
|
|
monitoring queue.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="flex items-start gap-4 p-5 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-transparent hover:border-primary/50 transition-colors cursor-pointer group"
|
|
onclick="window.location.href = '/logs'"
|
|
>
|
|
<div
|
|
class="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-all"
|
|
>
|
|
<span class="material-symbols-outlined"
|
|
>history</span
|
|
>
|
|
</div>
|
|
<div>
|
|
<h3
|
|
class="text-slate-900 dark:text-white font-bold text-lg leading-tight"
|
|
>
|
|
View Event Logs
|
|
</h3>
|
|
<p class="text-slate-500 text-sm">
|
|
Review detailed historical data and error
|
|
reports from across your stack.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<footer
|
|
class="mt-auto border-t border-slate-200 dark:border-slate-800 py-6"
|
|
>
|
|
<div
|
|
class="max-w-[1200px] mx-auto px-4 lg:px-10 flex flex-col sm:flex-row justify-between items-center gap-4 text-slate-500 text-xs font-medium"
|
|
>
|
|
<p>
|
|
© 2024 SIAX Monitor Inc. Todos los procesos del sistema
|
|
rastreados.
|
|
</p>
|
|
<div class="flex gap-6">
|
|
<a class="hover:text-primary" href="#"
|
|
>Términos de Servicio</a
|
|
>
|
|
<a class="hover:text-primary" href="#"
|
|
>Política de Privacidad</a
|
|
>
|
|
<a class="hover:text-primary" href="/api-docs"
|
|
>Documentación de API</a
|
|
>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</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>
|
|
async function loadApps() {
|
|
try {
|
|
const response = await fetch("/api/apps");
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data && result.data.apps) {
|
|
document.getElementById("app-count").textContent =
|
|
result.data.total || 0;
|
|
|
|
if (result.data.apps && result.data.apps.length > 0) {
|
|
displayApps(result.data.apps);
|
|
} else {
|
|
displayEmpty();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error:", error);
|
|
displayEmpty();
|
|
}
|
|
}
|
|
|
|
function displayApps(apps) {
|
|
const tbody = document.getElementById("apps-tbody");
|
|
tbody.innerHTML = apps
|
|
.map((app) => {
|
|
// Determinar color del badge según estado
|
|
const statusColors = {
|
|
Running: {
|
|
bg: "bg-green-100 dark:bg-green-900/30",
|
|
text: "text-green-700 dark:text-green-400",
|
|
dot: "bg-green-500",
|
|
},
|
|
Stopped: {
|
|
bg: "bg-gray-100 dark:bg-gray-800",
|
|
text: "text-gray-700 dark:text-gray-400",
|
|
dot: "bg-gray-400",
|
|
},
|
|
Failed: {
|
|
bg: "bg-red-100 dark:bg-red-900/30",
|
|
text: "text-red-700 dark:text-red-400",
|
|
dot: "bg-red-500",
|
|
},
|
|
Starting: {
|
|
bg: "bg-blue-100 dark:bg-blue-900/30",
|
|
text: "text-blue-700 dark:text-blue-400",
|
|
dot: "bg-blue-500",
|
|
},
|
|
Stopping: {
|
|
bg: "bg-yellow-100 dark:bg-yellow-900/30",
|
|
text: "text-yellow-700 dark:text-yellow-400",
|
|
dot: "bg-yellow-500",
|
|
},
|
|
Unknown: {
|
|
bg: "bg-slate-100 dark:bg-slate-800",
|
|
text: "text-slate-700 dark:text-slate-400",
|
|
dot: "bg-slate-400",
|
|
},
|
|
};
|
|
|
|
const statusStyle =
|
|
statusColors[app.status] || statusColors["Unknown"];
|
|
|
|
return `
|
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-colors">
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="size-8 rounded bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
|
<span class="material-symbols-outlined text-primary text-lg">terminal</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.service_name || "Servicio"}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${statusStyle.bg} ${statusStyle.text}">
|
|
<span class="size-1.5 rounded-full ${statusStyle.dot}"></span>
|
|
${app.status}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm">-</td>
|
|
<td class="px-6 py-4 text-sm">-</td>
|
|
<td class="px-6 py-4 text-sm text-slate-500">-</td>
|
|
<td class="px-6 py-4 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
${
|
|
app.status === "Running"
|
|
? `
|
|
<button class="text-red-400 hover:text-red-300 transition-colors p-1.5 rounded hover:bg-red-900/20"
|
|
onclick="controlApp('${app.name}', 'stop')"
|
|
title="Detener">
|
|
<span class="material-symbols-outlined text-[20px]">stop</span>
|
|
</button>
|
|
<button class="text-yellow-400 hover:text-yellow-300 transition-colors p-1.5 rounded hover:bg-yellow-900/20"
|
|
onclick="controlApp('${app.name}', 'restart')"
|
|
title="Reiniciar">
|
|
<span class="material-symbols-outlined text-[20px]">refresh</span>
|
|
</button>
|
|
`
|
|
: `
|
|
<button class="text-green-400 hover:text-green-300 transition-colors p-1.5 rounded hover:bg-green-900/20"
|
|
onclick="controlApp('${app.name}', 'start')"
|
|
title="Iniciar">
|
|
<span class="material-symbols-outlined text-[20px]">play_arrow</span>
|
|
</button>
|
|
`
|
|
}
|
|
<button class="text-blue-400 hover:text-blue-300 transition-colors p-1.5 rounded hover:bg-blue-900/20"
|
|
onclick="window.location.href='/logs'"
|
|
title="Ver logs">
|
|
<span class="material-symbols-outlined text-[20px]">visibility</span>
|
|
</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>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function displayEmpty() {
|
|
const tbody = document.getElementById("apps-tbody");
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-8 text-center text-slate-500">
|
|
<span class="material-symbols-outlined text-4xl mb-2">inbox</span>
|
|
<p>No hay aplicaciones registradas aún</p>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
async function controlApp(appName, action) {
|
|
const actionNames = {
|
|
start: "Iniciar",
|
|
stop: "Detener",
|
|
restart: "Reiniciar",
|
|
};
|
|
|
|
const confirmed = confirm(
|
|
`¿Estás seguro de ${actionNames[action]} la aplicación "${appName}"?`,
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/api/apps/${appName}/${action}`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert(`✅ ${result.data.message}`);
|
|
// Recargar la lista de apps
|
|
loadApps();
|
|
} else {
|
|
alert(`❌ Error: ${result.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error:", error);
|
|
alert("❌ Error al ejecutar la acción");
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
const menu = document.getElementById("mobile-menu");
|
|
menu.classList.toggle("hidden");
|
|
}
|
|
|
|
// 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>
|