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:
@@ -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)
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
146
web/logs.html
146
web/logs.html
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user