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:
420
web/scan.html
420
web/scan.html
@@ -1,71 +1,349 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Scan Results - SIAX</title>
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.process {
|
||||
background: #1e293b;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
.pid {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.name {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.cpu {
|
||||
color: #f59e0b;
|
||||
}
|
||||
.mem {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
.path {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.summary {
|
||||
color: #22c55e;
|
||||
font-size: 18px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.no-results {
|
||||
background: #7f1d1d;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #64748b;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: #475569;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 Escaneo de Procesos Node.js</h1>
|
||||
{{CONTENT}}
|
||||
<a href="/" class="back-btn">← Volver al Panel</a>
|
||||
</body>
|
||||
</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>Escaneo de Procesos - SIAX Monitor</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&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>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: "#137fec",
|
||||
"background-light": "#f6f7f8",
|
||||
"background-dark": "#101922",
|
||||
},
|
||||
fontFamily: { display: ["Inter", "sans-serif"] },
|
||||
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 font-display text-white min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Sticky Top Navigation -->
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full border-b border-solid border-[#283039] bg-background-dark/80 backdrop-blur-md px-4 md:px-10 py-3"
|
||||
>
|
||||
<div
|
||||
class="max-w-[1200px] mx-auto flex items-center justify-between whitespace-nowrap"
|
||||
>
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-4 text-white">
|
||||
<div
|
||||
class="size-8 bg-primary rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<span class="material-symbols-outlined text-white"
|
||||
>monitoring</span
|
||||
>
|
||||
</div>
|
||||
<h2
|
||||
class="text-white text-lg font-bold leading-tight tracking-[-0.015em]"
|
||||
>
|
||||
SIAX Monitor
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 justify-end gap-6 items-center">
|
||||
<nav class="hidden md:flex items-center gap-6">
|
||||
<a
|
||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||
href="/"
|
||||
>Panel</a
|
||||
>
|
||||
<a
|
||||
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
|
||||
href="/scan"
|
||||
>Escanear</a
|
||||
>
|
||||
<a
|
||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||
href="/select"
|
||||
>Agregar Detectada</a
|
||||
>
|
||||
<a
|
||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||
href="/register"
|
||||
>Registrar Nueva</a
|
||||
>
|
||||
<a
|
||||
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
|
||||
href="/logs"
|
||||
>Registros</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 max-w-[1200px] mx-auto w-full px-4 py-8 space-y-6">
|
||||
<!-- Page Heading -->
|
||||
<div class="flex flex-wrap justify-between items-end gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1
|
||||
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
|
||||
>
|
||||
Process Scan View
|
||||
</h1>
|
||||
<p class="text-[#9dabb9] text-base font-normal">
|
||||
Monitoreo activo de procesos Node.js y Python.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex items-center justify-center rounded-lg h-10 px-4 bg-primary text-white text-sm font-bold hover:brightness-110 transition-all gap-2"
|
||||
onclick="refreshScan()"
|
||||
>
|
||||
<span class="material-symbols-outlined text-[18px]"
|
||||
>refresh</span
|
||||
>
|
||||
<span>Actualizar Escaneo</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-xl p-6 border border-[#283039] bg-[#161f2a]"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Total Processes
|
||||
</p>
|
||||
<span class="material-symbols-outlined text-primary"
|
||||
>analytics</span
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
class="text-white text-3xl font-bold"
|
||||
id="total-processes"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Detectados en este escaneo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-xl p-6 border border-[#283039] bg-[#161f2a]"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Node.js Processes
|
||||
</p>
|
||||
<span class="material-symbols-outlined text-green-400"
|
||||
>terminal</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-white text-3xl font-bold" id="node-count">
|
||||
0
|
||||
</p>
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Running instances
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-xl p-6 border border-[#283039] bg-[#161f2a]"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Python Processes
|
||||
</p>
|
||||
<span class="material-symbols-outlined text-blue-400"
|
||||
>code</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-white text-3xl font-bold" id="python-count">
|
||||
0
|
||||
</p>
|
||||
<p class="text-[#9dabb9] text-sm font-medium">
|
||||
Running instances
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Table -->
|
||||
<div
|
||||
class="rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-4 border-b border-[#283039]"
|
||||
>
|
||||
<h3 class="text-white font-bold px-2">
|
||||
Detected Processes
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-[#1c2730] border-b border-[#283039]">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">PID</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Process Name</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Usuario</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">CPU %</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Memoria</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Status</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Tipo</th>
|
||||
<th class="px-4 py-3 text-left text-[#9dabb9] text-sm font-medium">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="processes-tbody" class="divide-y divide-[#283039]">
|
||||
<!-- Rows will be populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="hidden p-8 text-center">
|
||||
<span class="material-symbols-outlined text-[#9dabb9] text-5xl mb-3">search_off</span>
|
||||
<p class="text-[#9dabb9] text-sm">No se detectaron procesos. Haz clic en "Actualizar Escaneo" para intentar de nuevo.</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-state" class="p-8 text-center">
|
||||
<span class="material-symbols-outlined text-primary text-5xl mb-3 animate-spin">progress_activity</span>
|
||||
<p class="text-[#9dabb9] text-sm">Escaneando procesos...</p>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadProcesses() {
|
||||
const tbody = document.getElementById('processes-tbody');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const loadingState = document.getElementById('loading-state');
|
||||
const totalProcesses = document.getElementById('total-processes');
|
||||
const nodeCount = document.getElementById('node-count');
|
||||
const pythonCount = document.getElementById('python-count');
|
||||
|
||||
try {
|
||||
loadingState.classList.remove('hidden');
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
const response = await fetch('http://localhost:8080/api/scan');
|
||||
if (!response.ok) throw new Error('Failed to fetch processes');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
loadingState.classList.add('hidden');
|
||||
|
||||
if (!data.data || !data.data.processes || data.data.processes.length === 0) {
|
||||
emptyState.classList.remove('hidden');
|
||||
totalProcesses.textContent = '0';
|
||||
nodeCount.textContent = '0';
|
||||
pythonCount.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const nodeProcesses = data.data.processes.filter(p => p.process_type === 'nodejs');
|
||||
const pythonProcesses = data.data.processes.filter(p => p.process_type === 'python');
|
||||
|
||||
totalProcesses.textContent = data.data.processes.length;
|
||||
nodeCount.textContent = nodeProcesses.length;
|
||||
pythonCount.textContent = pythonProcesses.length;
|
||||
|
||||
// Populate table
|
||||
tbody.innerHTML = data.data.processes.map(process => {
|
||||
const typeIcon = process.process_type === 'nodejs' ? 'terminal' : 'code';
|
||||
const typeColor = process.process_type === 'nodejs' ? 'text-green-400' : 'text-blue-400';
|
||||
const typeLabel = process.process_type === 'nodejs' ? 'Node.js' : 'Python';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-[#1c2730] transition-colors">
|
||||
<td class="px-4 py-3 text-white text-sm font-mono">${process.pid}</td>
|
||||
<td class="px-4 py-3 text-white text-sm font-medium">${process.name}</td>
|
||||
<td class="px-4 py-3 text-[#9dabb9] text-sm">${process.user || 'N/A'}</td>
|
||||
<td class="px-4 py-3 text-white text-sm">${process.cpu_usage?.toFixed(2) || '0.00'}%</td>
|
||||
<td class="px-4 py-3 text-white text-sm">${formatMemory(process.memory_mb)}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-400"></span>
|
||||
Running
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex items-center gap-1 ${typeColor}">
|
||||
<span class="material-symbols-outlined text-[18px]">${typeIcon}</span>
|
||||
<span class="text-sm font-medium">${typeLabel}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<button
|
||||
onclick="viewDetails(${process.pid})"
|
||||
class="text-primary hover:text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading processes:', error);
|
||||
loadingState.classList.add('hidden');
|
||||
emptyState.classList.remove('hidden');
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center">
|
||||
<span class="material-symbols-outlined text-red-400 text-5xl mb-3">error</span>
|
||||
<p class="text-red-400 text-sm">Error al cargar procesos. Asegúrate de que la API esté ejecutándose en el puerto 8080.</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMemory(mb) {
|
||||
if (!mb) return '0 MB';
|
||||
if (mb < 1024) return `${mb.toFixed(0)} MB`;
|
||||
return `${(mb / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function viewDetails(pid) {
|
||||
alert(`Detalles del proceso para PID ${pid} - ¡Función próximamente!`);
|
||||
}
|
||||
|
||||
function refreshScan() {
|
||||
loadProcesses();
|
||||
}
|
||||
|
||||
// Load processes on page load
|
||||
document.addEventListener('DOMContentLoaded', loadProcesses);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user