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
This commit is contained in:
2026-01-18 04:07:37 -05:00
parent 868f3a2d30
commit fbc89e9bf0
3 changed files with 185 additions and 5 deletions

View File

@@ -315,3 +315,46 @@ pub async fn get_monitored_apps_handler() -> Result<Json<serde_json::Value>, Sta
Ok(Json(response))
}
/// Endpoint para obtener los logs de errores del sistema
pub async fn get_system_error_logs() -> Result<Json<serde_json::Value>, StatusCode> {
use std::fs;
use std::path::Path;
let log_path = "logs/errors.log";
// Verificar si el archivo existe
if !Path::new(log_path).exists() {
return Ok(Json(serde_json::json!({
"success": true,
"logs": [],
"message": "Archivo de logs no encontrado"
})));
}
// Leer el archivo
match fs::read_to_string(log_path) {
Ok(content) => {
// Dividir en líneas y tomar las últimas 500
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let recent_lines: Vec<&str> = if lines.len() > 500 {
lines[lines.len() - 500..].to_vec()
} else {
lines
};
Ok(Json(serde_json::json!({
"success": true,
"logs": recent_lines,
"total_lines": total
})))
}
Err(e) => {
Ok(Json(serde_json::json!({
"success": false,
"error": format!("Error leyendo archivo: {}", e)
})))
}
}
}

View File

@@ -68,6 +68,7 @@ async fn main() {
let api_router = Router::new()
.route("/api/health", get(api::health_handler))
.route("/api/monitored", get(api::get_monitored_apps_handler))
.route("/api/logs/errors", get(api::get_system_error_logs))
.route("/api/apps", get(api::list_apps_handler).post(api::register_app_handler))
.route("/api/apps/:name", delete(api::unregister_app_handler))
.route("/api/apps/:name/status", get(api::get_app_status_handler))

View File

@@ -207,10 +207,38 @@
</div>
</div>
<!-- Terminal Log Output -->
<!-- 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
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm"
id="log-terminal"
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 -->
@@ -224,6 +252,19 @@
</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>
@@ -402,8 +443,9 @@
logContainer.appendChild(logEntry);
// Auto-scroll
if (autoScroll) {
const terminal = document.getElementById("log-terminal");
if (autoScroll && currentTab === "app-logs") {
const terminal =
document.getElementById("content-app-logs");
terminal.scrollTop = terminal.scrollHeight;
}
@@ -460,6 +502,100 @@
`;
}
// 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);