Files
SIAX-MONITOR/web/logs.html
pablinux fbc89e9bf0 feat: Agregar sistema de tabs en logs.html con visualización de errores del sistema
Backend (handlers.rs + main.rs):
- Nuevo endpoint GET /api/logs/errors
- Lee logs/errors.log y retorna últimas 500 líneas
- Parsea y formatea logs con niveles (INFO, WARN, ERROR)

Frontend (logs.html):
- Sistema de tabs con 2 pestañas:
  * Tab 1: "Logs de App" - logs en tiempo real vía WebSocket (journalctl)
  * Tab 2: "Errores del Sistema" - logs del archivo errors.log
- Carga apps desde /api/apps (ya usaba el JSON correctamente)
- Colorización por nivel de log:
  * ERROR = rojo
  * WARN = amarillo
  * INFO = azul
- Auto-scroll en ambos tabs
- Diseño consistente con el resto de la UI

Ahora logs.html muestra:
 Logs de aplicaciones individuales (systemd/journalctl)
 Logs de errores del sistema SIAX Monitor (logs/errors.log)
 Navegación por tabs
 Lista de apps desde monitored_apps.json
2026-01-18 04:07:37 -05:00

611 lines
25 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>
<!-- Tabs -->
<div class="border-b border-[#283039] bg-[#161f2a] px-4">
<div class="flex gap-1">
<button
id="tab-app-logs"
onclick="switchTab('app-logs')"
class="tab-button px-4 py-2 text-sm font-medium transition-colors rounded-t-lg border-b-2 border-primary text-primary"
>
<span
class="material-symbols-outlined text-[16px] align-middle"
>terminal</span
>
Logs de App
</button>
<button
id="tab-system-errors"
onclick="switchTab('system-errors')"
class="tab-button px-4 py-2 text-sm font-medium transition-colors rounded-t-lg border-b-2 border-transparent text-[#9dabb9] hover:text-white"
>
<span
class="material-symbols-outlined text-[16px] align-middle"
>error</span
>
Errores del Sistema
</button>
</div>
</div>
<!-- Tab Content: App Logs -->
<div
id="content-app-logs"
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm tab-content"
>
<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>
<!-- Tab Content: System Errors -->
<div
id="content-system-errors"
class="hidden flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm tab-content"
>
<div id="system-errors-container" class="space-y-1">
<div class="text-[#9dabb9] opacity-50">
<span class="text-yellow-400"></span> Cargando logs
de errores del sistema...
</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 && currentTab === "app-logs") {
const terminal =
document.getElementById("content-app-logs");
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>
`;
}
// Tab switching
let currentTab = "app-logs";
function switchTab(tabName) {
currentTab = tabName;
// Update tab buttons
document.querySelectorAll(".tab-button").forEach((btn) => {
btn.classList.remove("border-primary", "text-primary");
btn.classList.add("border-transparent", "text-[#9dabb9]");
});
const activeTab = document.getElementById(`tab-${tabName}`);
activeTab.classList.remove(
"border-transparent",
"text-[#9dabb9]",
);
activeTab.classList.add("border-primary", "text-primary");
// Update tab content
document.querySelectorAll(".tab-content").forEach((content) => {
content.classList.add("hidden");
});
document
.getElementById(`content-${tabName}`)
.classList.remove("hidden");
// Load system errors if switching to that tab
if (tabName === "system-errors") {
loadSystemErrors();
}
}
async function loadSystemErrors() {
const container = document.getElementById(
"system-errors-container",
);
try {
const response = await fetch("/api/logs/errors");
const result = await response.json();
if (
result.success &&
result.logs &&
result.logs.length > 0
) {
container.innerHTML = result.logs
.map((line) => {
// Parse log line
let icon = "●";
let color = "text-white";
if (line.includes("[ERROR]")) {
icon = "✖";
color = "text-red-400";
} else if (line.includes("[WARN]")) {
icon = "⚠";
color = "text-yellow-400";
} else if (line.includes("[INFO]")) {
icon = "";
color = "text-blue-400";
}
return `<div class="log-line ${color}">${icon} ${escapeHtml(line)}</div>`;
})
.join("");
// Auto scroll to bottom
container.scrollTop = container.scrollHeight;
} else if (result.message) {
container.innerHTML = `
<div class="text-[#9dabb9]">
<span class="text-yellow-400">⚠</span> ${result.message}
</div>
`;
} else {
container.innerHTML = `
<div class="text-[#9dabb9]">
<span class="text-blue-400"></span> No hay logs de errores disponibles
</div>
`;
}
} catch (error) {
console.error("Error loading system errors:", error);
container.innerHTML = `
<div class="text-red-400">
<span class="text-red-400">✖</span> Error cargando logs del sistema
</div>
`;
}
}
// Load apps on page load
document.addEventListener("DOMContentLoaded", loadApps);
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
if (ws) {
ws.close();
}
});
</script>
</body>
</html>