// ========== LOG VIEWERS ==========
(function () {
// Inject logs-modal HTML
injectModal('logs-modal', `
`);
// ===== 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
${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: ${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
${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: ${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
${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: ${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;
})();