/* Logs page JavaScript */ // Logs State let isStreaming = true; let autoScroll = true; let logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; let searchDebounceTimer = null; let currentFilters = { level: 'all', service: 'all', search: '' }; let 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(); } });