✨ 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
472 lines
19 KiB
HTML
472 lines
19 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>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>
|