Files
SIAX-MONITOR/web/logs.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

472 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>Visor de Registros - 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;
}
</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="/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-primary text-sm font-medium border-b-2 border-primary pb-1"
href="/logs"
>Registros</a
>
</nav>
</div>
</div>
</header>
<div class="flex-1 flex max-w-[1400px] mx-auto w-full">
<!-- Sidebar - App List -->
<aside
class="w-64 border-r border-[#283039] bg-[#161f2a] p-4 space-y-4 overflow-y-auto"
>
<div class="flex items-center justify-between mb-4">
<h3 class="text-white font-bold text-sm">Aplicaciones</h3>
<button
onclick="loadApps()"
class="text-[#9dabb9] hover:text-white transition-colors"
>
<span class="material-symbols-outlined text-[18px]"
>refresh</span
>
</button>
</div>
<div id="app-list" class="space-y-2">
<!-- Apps will be loaded here -->
</div>
<!-- Loading State -->
<div id="sidebar-loading" class="hidden text-center py-8">
<span
class="material-symbols-outlined text-primary text-3xl animate-spin"
>progress_activity</span
>
</div>
<!-- Empty State -->
<div id="sidebar-empty" class="hidden text-center py-8">
<span
class="material-symbols-outlined text-[#9dabb9] text-3xl"
>inbox</span
>
<p class="text-[#9dabb9] text-xs mt-2">
No hay apps registradas
</p>
</div>
</aside>
<!-- Main Content - Log Viewer -->
<main class="flex-1 flex flex-col">
<!-- Log Header -->
<div class="border-b border-[#283039] bg-[#161f2a] p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary"
>terminal</span
>
<div>
<h1
class="text-white text-lg font-bold"
id="current-app-name"
>
Selecciona una aplicación
</h1>
<p
class="text-[#9dabb9] text-xs"
id="connection-status"
>
Not connected
</p>
</div>
</div>
<div class="flex items-center gap-2">
<button
id="auto-scroll-btn"
onclick="toggleAutoScroll()"
class="flex items-center gap-2 px-3 py-2 bg-primary/20 border border-primary/30 rounded-lg text-primary text-sm font-medium hover:bg-primary/30 transition-colors"
>
<span
class="material-symbols-outlined text-[18px]"
>swap_vert</span
>
Auto-scroll: ON
</button>
<button
onclick="clearLogViewer()"
class="flex items-center gap-2 px-3 py-2 bg-red-500/20 border border-red-500/30 rounded-lg text-red-400 text-sm font-medium hover:bg-red-500/30 transition-colors"
>
<span
class="material-symbols-outlined text-[18px]"
>delete</span
>
Clear
</button>
</div>
</div>
</div>
<!-- Terminal Log Output -->
<div
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm"
id="log-terminal"
>
<div id="log-container" class="space-y-1">
<!-- Welcome Message -->
<div class="text-[#9dabb9] opacity-50">
<span class="text-green-400"></span> SIAX Monitor
Log Viewer
</div>
<div class="text-[#9dabb9] opacity-50">
<span class="text-blue-400"></span> Select an
application from the sidebar to view logs
</div>
</div>
</div>
</main>
</div>
<script>
let ws = null;
let autoScroll = true;
let currentApp = null;
async function loadApps() {
const appList = document.getElementById("app-list");
const loading = document.getElementById("sidebar-loading");
const empty = document.getElementById("sidebar-empty");
appList.classList.add("hidden");
loading.classList.remove("hidden");
empty.classList.add("hidden");
try {
const response = await fetch(
"http://localhost:8080/api/apps",
);
const data = await response.json();
loading.classList.add("hidden");
if (!data.apps || data.apps.length === 0) {
empty.classList.remove("hidden");
return;
}
appList.classList.remove("hidden");
appList.innerHTML = data.apps
.map((app) => {
const statusColor =
app.status === "Running"
? "text-green-400"
: app.status === "Stopped"
? "text-gray-400"
: "text-red-400";
const statusIcon =
app.status === "Running"
? "play_circle"
: app.status === "Stopped"
? "stop_circle"
: "error";
return `
<button
onclick="connectToApp('${app.name}')"
class="app-item w-full text-left p-3 rounded-lg border border-[#283039] hover:border-primary hover:bg-[#1c2730] transition-all ${currentApp === app.name ? "bg-primary/20 border-primary" : ""}"
data-app="${app.name}"
>
<div class="flex items-center justify-between mb-1">
<span class="text-white text-sm font-medium truncate">${app.name}</span>
<span class="material-symbols-outlined ${statusColor} text-[16px]">${statusIcon}</span>
</div>
<div class="text-[#9dabb9] text-xs">${app.status}</div>
</button>
`;
})
.join("");
} catch (error) {
console.error("Error loading apps:", error);
loading.classList.add("hidden");
empty.classList.remove("hidden");
}
}
function connectToApp(appName) {
// Close existing connection
if (ws) {
ws.close();
}
currentApp = appName;
document.getElementById("current-app-name").textContent =
appName;
document.getElementById("connection-status").textContent =
"Connecting...";
// Clear log container
const logContainer = document.getElementById("log-container");
logContainer.innerHTML = `
<div class="text-[#9dabb9]">
<span class="text-blue-400"></span> Connecting to ${appName} logs...
</div>
`;
// Update active state
document.querySelectorAll(".app-item").forEach((item) => {
if (item.dataset.app === appName) {
item.classList.add("bg-primary/20", "border-primary");
} else {
item.classList.remove(
"bg-primary/20",
"border-primary",
);
}
});
// Connect WebSocket
ws = new WebSocket(
`ws://localhost:8080/api/apps/${appName}/logs`,
);
ws.onopen = () => {
document.getElementById("connection-status").textContent =
"Connected - Live streaming";
appendLog("success", `Connected to ${appName} logs`);
};
ws.onmessage = (event) => {
try {
const log = JSON.parse(event.data);
appendLog("log", log.MESSAGE || event.data, log);
} catch (e) {
appendLog("log", event.data);
}
};
ws.onerror = (error) => {
appendLog("error", "WebSocket error occurred");
document.getElementById("connection-status").textContent =
"Connection error";
};
ws.onclose = () => {
appendLog("warning", `Disconnected from ${appName}`);
document.getElementById("connection-status").textContent =
"Disconnected";
};
}
function appendLog(type, message, logData = null) {
const logContainer = document.getElementById("log-container");
const logEntry = document.createElement("div");
logEntry.className = "log-line";
const timestamp = new Date()
.toISOString()
.split("T")[1]
.slice(0, 12);
let icon = "●";
let color = "text-white";
if (type === "error") {
icon = "✖";
color = "text-red-400";
} else if (type === "warning") {
icon = "⚠";
color = "text-yellow-400";
} else if (type === "success") {
icon = "✓";
color = "text-green-400";
} else if (logData) {
// Parse log priority
const priority = logData.PRIORITY || "6";
if (priority <= "3") {
icon = "✖";
color = "text-red-400";
} else if (priority === "4") {
icon = "⚠";
color = "text-yellow-400";
} else {
icon = "●";
color = "text-blue-400";
}
}
logEntry.innerHTML = `
<span class="text-[#9dabb9] opacity-50">[${timestamp}]</span>
<span class="${color}">${icon}</span>
<span class="${color}">${escapeHtml(message)}</span>
`;
logContainer.appendChild(logEntry);
// Auto-scroll
if (autoScroll) {
const terminal = document.getElementById("log-terminal");
terminal.scrollTop = terminal.scrollHeight;
}
// Limit to last 1000 lines
while (logContainer.children.length > 1000) {
logContainer.removeChild(logContainer.firstChild);
}
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function toggleAutoScroll() {
autoScroll = !autoScroll;
const btn = document.getElementById("auto-scroll-btn");
btn.innerHTML = `
<span class="material-symbols-outlined text-[18px]">swap_vert</span>
Auto-scroll: ${autoScroll ? "ON" : "OFF"}
`;
if (autoScroll) {
btn.classList.add(
"bg-primary/20",
"border-primary/30",
"text-primary",
);
btn.classList.remove(
"bg-[#1c2730]",
"border-[#283039]",
"text-[#9dabb9]",
);
} else {
btn.classList.remove(
"bg-primary/20",
"border-primary/30",
"text-primary",
);
btn.classList.add(
"bg-[#1c2730]",
"border-[#283039]",
"text-[#9dabb9]",
);
}
}
function clearLogViewer() {
const logContainer = document.getElementById("log-container");
logContainer.innerHTML = `
<div class="text-[#9dabb9]">
<span class="text-blue-400"></span> Log viewer cleared
</div>
`;
}
// Load apps on page load
document.addEventListener("DOMContentLoaded", loadApps);
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
if (ws) {
ws.close();
}
});
</script>
</body>
</html>