Files
SIAX-MONITOR/web/api-docs.html
pablinux b0489739cf 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
2026-01-13 08:24:13 -05:00

566 lines
29 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>Documentación API - 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=JetBrains+Mono:wght@400;500;600&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"],
mono: ["JetBrains Mono", "monospace"],
},
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;
}
.code-block {
font-family: "JetBrains Mono", monospace;
}
</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-[1400px] 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-[#9dabb9] text-sm font-medium hover:text-white transition-colors"
href="/register"
>Registrar Nueva</a
>
<a
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
href="/api-docs"
>Documentación API</a
>
</nav>
</div>
</div>
</header>
<div class="flex-1 flex max-w-[1400px] mx-auto w-full">
<!-- Sidebar - Table of Contents -->
<aside class="w-64 border-r border-[#283039] bg-[#161f2a] p-6 space-y-6 overflow-y-auto">
<div>
<h3 class="text-white font-bold text-sm mb-3">CONTENIDO</h3>
<nav class="space-y-2">
<a href="#intro" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Introducción</a>
<a href="#auth" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Autenticación</a>
<a href="#apps" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Gestión de Apps</a>
<a href="#scan" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Escaneo</a>
<a href="#lifecycle" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Ciclo de Vida</a>
<a href="#websocket" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">WebSocket</a>
<a href="#errors" class="block text-[#9dabb9] text-sm hover:text-primary transition-colors">Códigos de Error</a>
</nav>
</div>
<div class="pt-6 border-t border-[#283039]">
<h3 class="text-white font-bold text-sm mb-3">INFO</h3>
<div class="space-y-2 text-xs">
<div>
<span class="text-[#9dabb9]">Versión:</span>
<span class="text-white font-mono">v1.0.0</span>
</div>
<div>
<span class="text-[#9dabb9]">Base URL:</span>
<span class="text-white font-mono">localhost:8080</span>
</div>
<div>
<span class="text-[#9dabb9]">Protocolo:</span>
<span class="text-white font-mono">HTTP/WS</span>
</div>
</div>
</div>
</aside>
<!-- Main Content - API Documentation -->
<main class="flex-1 p-8 overflow-y-auto">
<!-- Introduction -->
<section id="intro" class="mb-12">
<h1 class="text-white text-4xl font-black mb-4">Documentación API REST</h1>
<p class="text-[#9dabb9] text-lg mb-6">
API para gestión y monitoreo de aplicaciones Node.js y Python con systemd.
</p>
<div class="rounded-xl border border-primary/30 bg-primary/10 p-4 mb-6">
<div class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary mt-0.5">info</span>
<div>
<p class="text-white font-semibold mb-1">Endpoint Base</p>
<code class="text-primary font-mono text-sm">http://localhost:8080/api</code>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<span class="material-symbols-outlined text-green-400 mb-2">check_circle</span>
<p class="text-white font-semibold text-sm">REST API</p>
<p class="text-[#9dabb9] text-xs">JSON responses</p>
</div>
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<span class="material-symbols-outlined text-blue-400 mb-2">bolt</span>
<p class="text-white font-semibold text-sm">WebSocket</p>
<p class="text-[#9dabb9] text-xs">Logs en tiempo real</p>
</div>
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<span class="material-symbols-outlined text-purple-400 mb-2">schedule</span>
<p class="text-white font-semibold text-sm">Rate Limiting</p>
<p class="text-[#9dabb9] text-xs">1 op/segundo</p>
</div>
</div>
</section>
<!-- Authentication -->
<section id="auth" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">lock</span>
Autenticación
</h2>
<p class="text-[#9dabb9] mb-4">
Actualmente la API no requiere autenticación ya que está diseñada para acceso local vía VPN.
</p>
<div class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4">
<div class="flex items-start gap-3">
<span class="material-symbols-outlined text-yellow-400">warning</span>
<div>
<p class="text-yellow-400 font-semibold">Nota de Seguridad</p>
<p class="text-[#9dabb9] text-sm">Esta API debe ser accesible solo desde redes privadas o VPN.</p>
</div>
</div>
</div>
</section>
<!-- Apps Management -->
<section id="apps" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">apps</span>
Gestión de Aplicaciones
</h2>
<!-- List Apps -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
<code class="text-white font-mono text-sm">/api/apps</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Listar todas las aplicaciones registradas</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
"success": true,
"data": {
"apps": ["app_tareas", "fidelizacion"],
"total": 2
},
"error": null
}</pre>
</div>
<button onclick="tryEndpoint('GET', '/api/apps')" 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-sm">play_arrow</span>
Probar endpoint
</button>
</div>
</div>
<!-- Register App -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
<code class="text-white font-mono text-sm">/api/apps</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Registrar una nueva aplicación</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Body (JSON)</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-blue-400 overflow-x-auto">{
"app_name": "mi-app",
"script_path": "/opt/apps/mi-app/index.js",
"working_directory": "/opt/apps/mi-app",
"user": "nodejs",
"environment": {
"NODE_ENV": "production",
"PORT": "3000"
},
"restart_policy": "always",
"app_type": "nodejs",
"description": "Mi aplicación Node.js"
}</pre>
</div>
<div>
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
"success": true,
"data": {
"app_name": "mi-app",
"operation": "register",
"success": true,
"message": "Aplicación registrada exitosamente"
},
"error": null
}</pre>
</div>
</div>
</div>
<!-- Delete App -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">DELETE</span>
<code class="text-white font-mono text-sm">/api/apps/:name</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Eliminar una aplicación registrada</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Parámetros</p>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<code class="text-primary font-mono text-sm">name</code>
<span class="text-[#9dabb9] text-sm">- Nombre de la aplicación</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Get Status -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
<code class="text-white font-mono text-sm">/api/apps/:name/status</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Obtener estado de una aplicación</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
"success": true,
"data": {
"name": "mi-app",
"status": "Running",
"pid": 12345,
"cpu_usage": 2.5,
"memory_usage": "128.50 MB",
"systemd_status": "active",
"last_updated": "2026-01-13T12:34:56"
}
}</pre>
</div>
</div>
</div>
</section>
<!-- Scan -->
<section id="scan" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">search</span>
Escaneo de Procesos
</h2>
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-green-500/20 text-green-400 text-xs font-bold font-mono">GET</span>
<code class="text-white font-mono text-sm">/api/scan</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Escanear procesos Node.js y Python en ejecución</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Respuesta exitosa (200)</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-green-400 overflow-x-auto">{
"success": true,
"data": {
"processes": [
{
"pid": 5769,
"name": "node",
"user": "1000",
"cpu_usage": 2.5,
"memory_mb": 112.54,
"process_type": "nodejs"
}
],
"total": 1
}
}</pre>
</div>
<button onclick="tryEndpoint('GET', '/api/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-sm">play_arrow</span>
Probar endpoint
</button>
</div>
</div>
</section>
<!-- Lifecycle -->
<section id="lifecycle" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">settings_power</span>
Ciclo de Vida
</h2>
<!-- Start -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
<code class="text-white font-mono text-sm">/api/apps/:name/start</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Iniciar una aplicación</p>
</div>
</div>
<!-- Stop -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
<code class="text-white font-mono text-sm">/api/apps/:name/stop</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Detener una aplicación</p>
</div>
</div>
<!-- Restart -->
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-blue-500/20 text-blue-400 text-xs font-bold font-mono">POST</span>
<code class="text-white font-mono text-sm">/api/apps/:name/restart</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Reiniciar una aplicación</p>
</div>
</div>
<div class="rounded-xl border border-yellow-500/30 bg-yellow-500/10 p-4">
<div class="flex items-start gap-3">
<span class="material-symbols-outlined text-yellow-400">schedule</span>
<div>
<p class="text-yellow-400 font-semibold">Rate Limiting</p>
<p class="text-[#9dabb9] text-sm">Las operaciones están limitadas a 1 por segundo por aplicación.</p>
</div>
</div>
</div>
</section>
<!-- WebSocket -->
<section id="websocket" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">cable</span>
WebSocket (Logs en tiempo real)
</h2>
<div class="mb-8 rounded-xl border border-[#283039] bg-[#161f2a] overflow-hidden">
<div class="bg-[#1c2730] px-6 py-4 border-b border-[#283039]">
<div class="flex items-center gap-3">
<span class="px-2 py-1 rounded bg-purple-500/20 text-purple-400 text-xs font-bold font-mono">WS</span>
<code class="text-white font-mono text-sm">ws://localhost:8080/api/apps/:name/logs</code>
</div>
<p class="text-[#9dabb9] text-sm mt-2">Stream de logs en tiempo real desde journalctl</p>
</div>
<div class="p-6 space-y-4">
<div>
<p class="text-white font-semibold text-sm mb-2">Ejemplo JavaScript</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-purple-400 overflow-x-auto">const ws = new WebSocket('ws://localhost:8080/api/apps/mi-app/logs');
ws.onopen = () => {
console.log('Conectado a logs');
};
ws.onmessage = (event) => {
const log = JSON.parse(event.data);
console.log(log.MESSAGE);
};
ws.onerror = (error) => {
console.error('Error:', error);
};
ws.onclose = () => {
console.log('Desconectado');
};</pre>
</div>
<div>
<p class="text-white font-semibold text-sm mb-2">Límites</p>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<span class="material-symbols-outlined text-primary text-sm">check</span>
<span class="text-[#9dabb9] text-sm">Máximo 5 conexiones concurrentes por aplicación</span>
</li>
<li class="flex items-start gap-2">
<span class="material-symbols-outlined text-primary text-sm">check</span>
<span class="text-[#9dabb9] text-sm">Formato JSON desde systemd journalctl</span>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Error Codes -->
<section id="errors" class="mb-12">
<h2 class="text-white text-2xl font-bold mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">error</span>
Códigos de Error
</h2>
<div class="space-y-4">
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">400</span>
<p class="text-white font-semibold">Bad Request</p>
</div>
<p class="text-[#9dabb9] text-sm">Datos de entrada inválidos o faltantes</p>
</div>
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">404</span>
<p class="text-white font-semibold">Not Found</p>
</div>
<p class="text-[#9dabb9] text-sm">Aplicación no encontrada</p>
</div>
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">429</span>
<p class="text-white font-semibold">Too Many Requests</p>
</div>
<p class="text-[#9dabb9] text-sm">Rate limit excedido (1 operación/segundo)</p>
</div>
<div class="rounded-xl border border-[#283039] bg-[#161f2a] p-4">
<div class="flex items-center gap-3 mb-2">
<span class="px-2 py-1 rounded bg-red-500/20 text-red-400 text-xs font-bold font-mono">500</span>
<p class="text-white font-semibold">Internal Server Error</p>
</div>
<p class="text-[#9dabb9] text-sm">Error interno del servidor</p>
</div>
</div>
<div class="mt-6">
<p class="text-white font-semibold text-sm mb-2">Estructura de error</p>
<pre class="code-block bg-[#0a0f16] p-4 rounded-lg text-sm text-red-400 overflow-x-auto">{
"success": false,
"data": null,
"error": "Descripción del error"
}</pre>
</div>
</section>
</main>
</div>
<script>
async function tryEndpoint(method, path) {
const resultDiv = event.target.parentElement.querySelector('.result') ||
event.target.parentElement.appendChild(document.createElement('div'));
resultDiv.className = 'result mt-4 code-block bg-[#0a0f16] p-4 rounded-lg text-sm overflow-x-auto';
resultDiv.textContent = 'Ejecutando...';
try {
const response = await fetch(`http://localhost:8080${path}`, {
method: method
});
const data = await response.json();
resultDiv.innerHTML = `<pre class="text-green-400">${JSON.stringify(data, null, 2)}</pre>`;
} catch (error) {
resultDiv.innerHTML = `<pre class="text-red-400">Error: ${error.message}</pre>`;
}
}
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
</body>
</html>