Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
673 lines
22 KiB
JavaScript
673 lines
22 KiB
JavaScript
// ========== LOG VIEWERS ==========
|
|
(function () {
|
|
|
|
// Inject logs-modal HTML
|
|
injectModal('logs-modal', `
|
|
<div id="logs-modal" class="logs-modal">
|
|
<div class="logs-modal-content" style="min-width: 800px; max-width: 1000px;">
|
|
<div class="logs-header">
|
|
<h3 id="logs-title">DNS Logs</h3>
|
|
<div class="logs-controls">
|
|
<label for="log-lines">Show:</label>
|
|
<select id="log-lines">
|
|
<option value="25" selected>25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
<option value="200">200</option>
|
|
</select>
|
|
<button id="logs-stream" class="stream-btn" title="Enable real-time streaming">📡 Live</button>
|
|
<button id="logs-pause" class="pause-btn">⏸️ Pause</button>
|
|
<button id="logs-close" class="close-btn">✕</button>
|
|
</div>
|
|
</div>
|
|
<div class="logs-container scroll-container">
|
|
<div id="logs-content" class="logs-content">
|
|
<div class="logs-loading">Loading logs...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
|
|
// ===== 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 = `<span style="color: var(--muted); font-family: monospace; font-size: 0.75rem;">${escapeHtml(log.raw)}</span>`;
|
|
return div;
|
|
}
|
|
|
|
const rcodeColor = getRcodeColor(log.rcode);
|
|
const isBlocked = log.rcode === 'Refused' || log.rcode === 'REFUSED';
|
|
|
|
div.innerHTML = `
|
|
<span style="color: var(--muted); font-size: 0.75rem;">${escapeHtml(log.timestamp)}</span>
|
|
<span style="color: var(--accent); font-size: 0.75rem;" title="${escapeHtml(log.client)}">${escapeHtml(log.client)}</span>
|
|
<span style="font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; ${isBlocked ? 'text-decoration: line-through; opacity: 0.6;' : ''}" title="${escapeHtml(log.domain)}">${escapeHtml(log.domain)}</span>
|
|
<span style="color: var(--muted); font-size: 0.75rem;">${escapeHtml(log.type)}</span>
|
|
<span style="color: ${rcodeColor}; font-weight: 500; font-size: 0.75rem;">${escapeHtml(log.rcode)}</span>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">⚠️ Error</div>
|
|
<div>${result.error}</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
// Add header row
|
|
logsContent.innerHTML = `
|
|
<div style="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
<span>Time</span>
|
|
<span>Client</span>
|
|
<span>Domain</span>
|
|
<span>Type</span>
|
|
<span>Status</span>
|
|
</div>`;
|
|
|
|
if (result.logs && result.logs.length > 0) {
|
|
result.logs.forEach(log => {
|
|
const logElement = renderDnsLogEntry(log);
|
|
logsContent.appendChild(logElement);
|
|
});
|
|
} else {
|
|
logsContent.innerHTML += `
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
No DNS queries logged yet
|
|
</div>`;
|
|
}
|
|
} catch (error) {
|
|
logsContent.innerHTML = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
Failed to fetch logs: ${error.message}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
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 = `<span style="background: ${bgColor}; color: ${levelColor}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${isError ? 'STDERR' : 'STDOUT'}</span>`;
|
|
|
|
entry.innerHTML = `
|
|
<div style="flex-shrink: 0;">${levelBadge}</div>
|
|
<div style="flex: 1; color: ${levelColor}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(data.text)}</div>
|
|
`;
|
|
|
|
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' ?
|
|
'<span style="background: var(--bad-bg); color: var(--bad-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDERR</span>' :
|
|
'<span style="background: var(--ok-bg); color: var(--ok-fg); padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600;">STDOUT</span>';
|
|
|
|
div.innerHTML = `
|
|
<div style="flex-shrink: 0;">${streamBadge}</div>
|
|
<div style="flex: 1; color: ${streamColor}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(log.text)}</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">⚠️ Error</div>
|
|
<div>${result.error}</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
// Add header row
|
|
logsContent.innerHTML = `
|
|
<div style="display: flex; gap: 12px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
<span style="flex-shrink: 0; width: 80px;">Stream</span>
|
|
<span style="flex: 1;">Log Output</span>
|
|
</div>`;
|
|
|
|
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 += `
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
No logs available for this container
|
|
</div>`;
|
|
}
|
|
} catch (error) {
|
|
logsContent.innerHTML = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
Failed to fetch logs: ${error.message}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
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 = `<span style="background: ${bgColor}; color: ${levelColor}; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; min-width: 50px; text-align: center;">${logLevel}</span>`;
|
|
|
|
div.innerHTML = `
|
|
<div style="flex-shrink: 0;">${levelBadge}</div>
|
|
<div style="flex: 1; color: ${levelColor}; word-break: break-all; white-space: pre-wrap;">${escapeHtml(text)}</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
<div style="font-size: 1.2rem; margin-bottom: 8px;">⚠️ Error</div>
|
|
<div>${result.error}</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
logsContent.innerHTML = `
|
|
<div style="display: flex; gap: 12px; padding: 8px 10px; background: var(--card-base); border-bottom: 2px solid var(--border); font-size: 0.75rem; font-weight: 600; color: var(--muted); position: sticky; top: 0;">
|
|
<span style="flex: 1;">Log Output (${result.count} of ${result.totalLines} lines)</span>
|
|
</div>`;
|
|
|
|
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 += `
|
|
<div style="padding: 20px; text-align: center; color: var(--muted);">
|
|
No logs available in this file
|
|
</div>`;
|
|
}
|
|
} catch (error) {
|
|
logsContent.innerHTML = `
|
|
<div style="padding: 20px; text-align: center; color: var(--bad-fg);">
|
|
Failed to fetch logs: ${error.message}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
})();
|