Files
SIAX-MONITOR/web/logs.html
pablinux 8822e9e6b5 feat: Mejorar estructura de monitored_apps.json con metadata completa
- Añadir campos al modelo MonitoredApp:
  * service_name: Nombre del servicio systemd
  * path: WorkingDirectory de la aplicación
  * entry_point: Archivo de entrada (server.js, app.js, etc.)
  * node_bin: Ruta completa al binario de node/python
  * mode: Modo de ejecución (production, development, test)
  * service_file_path: Ruta al archivo .service de systemd
  * registered_at: Timestamp de registro (ISO 8601)

- Actualizar discovery.rs para extraer toda la información:
  * Parsear ExecStart para obtener node_bin y entry_point
  * Extraer NODE_ENV para determinar el modo
  * Guardar ruta completa al archivo .service
  * Usar add_app_full() con información completa

- Integrar ConfigManager en AppManager:
  * Guardar automáticamente en monitored_apps.json al registrar apps
  * Eliminar del JSON al desregistrar apps
  * Extraer metadata desde ServiceConfig (puerto, entry_point, mode, etc.)

- Mantener retrocompatibilidad con JSON antiguo mediante campos deprecated
- Todos los nuevos campos usan #[serde(default)] para evitar errores de deserialización
2026-01-18 03:26:42 -05:00

475 lines
20 KiB
HTML
Raw 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>
<link rel="icon" type="image/svg+xml" href="/static/icon/favicon.svg" />
<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"
>
<img
src="/static/icon/logo.png"
alt="Logo"
class="w-full h-full object-cover"
/>
</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"
>Selecionar Detectada</a
>
<a
class="text-primary text-sm font-medium border-b-2 border-primary pb-1"
href="/logs"
>Registros</a
>
</nav>
<button
class="hidden lg:flex cursor-pointer items-center justify-center rounded-lg h-9 px-4 bg-primary text-white text-sm font-bold transition-opacity hover:opacity-90"
onclick="window.location.href = '/register'"
>
<span>Nueva App</span>
</button>
</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("/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
const protocol =
window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/api/apps/${appName}/logs`;
ws = new WebSocket(wsUrl);
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>