// ========== LOG VIEWERS ========== (function () { // Inject logs-modal HTML injectModal('logs-modal', `

DNS Logs

Loading logs...
`); // ===== State ===== let currentDnsService = null; let logsInterval = null; let logsPaused = false; let currentContainerId = null; let currentContainerName = null; let containerLogsMode = false; let currentLogPath = null; let currentLogServiceName = null; let fileLogsMode = false; let logEventSource = null; let isStreaming = false; // ===== DNS LOGS ===== async function fetchDnsLogs(dnsId, lines = 25) { try { const serverIP = getDnsServerAddr(dnsId); const response = await fetch(`/api/v1/dns/logs?server=${serverIP}&limit=${lines}`, { cache: 'no-store', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }); if (response.ok) { const result = await response.json(); if (result.success && result.logs) { return { logs: result.logs, count: result.count, server: result.server }; } else { return { error: result.error || 'Failed to fetch logs' }; } } else if (response.status === 401) { return { error: 'DNS auto-auth failed - check credentials in settings' }; } else { return { error: `HTTP ${response.status}` }; } } catch (error) { console.error('DNS logs fetch failed:', error); return { error: error.message }; } } function getRcodeColor(rcode) { const colors = { 'NoError': 'var(--ok-fg)', 'NOERROR': 'var(--ok-fg)', 'NxDomain': 'var(--muted)', 'NXDOMAIN': 'var(--muted)', 'Refused': 'var(--bad-fg)', 'REFUSED': 'var(--bad-fg)', 'ServerFailure': '#f39c12', 'SERVFAIL': '#f39c12' }; return colors[rcode] || 'var(--fg)'; } function renderDnsLogEntry(log) { const div = document.createElement('div'); div.className = 'log-entry'; div.style.cssText = 'display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;'; // If unparsed raw log if (log.parsed === false) { div.style.gridTemplateColumns = '1fr'; div.innerHTML = `${escapeHtml(log.raw)}`; return div; } const rcodeColor = getRcodeColor(log.rcode); const isBlocked = log.rcode === 'Refused' || log.rcode === 'REFUSED'; div.innerHTML = ` ${escapeHtml(log.timestamp)} ${escapeHtml(log.client)} ${escapeHtml(log.domain)} ${escapeHtml(log.type)} ${escapeHtml(log.rcode)} `; return div; } async function updateLogsDisplay() { // Handle file logs mode if (fileLogsMode) { await updateFileLogsDisplay(); return; } // Handle container logs mode if (containerLogsMode) { await updateContainerLogsDisplay(); return; } // Handle DNS logs mode if (logsPaused || !currentDnsService) return; const lines = parseInt(document.getElementById('log-lines').value); const logsContent = document.getElementById('logs-content'); try { const result = await fetchDnsLogs(currentDnsService, lines); if (result.error) { logsContent.innerHTML = `
⚠️ Error
${escapeHtml(result.error)}
`; return; } // Add header row logsContent.innerHTML = `
Time Client Domain Type Status
`; if (result.logs && result.logs.length > 0) { result.logs.forEach(log => { const logElement = renderDnsLogEntry(log); logsContent.appendChild(logElement); }); } else { logsContent.innerHTML += `
No DNS queries logged yet
`; } } catch (error) { logsContent.innerHTML = `
Failed to fetch logs: ${escapeHtml(error.message)}
`; } } function openLogsModal(dnsId) { currentDnsService = dnsId; logsPaused = false; containerLogsMode = false; const modal = document.getElementById('logs-modal'); const title = document.getElementById('logs-title'); const pauseBtn = document.getElementById('logs-pause'); const streamBtn = document.getElementById('logs-stream'); title.textContent = `${dnsId.toUpperCase()} DNS Logs`; pauseBtn.textContent = '⏸️ Pause'; pauseBtn.classList.remove('paused'); // Hide stream button for DNS logs (only available for container logs) if (streamBtn) { streamBtn.style.display = 'none'; } modal.classList.add('show'); // Initial load updateLogsDisplay(); // Start auto-refresh every 3 seconds logsInterval = setInterval(updateLogsDisplay, DC.POLL.LOGS); } function closeLogsModal() { const modal = document.getElementById('logs-modal'); modal.classList.remove('show'); if (logsInterval) { clearInterval(logsInterval); logsInterval = null; } // Stop SSE streaming if active stopLogStreaming(); // Reset all log modes currentDnsService = null; containerLogsMode = false; currentContainerId = null; currentContainerName = null; fileLogsMode = false; currentLogPath = null; currentLogServiceName = null; logsPaused = false; } // ===== SSE LOG STREAMING ===== function startLogStreaming(containerId) { if (logEventSource) { stopLogStreaming(); } const streamBtn = document.getElementById('logs-stream'); const pauseBtn = document.getElementById('logs-pause'); const logsContent = document.getElementById('logs-content'); // Stop interval-based refresh if (logsInterval) { clearInterval(logsInterval); logsInterval = null; } try { logEventSource = new EventSource(`/api/v1/logs/stream/${containerId}`); isStreaming = true; streamBtn.classList.add('active'); streamBtn.textContent = 'πŸ”΄ Live'; streamBtn.title = 'Streaming - click to stop'; pauseBtn.style.display = 'none'; // Add streaming indicator to header const title = document.getElementById('logs-title'); if (!title.textContent.includes('πŸ”΄')) { title.innerHTML = title.textContent.replace('πŸ“‹', 'πŸ“‹ πŸ”΄'); } logEventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.error) { console.error('Stream error:', data.error); stopLogStreaming(); return; } // Append new log entry const entry = document.createElement('div'); entry.className = 'log-entry'; entry.style.cssText = 'display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;'; const streamType = data.stream || 'stdout'; const isError = streamType === 'stderr'; const levelColor = isError ? 'var(--bad-fg)' : 'var(--fg)'; const bgColor = isError ? 'var(--bad-bg)' : 'var(--ok-bg)'; const levelBadge = `${isError ? 'STDERR' : 'STDOUT'}`; entry.innerHTML = `
${levelBadge}
${escapeHtml(data.text)}
`; logsContent.appendChild(entry); // Auto-scroll to bottom logsContent.scrollTop = logsContent.scrollHeight; // Limit entries to prevent memory issues (keep last 500) while (logsContent.children.length > 500) { logsContent.removeChild(logsContent.firstChild); } } catch (err) { console.error('Error parsing stream data:', err); } }; logEventSource.onerror = (err) => { console.error('EventSource error:', err); stopLogStreaming(); }; } catch (err) { console.error('Failed to start streaming:', err); stopLogStreaming(); } } function stopLogStreaming() { if (logEventSource) { logEventSource.close(); logEventSource = null; } isStreaming = false; const streamBtn = document.getElementById('logs-stream'); const pauseBtn = document.getElementById('logs-pause'); const title = document.getElementById('logs-title'); if (streamBtn) { streamBtn.classList.remove('active'); streamBtn.textContent = 'πŸ“‘ Live'; streamBtn.title = 'Enable real-time streaming'; } if (pauseBtn) { pauseBtn.style.display = ''; } if (title) { title.textContent = title.textContent.replace(' πŸ”΄', ''); } // Restart interval-based refresh if container logs modal is open if (containerLogsMode && currentContainerId && !logsInterval) { logsInterval = setInterval(updateContainerLogsDisplay, DC.POLL.LOGS); } } // ===== CONTAINER LOGS ===== async function fetchContainerLogs(containerId, lines = 100) { try { const endpoint = `/api/v1/logs/container/${containerId}?tail=${lines}×tamps=true`; const response = await fetch(endpoint, { cache: 'no-store', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }); if (response.ok) { const result = await response.json(); if (result.success && result.logs) { return { logs: result.logs, count: result.count, containerName: result.containerName, containerId: result.containerId }; } else { return { error: result.error || 'Failed to fetch container logs' }; } } else { return { error: `HTTP ${response.status}: ${response.statusText}` }; } } catch (error) { console.error('Container logs fetch failed:', error); return { error: error.message }; } } function renderContainerLogEntry(log) { const div = document.createElement('div'); div.className = 'log-entry'; div.style.cssText = 'display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;'; const streamColor = log.stream === 'stderr' ? 'var(--bad-fg)' : 'var(--fg)'; const streamBadge = log.stream === 'stderr' ? 'STDERR' : 'STDOUT'; div.innerHTML = `
${streamBadge}
${escapeHtml(log.text)}
`; return div; } async function updateContainerLogsDisplay() { if (logsPaused || !currentContainerId || !containerLogsMode) return; const lines = parseInt(document.getElementById('log-lines').value); const logsContent = document.getElementById('logs-content'); try { const result = await fetchContainerLogs(currentContainerId, lines); if (result.error) { logsContent.innerHTML = `
⚠️ Error
${escapeHtml(result.error)}
`; return; } // Add header row logsContent.innerHTML = `
Stream Log Output
`; if (result.logs && result.logs.length > 0) { result.logs.forEach(log => { const logElement = renderContainerLogEntry(log); logsContent.appendChild(logElement); }); // Auto-scroll to bottom logsContent.scrollTop = logsContent.scrollHeight; } else { logsContent.innerHTML += `
No logs available for this container
`; } } catch (error) { logsContent.innerHTML = `
Failed to fetch logs: ${escapeHtml(error.message)}
`; } } function openContainerLogsModal(containerId, containerName) { currentContainerId = containerId; currentContainerName = containerName; containerLogsMode = true; fileLogsMode = false; logsPaused = false; // Stop any existing streaming stopLogStreaming(); const modal = document.getElementById('logs-modal'); const title = document.getElementById('logs-title'); const pauseBtn = document.getElementById('logs-pause'); const streamBtn = document.getElementById('logs-stream'); title.textContent = `πŸ“‹ ${containerName} - Container Logs`; pauseBtn.textContent = '⏸️ Pause'; pauseBtn.classList.remove('paused'); // Show stream button for container logs if (streamBtn) { streamBtn.style.display = ''; } modal.classList.add('show'); // Initial load updateContainerLogsDisplay(); // Start auto-refresh every 3 seconds logsInterval = setInterval(updateContainerLogsDisplay, DC.POLL.LOGS); } // ===== FILE-BASED LOGS (for native apps) ===== async function fetchFileLogs(logPath, lines = 100) { try { const endpoint = `/api/v1/logs/file?path=${encodeURIComponent(logPath)}&tail=${lines}`; const response = await fetch(endpoint, { cache: 'no-store', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }); if (response.ok) { const result = await response.json(); if (result.success && result.logs) { return { logs: result.logs, count: result.count, logPath: result.logPath, totalLines: result.totalLines }; } else { return { error: result.error || 'Failed to fetch file logs' }; } } else { const result = await response.json().catch(() => ({})); return { error: result.error || `HTTP ${response.status}` }; } } catch (error) { console.error('File logs fetch failed:', error); return { error: error.message }; } } function renderFileLogEntry(log) { const div = document.createElement('div'); div.className = 'log-entry'; div.style.cssText = 'display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;'; const text = log.text; let logLevel = 'INFO'; let levelColor = 'var(--fg)'; if (text.match(/ERROR|FATAL|CRITICAL/i)) { logLevel = 'ERROR'; levelColor = 'var(--bad-fg)'; } else if (text.match(/WARN|WARNING/i)) { logLevel = 'WARN'; levelColor = '#f39c12'; } else if (text.match(/DEBUG/i)) { logLevel = 'DEBUG'; levelColor = 'var(--muted)'; } const bgColor = levelColor === 'var(--bad-fg)' ? 'var(--bad-bg)' : 'var(--ok-bg)'; const levelBadge = `${logLevel}`; div.innerHTML = `
${levelBadge}
${escapeHtml(text)}
`; return div; } async function updateFileLogsDisplay() { if (logsPaused || !currentLogPath || !fileLogsMode) return; const lines = parseInt(document.getElementById('log-lines').value); const logsContent = document.getElementById('logs-content'); try { const result = await fetchFileLogs(currentLogPath, lines); if (result.error) { logsContent.innerHTML = `
⚠️ Error
${escapeHtml(result.error)}
`; return; } logsContent.innerHTML = `
Log Output (${result.count} of ${result.totalLines} lines)
`; if (result.logs && result.logs.length > 0) { result.logs.forEach(log => { const logElement = renderFileLogEntry(log); logsContent.appendChild(logElement); }); logsContent.scrollTop = logsContent.scrollHeight; } else { logsContent.innerHTML += `
No logs available in this file
`; } } catch (error) { logsContent.innerHTML = `
Failed to fetch logs: ${escapeHtml(error.message)}
`; } } function openFileLogsModal(logPath, serviceName) { currentLogPath = logPath; currentLogServiceName = serviceName; fileLogsMode = true; containerLogsMode = false; logsPaused = false; const modal = document.getElementById('logs-modal'); const title = document.getElementById('logs-title'); const pauseBtn = document.getElementById('logs-pause'); const streamBtn = document.getElementById('logs-stream'); title.textContent = `πŸ“‹ ${serviceName} - Application Logs`; pauseBtn.textContent = '⏸️ Pause'; pauseBtn.classList.remove('paused'); // Hide stream button for file logs (only available for container logs) if (streamBtn) { streamBtn.style.display = 'none'; } modal.classList.add('show'); updateFileLogsDisplay(); logsInterval = setInterval(updateFileLogsDisplay, DC.POLL.LOGS); } // ===== EVENT LISTENERS ===== // DNS log buttons β€” event delegation for dynamic cards document.querySelector('.top')?.addEventListener('click', (e) => { const logsBtn = e.target.closest('[id$="-logs"]'); if (!logsBtn) return; const dnsId = logsBtn.id.replace('-logs', ''); if (!SITE.dnsServers[dnsId]) return; openLogsModal(dnsId); }); document.getElementById('logs-close')?.addEventListener('click', closeLogsModal); document.getElementById('logs-pause')?.addEventListener('click', () => { logsPaused = !logsPaused; const pauseBtn = document.getElementById('logs-pause'); if (logsPaused) { pauseBtn.textContent = '▢️ Resume'; pauseBtn.classList.add('paused'); } else { pauseBtn.textContent = '⏸️ Pause'; pauseBtn.classList.remove('paused'); updateLogsDisplay(); } }); document.getElementById('log-lines')?.addEventListener('change', () => { if (!logsPaused) { updateLogsDisplay(); } }); // Stream button for real-time SSE logs (only works with container logs) document.getElementById('logs-stream')?.addEventListener('click', () => { if (!containerLogsMode || !currentContainerId) { // Streaming only available for container logs return; } if (isStreaming) { stopLogStreaming(); } else { startLogStreaming(currentContainerId); } }); // Close modal on outside click document.getElementById('logs-modal')?.addEventListener('click', (e) => { if (e.target.id === 'logs-modal') { closeLogsModal(); } }); // Close logs-modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (document.getElementById('logs-modal')?.classList.contains('show')) { closeLogsModal(); } } }); // ===== EXPORTS ===== window.openContainerLogsModal = openContainerLogsModal; window.openFileLogsModal = openFileLogsModal; window.openLogsModal = openLogsModal; })();