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:
2026-01-13 08:24:13 -05:00
parent 3595e55a1e
commit b0489739cf
33 changed files with 6893 additions and 1261 deletions

View File

@@ -1,160 +1,416 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Gestionar Procesos - SIAX</title>
<style>
body {
background: #0f172a;
color: white;
font-family: sans-serif;
padding: 40px;
}
h1 {
color: #3b82f6;
}
h2 {
color: #60a5fa;
margin-top: 40px;
}
.process-item {
background: #1e293b;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.process-info {
flex-grow: 1;
}
.pid {
color: #22c55e;
font-weight: bold;
}
.path {
color: #94a3b8;
font-size: 12px;
margin-top: 5px;
}
.select-btn {
padding: 8px 16px;
background: #22c55e;
border: none;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.select-btn:hover {
background: #16a34a;
}
.form-section {
background: #1e293b;
padding: 25px;
border-radius: 12px;
margin-top: 20px;
border: 2px solid #3b82f6;
transition: border-color 0.3s;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
color: #60a5fa;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 100%;
padding: 10px;
background: #0f172a;
border: 2px solid #475569;
border-radius: 6px;
color: white;
font-size: 14px;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
small {
color: #94a3b8;
display: block;
margin-top: 5px;
}
.submit-btn {
padding: 12px 24px;
background: #3b82f6;
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}
.submit-btn:hover {
background: #2563eb;
}
.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;
}
.no-results {
background: #7f1d1d;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #ef4444;
}
</style>
</head>
<body>
<h1>⚙️ Gestionar Procesos a Monitorear</h1>
<!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>Agregar App Detectada - 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-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/scan"
>Escanear</a
>
<a
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
href="/select"
>Agregar Detectada</a
>
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/register"
>Nueva App</a
>
<a
class="text-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/logs"
>Registros</a
>
</nav>
</div>
</div>
</header>
<h2>📋 Procesos Node.js Detectados</h2>
{{PROCESSES_LIST}}
<main class="flex-1 max-w-[1200px] mx-auto w-full px-4 py-8 space-y-6">
<!-- Page Heading -->
<div class="flex flex-col gap-2">
<h1
class="text-white text-4xl font-black leading-tight tracking-[-0.033em]"
>
Add Detected Application
</h1>
<p class="text-[#9dabb9] text-base font-normal">
Selecciona un proceso detectado y configúralo para
monitoreo.
</p>
</div>
<h2> Agregar Proceso Personalizado</h2>
<div class="form-section">
<form method="POST" action="/add-process">
<div class="form-group">
<label for="app_name">Nombre de la Aplicación:</label>
<input type="text" id="app_name" name="app_name" placeholder="Ej: app_tareas, fidelizacion, mi-api" required>
<small>💡 Este nombre se usará para identificar el proceso en el directorio de trabajo</small>
</div>
<!-- Detected Processes Section -->
<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 Node.js & Python Processes
</h3>
<button
onclick="window.location.href = '/scan'"
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
>
<span class="material-symbols-outlined text-[18px]"
>refresh</span
>
Scan Again
</button>
</div>
<div class="form-group">
<label for="port">Puerto:</label>
<input type="number" id="port" name="port" placeholder="Ej: 3000, 3001, 8080" required>
<small>💡 Puerto donde corre la aplicación</small>
</div>
<div id="processes-container" class="p-4">
<!-- Processes will be loaded here -->
</div>
<button type="submit" class="submit-btn">💾 Guardar y Monitorear</button>
</form>
</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">
Cargando procesos detectados...
</p>
</div>
<a href="/" class="back-btn">← Volver al Panel</a>
<!-- 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 mb-4">
No se detectaron procesos
</p>
<button
onclick="window.location.href = '/scan'"
class="px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
>
Run Scan
</button>
</div>
</div>
<script>
function fillForm(appName, pid) {
document.getElementById('app_name').value = appName;
document.querySelector('.form-section').scrollIntoView({ behavior: 'smooth' });
document.querySelector('.form-section').style.borderColor = '#22c55e';
setTimeout(() => {
document.querySelector('.form-section').style.borderColor = '#3b82f6';
}, 2000);
}
</script>
</body>
</html>
<!-- Quick Add Form -->
<div
class="rounded-xl border border-[#283039] bg-[#161f2a] p-6 space-y-5"
>
<h3 class="text-white text-lg font-bold">
Quick Configuration
</h3>
<p class="text-[#9dabb9] text-sm">
Click on a detected process above, or fill in the details
manually to add it to monitoring.
</p>
<form id="quickAddForm" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<label
class="block text-[#9dabb9] text-sm font-medium"
>
Application Name
<span class="text-red-400">*</span>
</label>
<input
type="text"
id="app_name"
name="app_name"
required
placeholder="mi-app"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div class="space-y-2">
<label
class="block text-[#9dabb9] text-sm font-medium"
>
Process ID (PID)
</label>
<input
type="text"
id="pid"
name="pid"
readonly
placeholder="Auto-detectado"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-[#9dabb9] placeholder-[#9dabb9] cursor-not-allowed"
/>
</div>
</div>
<div class="space-y-2">
<label class="block text-[#9dabb9] text-sm font-medium">
Script Path <span class="text-red-400">*</span>
</label>
<input
type="text"
id="script_path"
name="script_path"
required
placeholder="/opt/apps/mi-app/index.js"
class="w-full bg-[#1c2730] border border-[#283039] rounded-lg px-4 py-2.5 text-white placeholder-[#9dabb9] focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div
class="flex flex-col-reverse sm:flex-row gap-3 justify-end pt-4"
>
<button
type="button"
onclick="resetForm()"
class="flex items-center justify-center rounded-lg h-12 px-6 bg-[#1c2730] hover:bg-[#283039] border border-[#283039] text-white text-sm font-bold transition-colors"
>
Clear
</button>
<button
type="button"
onclick="window.location.href = '/register'"
class="flex items-center justify-center rounded-lg h-12 px-6 bg-[#283039] hover:bg-[#3a4654] border border-[#283039] text-white text-sm font-bold transition-colors gap-2"
>
<span class="material-symbols-outlined text-[18px]"
>settings</span
>
<span>Configuración Avanzada</span>
</button>
<button
type="submit"
class="flex items-center justify-center rounded-lg h-12 px-6 bg-primary hover:brightness-110 text-white text-sm font-bold transition-all gap-2"
>
<span class="material-symbols-outlined text-[18px]"
>add_circle</span
>
<span>Agregar al Monitor</span>
</button>
</div>
</form>
</div>
</main>
<script>
let detectedProcesses = [];
async function loadProcesses() {
const container = document.getElementById(
"processes-container",
);
const loading = document.getElementById("loading-state");
const empty = document.getElementById("empty-state");
try {
const response = await fetch(
"http://localhost:8080/api/scan",
);
if (!response.ok)
throw new Error("Failed to fetch processes");
const data = await response.json();
detectedProcesses = data.data.processes || [];
loading.classList.add("hidden");
if (detectedProcesses.length === 0) {
empty.classList.remove("hidden");
container.classList.add("hidden");
return;
}
container.classList.remove("hidden");
container.innerHTML = `
<div class="space-y-3">
${detectedProcesses
.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 `
<div class="flex items-center justify-between p-4 rounded-lg border border-[#283039] hover:border-primary hover:bg-[#1c2730] transition-all cursor-pointer" onclick="selectProcess(${process.pid})">
<div class="flex items-center gap-4 flex-1">
<div class="flex items-center justify-center w-12 h-12 rounded-lg bg-[#1c2730]">
<span class="material-symbols-outlined ${typeColor}">${typeIcon}</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-3 mb-1">
<span class="text-white font-medium">${process.name}</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${typeColor} bg-current bg-opacity-20">
${typeLabel}
</span>
</div>
<div class="text-[#9dabb9] text-sm">
PID: <span class="font-mono">${process.pid}</span> ·
CPU: ${process.cpu_usage?.toFixed(2) || "0.00"}% ·
Memory: ${formatMemory(process.memory_mb)}
</div>
</div>
</div>
<button
onclick="event.stopPropagation(); selectProcess(${process.pid})"
class="flex items-center gap-2 px-4 py-2 bg-primary hover:brightness-110 rounded-lg text-white text-sm font-medium transition-all"
>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
Select
</button>
</div>
`;
})
.join("")}
</div>
`;
} catch (error) {
console.error("Error loading processes:", error);
loading.classList.add("hidden");
empty.classList.remove("hidden");
}
}
function selectProcess(pid) {
const process = detectedProcesses.find((p) => p.pid === pid);
if (!process) return;
document.getElementById("app_name").value = process.name || "";
document.getElementById("pid").value = pid;
document.getElementById("script_path").value = ""; // User needs to provide this
// Scroll to form
document
.getElementById("quickAddForm")
.scrollIntoView({ behavior: "smooth" });
// Focus on script path
setTimeout(() => {
document.getElementById("script_path").focus();
}, 500);
}
function formatMemory(mb) {
if (!mb) return "0 MB";
if (mb < 1024) return `${mb.toFixed(0)} MB`;
return `${(mb / 1024).toFixed(2)} GB`;
}
function resetForm() {
document.getElementById("quickAddForm").reset();
}
document
.getElementById("quickAddForm")
.addEventListener("submit", async (e) => {
e.preventDefault();
alert(
'¡Función de agregar rápido próximamente! Por favor usa el botón "Configuración Avanzada" para registrar la aplicación con configuración completa.',
);
// Redirect to register page with pre-filled data
const appName = document.getElementById("app_name").value;
const scriptPath =
document.getElementById("script_path").value;
if (appName && scriptPath) {
// Store in sessionStorage for pre-filling
sessionStorage.setItem("prefill_app_name", appName);
sessionStorage.setItem(
"prefill_script_path",
scriptPath,
);
window.location.href = "/register";
}
});
// Load processes on page load
document.addEventListener("DOMContentLoaded", loadProcesses);
</script>
</body>
</html>