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))
|
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()
|
let api_router = Router::new()
|
||||||
.route("/api/health", get(api::health_handler))
|
.route("/api/health", get(api::health_handler))
|
||||||
.route("/api/monitored", get(api::get_monitored_apps_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", get(api::list_apps_handler).post(api::register_app_handler))
|
||||||
.route("/api/apps/:name", delete(api::unregister_app_handler))
|
.route("/api/apps/:name", delete(api::unregister_app_handler))
|
||||||
.route("/api/apps/:name/status", get(api::get_app_status_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>
|
||||||
</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
|
<div
|
||||||
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm"
|
id="content-app-logs"
|
||||||
id="log-terminal"
|
class="flex-1 bg-[#0a0f16] overflow-y-auto p-4 font-mono text-sm tab-content"
|
||||||
>
|
>
|
||||||
<div id="log-container" class="space-y-1">
|
<div id="log-container" class="space-y-1">
|
||||||
<!-- Welcome Message -->
|
<!-- Welcome Message -->
|
||||||
@@ -224,6 +252,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -402,8 +443,9 @@
|
|||||||
logContainer.appendChild(logEntry);
|
logContainer.appendChild(logEntry);
|
||||||
|
|
||||||
// Auto-scroll
|
// Auto-scroll
|
||||||
if (autoScroll) {
|
if (autoScroll && currentTab === "app-logs") {
|
||||||
const terminal = document.getElementById("log-terminal");
|
const terminal =
|
||||||
|
document.getElementById("content-app-logs");
|
||||||
terminal.scrollTop = terminal.scrollHeight;
|
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
|
// Load apps on page load
|
||||||
document.addEventListener("DOMContentLoaded", loadApps);
|
document.addEventListener("DOMContentLoaded", loadApps);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user