feat: Implementación completa Fase 4 - Sistema de monitoreo con API REST y WebSocket
✨ Nuevas funcionalidades: - API REST unificada en puerto 8080 (eliminado CORS) - WebSocket para logs en tiempo real desde journalctl - Integración completa con systemd para gestión de servicios - Escaneo automático de procesos Node.js y Python - Rate limiting (1 operación/segundo por app) - Interface web moderna con Tailwind CSS (tema oscuro) - Documentación API estilo Swagger completamente en español 🎨 Interface Web (todas las páginas en español): - Dashboard con estadísticas en tiempo real - Visor de escaneo de procesos con filtros - Formulario de registro de aplicaciones con variables de entorno - Visor de logs en tiempo real con WebSocket y sidebar - Página de selección de apps detectadas - Documentación completa de API REST 🏗️ Arquitectura: - Módulo models: ServiceConfig, ManagedApp, AppStatus - Módulo systemd: wrapper de systemctl, generador de .service, parser - Módulo orchestrator: AppManager, LifecycleManager con validaciones - Módulo api: handlers REST, WebSocket manager, DTOs - Servidor unificado en puerto 8080 (Web + API + WS) 🔧 Mejoras técnicas: - Eliminación de CORS mediante servidor unificado - Separación clara frontend/backend con carga dinámica - Thread-safe con Arc<DashMap> para estado compartido - Reconciliación de estados: sysinfo vs systemd - Validaciones de paths, usuarios y configuraciones - Manejo robusto de errores con thiserror 📝 Documentación: - README.md actualizado con arquitectura completa - EJEMPLOS.md con casos de uso detallados - ESTADO_PROYECTO.md con progreso de Fase 4 - API docs interactiva en /api-docs - Script de despliegue mejorado con health checks 🚀 Producción: - Deployment script con validaciones - Health checks y rollback capability - Configuración de sudoers para systemctl - Hardening de seguridad en servicios systemd
This commit is contained in:
594
web/index.html
594
web/index.html
@@ -1,78 +1,522 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SIAX Emergency Panel</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.status-online {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.server-info {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.button-container {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.btn-success {
|
||||
background: #22c55e;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚨 SIAX EMERGENCY PANEL</h1>
|
||||
<p>Estado del Agente: <span class="status-online">● ONLINE</span></p>
|
||||
<p class="server-info">Servidor: {{SERVER_NAME}}</p>
|
||||
<!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>
|
||||
<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="sticky top-0 z-50 w-full border-b border-slate-200 dark:border-slate-800 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md"
|
||||
>
|
||||
<div
|
||||
class="max-w-[1200px] mx-auto px-4 lg:px-10 py-3 flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-3 text-primary">
|
||||
<div
|
||||
class="size-8 bg-primary rounded-lg flex items-center justify-center text-white"
|
||||
>
|
||||
<span class="material-symbols-outlined"
|
||||
>monitoring</span
|
||||
>
|
||||
</div>
|
||||
<h2
|
||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||
>
|
||||
SIAX Monitor
|
||||
</h2>
|
||||
</div>
|
||||
<nav class="hidden md:flex items-center gap-6">
|
||||
<a
|
||||
class="text-primary text-sm font-semibold leading-normal"
|
||||
href="/"
|
||||
>Inicio</a
|
||||
>
|
||||
<a
|
||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/scan"
|
||||
>
|
||||
Escanear
|
||||
</a>
|
||||
<a
|
||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/select"
|
||||
>
|
||||
Agregar
|
||||
</a>
|
||||
<a
|
||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/register"
|
||||
>
|
||||
Nueva App
|
||||
</a>
|
||||
<a
|
||||
class="text-slate-600 dark:text-slate-400 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/logs"
|
||||
>
|
||||
Registros
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="hidden sm:block">
|
||||
<label class="relative block">
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-500"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-outlined text-sm"
|
||||
>
|
||||
search
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
class="form-input w-64 rounded-lg border-none bg-slate-200 dark:bg-slate-800 text-sm py-2 pl-10 pr-4 placeholder:text-slate-500 focus:ring-1 focus:ring-primary"
|
||||
placeholder="Buscar..."
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="hidden lg:flex cursor-pointer items-center justify-center rounded-lg h-9 px-4 bg-primary text-white text-sm font-bold transition-opacity hover:opacity-90"
|
||||
onclick="window.location.href = '/register'"
|
||||
>
|
||||
<span>Registrar App</span>
|
||||
</button>
|
||||
<div
|
||||
class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-9 border-2 border-slate-700"
|
||||
style="
|
||||
background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCT0iINTncUFHp353HCJXRR5C0OKbSp_7IBOVNoDU07yuF2aToQQdnXNOeGI9RLUjVBsVNcU--ZoTMY90FFJvrQvYvRzKvq-CFCzBlVkCeoi5AgG84cB71wW0NIMg626M_sCjmDjxqmAJwIbkAcSmSlAg3TUThW1U2A3StNVgqFXEpgFbpJcU5nxLs6vuRkfYR1kIXcV44TQpgOosbsjSB1Pk1UTOQJ_OEcQtY-5c3FJw7gXBDxlp6y3jsY3rBm0xWGJi8NWnrUrhpl");
|
||||
"
|
||||
></div>
|
||||
</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"
|
||||
>
|
||||
Dashboard Index
|
||||
</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">
|
||||
Actions
|
||||
</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>
|
||||
<!-- 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="#"
|
||||
>Documentación de API</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script>
|
||||
async function loadApps() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/apps",
|
||||
);
|
||||
const result = await response.json();
|
||||
|
||||
<div class="button-container">
|
||||
<a href="/scan" class="btn btn-primary">
|
||||
🔍 Escanear Sistema
|
||||
</a>
|
||||
if (result.success && result.data && result.data.apps) {
|
||||
document.getElementById("app-count").textContent =
|
||||
result.data.total || 0;
|
||||
|
||||
<a href="/select" class="btn btn-success">
|
||||
⚙️ Gestionar Procesos
|
||||
</a>
|
||||
if (result.data.apps && result.data.apps.length > 0) {
|
||||
displayApps(result.data.apps);
|
||||
} else {
|
||||
displayEmpty();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
displayEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
<a href="/logs" class="btn btn-warning">
|
||||
📋 Ver Logs
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
function displayApps(apps) {
|
||||
const tbody = document.getElementById("apps-tbody");
|
||||
tbody.innerHTML = apps
|
||||
.map(
|
||||
(app) => `
|
||||
<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}</p>
|
||||
<p class="text-slate-500 text-xs">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 bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400">
|
||||
<span class="size-1.5 rounded-full bg-slate-400"></span>
|
||||
Unknown
|
||||
</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">
|
||||
<button class="text-slate-400 hover:text-white transition-colors">
|
||||
<span class="material-symbols-outlined">more_vert</span>
|
||||
</button>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", loadApps);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user