/* Logs page JavaScript */ // Logs State - guard against duplicate declarations on HTMX reload if (typeof window.logsModuleInitialized === "undefined") { window.logsModuleInitialized = true; var isStreaming = true; var autoScroll = true; var logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; var searchDebounceTimer = null; var currentFilters = { level: "all", service: "all", search: "", }; var logsWs = null; } function initLogsWebSocket() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`); logsWs.onopen = function () { updateLogsConnectionStatus("connected", "Connected"); }; logsWs.onclose = function () { updateLogsConnectionStatus("disconnected", "Disconnected"); // Reconnect after 3 seconds setTimeout(initLogsWebSocket, 3000); }; logsWs.onerror = function () { updateLogsConnectionStatus("disconnected", "Error"); }; logsWs.onmessage = function (event) { if (!isStreaming) return; try { const logData = JSON.parse(event.data); appendLog(logData); } catch (e) { console.error("Failed to parse log message:", e); } }; } function updateLogsConnectionStatus(status, text) { const statusEl = document.getElementById("connection-status"); if (statusEl) { statusEl.className = `connection-status ${status}`; statusEl.querySelector(".status-text").textContent = text; } } function appendLog(log) { const stream = document.getElementById("log-stream"); if (!stream) return; const placeholder = stream.querySelector(".log-placeholder"); if (placeholder) { placeholder.remove(); } const entry = document.createElement("div"); entry.className = "log-entry"; entry.dataset.level = log.level || "info"; entry.dataset.service = log.service || "unknown"; entry.dataset.id = log.id || Date.now(); entry.innerHTML = ` ${formatLogTimestamp(log.timestamp)} ${(log.level || "INFO").toUpperCase()} ${log.service || "unknown"} ${escapeLogHtml(log.message || "")} `; // Store full log data for detail view entry._logData = log; // Apply current filters if (!matchesLogFilters(entry)) { entry.classList.add("hidden"); } stream.appendChild(entry); // Update counts const level = log.level || "info"; if (logCounts[level] !== undefined) { logCounts[level]++; const countEl = document.getElementById(`${level}-count`); if (countEl) countEl.textContent = logCounts[level]; } const totalEl = document.getElementById("total-count"); if (totalEl) { totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0); } // Auto-scroll to bottom if (autoScroll) { stream.scrollTop = stream.scrollHeight; } // Limit log entries to prevent memory issues const maxEntries = 1000; while (stream.children.length > maxEntries) { const removed = stream.firstChild; if (removed._logData) { const removedLevel = removed._logData.level || "info"; if (logCounts[removedLevel] > 0) { logCounts[removedLevel]--; } } stream.removeChild(removed); } } function formatLogTimestamp(timestamp) { if (!timestamp) return "--"; const date = new Date(timestamp); return date.toISOString().replace("T", " ").slice(0, 23); } function escapeLogHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function matchesLogFilters(entry) { // Level filter if ( currentFilters.level !== "all" && entry.dataset.level !== currentFilters.level ) { return false; } // Service filter if ( currentFilters.service !== "all" && entry.dataset.service !== currentFilters.service ) { return false; } // Search filter if (currentFilters.search) { const text = entry.textContent.toLowerCase(); if (!text.includes(currentFilters.search.toLowerCase())) { return false; } } return true; } function applyLogFilters() { currentFilters.level = document.getElementById("log-level-filter")?.value || "all"; currentFilters.service = document.getElementById("service-filter")?.value || "all"; const entries = document.querySelectorAll(".log-entry"); entries.forEach((entry) => { if (matchesLogFilters(entry)) { entry.classList.remove("hidden"); } else { entry.classList.add("hidden"); } }); } function debounceLogSearch(value) { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { currentFilters.search = value; applyLogFilters(); }, 300); } function toggleStream() { isStreaming = !isStreaming; const btn = document.getElementById("stream-toggle"); if (!btn) return; if (isStreaming) { btn.classList.remove("paused"); btn.innerHTML = ` Pause `; } else { btn.classList.add("paused"); btn.innerHTML = ` Resume `; } } function clearLogs() { if (confirm("Are you sure you want to clear all logs?")) { const stream = document.getElementById("log-stream"); if (!stream) return; stream.innerHTML = `

Logs cleared

New logs will appear here
`; // Reset counts logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; Object.keys(logCounts).forEach((level) => { const el = document.getElementById(`${level}-count`); if (el) el.textContent = "0"; }); const totalEl = document.getElementById("total-count"); if (totalEl) totalEl.textContent = "0"; } } function downloadLogs() { const entries = document.querySelectorAll(".log-entry"); let logs = []; entries.forEach((entry) => { if (entry._logData) { logs.push(entry._logData); } }); const blob = new Blob([JSON.stringify(logs, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-")}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function scrollToTop() { const stream = document.getElementById("log-stream"); if (stream) { stream.scrollTop = 0; autoScroll = false; updateLogScrollButtons(); } } function scrollToBottom() { const stream = document.getElementById("log-stream"); if (stream) { stream.scrollTop = stream.scrollHeight; autoScroll = true; updateLogScrollButtons(); } } function updateLogScrollButtons() { const topBtn = document.getElementById("scroll-top-btn"); const bottomBtn = document.getElementById("scroll-bottom-btn"); if (topBtn) topBtn.classList.toggle("active", !autoScroll); if (bottomBtn) bottomBtn.classList.toggle("active", autoScroll); } function expandLog(btn) { const entry = btn.closest(".log-entry"); const logData = entry._logData || { timestamp: entry.querySelector(".log-timestamp").textContent, level: entry.dataset.level, service: entry.dataset.service, message: entry.querySelector(".log-message").textContent, }; const panel = document.getElementById("log-detail-panel"); const content = document.getElementById("log-detail-content"); if (!panel || !content) return; content.innerHTML = `
Timestamp
${logData.timestamp || "--"}
Level
${(logData.level || "info").toUpperCase()}
Service
${logData.service || "unknown"}
Message
${escapeLogHtml(logData.message || "")}
${ logData.stack ? `
Stack Trace
${escapeLogHtml(logData.stack)}
` : "" } ${ logData.context ? `
Context
${escapeLogHtml(JSON.stringify(logData.context, null, 2))}
` : "" } `; panel.classList.add("open"); } function closeLogDetail() { const panel = document.getElementById("log-detail-panel"); if (panel) panel.classList.remove("open"); } // Initialize on page load document.addEventListener("DOMContentLoaded", function () { // Initialize WebSocket connection if on logs page if (document.getElementById("log-stream")) { initLogsWebSocket(); } });