Files
dashcaddy/status/js/logs-page.js
T

172 lines
7.3 KiB
JavaScript

// Logs page (status/logs.html) — dedicated admin log viewer.
// Two tabs: Error log (calls /api/v1/error-logs) + Container (calls
// /api/v1/logs/containers + /api/v1/logs/container/:id). Mirrors the
// /api/v1/error-logs parser fix from DC-051 — U+2500 separator, capped
// tail, level filter.
(function() {
'use strict';
function escapeHtml(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function formatBytes(n) {
if (!Number.isFinite(n) || n <= 0) return '0 B';
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KiB';
return (n / 1024 / 1024).toFixed(2) + ' MiB';
}
const out = document.getElementById('output');
const meta = document.getElementById('meta');
const tabError = document.getElementById('tab-error');
const tabContainer = document.getElementById('tab-container');
const errorCtrls = document.getElementById('error-controls');
const containerCtrls = document.getElementById('container-controls');
let activeTab = 'error';
let containers = [];
function switchTab(tab) {
activeTab = tab;
tabError.classList.toggle('active', tab === 'error');
tabContainer.classList.toggle('active', tab === 'container');
errorCtrls.style.display = tab === 'error' ? 'flex' : 'none';
containerCtrls.style.display = tab === 'container' ? 'flex' : 'none';
if (tab === 'error') loadErrorLog();
else loadContainerList();
}
async function fetchJson(url, opts) {
const r = await fetch(url, opts);
const data = await r.json().catch(() => ({}));
if (!r.ok || (data && data.success === false)) {
throw new Error((data && (data.error || data.message)) || `HTTP ${r.status}`);
}
return data;
}
async function loadErrorLog() {
const tailEl = /** @type {HTMLSelectElement} */ (document.getElementById('tail'));
const levelEl = /** @type {HTMLSelectElement} */ (document.getElementById('level'));
const tail = tailEl.value;
const level = levelEl.value;
let qs = `tail=${encodeURIComponent(tail)}`;
if (level) qs += '&level=' + encodeURIComponent(level);
out.innerHTML = '<div class="logs-loading">Loading error log…</div>';
meta.textContent = '';
try {
const data = await fetchJson('/api/v1/error-logs?' + qs);
const logs = data.logs || [];
if (logs.length === 0) {
out.innerHTML = '<div class="empty">✅ No errors logged</div>';
} else {
out.innerHTML = logs.map((log, idx) => {
const date = new Date(log.timestamp).toLocaleString();
const lvl = (log.level || 'ERR').toUpperCase();
const cls = ['ERR','WRN','INF','DBG'].includes(lvl) ? lvl.toLowerCase() : 'error';
const details = log.details ? escapeHtml(log.details) : null;
return `
<div class="log-entry ${cls}">
<span class="log-timestamp">${escapeHtml(date)}</span>
<span class="log-level">${escapeHtml(lvl)}</span>
<div class="log-message">
<strong>${escapeHtml(log.context || '')}</strong>: ${escapeHtml(log.message || '')}
${details ? `<details><summary style="cursor:pointer;opacity:.7">stack + request context</summary><pre>${details}</pre></details>` : ''}
</div>
</div>
`;
}).join('');
}
const sizeStr = formatBytes(data.totalSize);
const truncStr = data.truncated ? ' (last 2 MiB)' : '';
const returnedStr = data.returned ?? logs.length;
meta.textContent = `${returnedStr} entries · ${sizeStr}${truncStr}`;
} catch (err) {
out.innerHTML = `<div class="empty">❌ ${escapeHtml(err.message)}</div>`;
}
}
async function clearErrorLog() {
if (!confirm('Clear all error logs?')) return;
try {
await fetchJson('/api/v1/error-logs', { method: 'DELETE' });
meta.textContent = '✅ cleared';
loadErrorLog();
} catch (err) {
meta.textContent = '❌ ' + err.message;
}
}
async function loadContainerList() {
const sel = document.getElementById('container-select');
out.innerHTML = '<div class="logs-loading">Loading containers…</div>';
document.getElementById('container-meta').textContent = '';
try {
const data = await fetchJson('/api/v1/logs/containers');
containers = data.containers || [];
sel.innerHTML = containers.map(c => {
const name = c.name || c.id;
const state = (c.status || 'unknown');
return `<option value="${escapeHtml(c.id)}">${escapeHtml(name)} (${escapeHtml(state)})</option>`;
}).join('');
if (containers.length === 0) {
out.innerHTML = '<div class="empty">No containers running</div>';
return;
}
loadContainerLog();
} catch (err) {
out.innerHTML = `<div class="empty">❌ ${escapeHtml(err.message)}</div>`;
}
}
async function loadContainerLog() {
const sel = /** @type {HTMLSelectElement} */ (document.getElementById('container-select'));
const tailEl = /** @type {HTMLSelectElement} */ (document.getElementById('container-tail'));
const tail = tailEl.value;
const id = sel.value;
if (!id) {
out.innerHTML = '<div class="empty">Select a container</div>';
return;
}
out.innerHTML = '<div class="logs-loading">Loading container logs…</div>';
document.getElementById('container-meta').textContent = '';
try {
const data = await fetchJson(`/api/v1/logs/container/${encodeURIComponent(id)}?tail=${encodeURIComponent(tail)}&timestamps=true`);
const logs = data.logs || [];
if (logs.length === 0) {
out.innerHTML = '<div class="empty">No log lines</div>';
} else {
out.innerHTML = logs.map(l => {
const cls = l.stream === 'stderr' ? 'error' : 'info';
const ts = l.timestamp || (data.logs.length ? '' : '');
return `
<div class="log-entry ${cls}">
${ts ? `<span class="log-timestamp">${escapeHtml(new Date(ts).toLocaleString())}</span>` : ''}
<span class="log-level">${l.stream === 'stderr' ? 'ERR' : 'OUT'}</span>
<div class="log-message"><pre style="margin:0;white-space:pre-wrap">${escapeHtml(l.text)}</pre></div>
</div>
`;
}).join('');
}
const containerName = data.containerName || '';
document.getElementById('container-meta').textContent = `${escapeHtml(containerName)} · ${logs.length} lines`;
} catch (err) {
out.innerHTML = `<div class="empty">❌ ${escapeHtml(err.message)}</div>`;
}
}
tabError.addEventListener('click', () => switchTab('error'));
tabContainer.addEventListener('click', () => switchTab('container'));
document.getElementById('refresh').addEventListener('click', loadErrorLog);
document.getElementById('clear').addEventListener('click', clearErrorLog);
document.getElementById('level').addEventListener('change', loadErrorLog);
document.getElementById('tail').addEventListener('change', loadErrorLog);
document.getElementById('container-refresh').addEventListener('click', loadContainerLog);
document.getElementById('container-select').addEventListener('change', loadContainerLog);
document.getElementById('container-tail').addEventListener('change', loadContainerLog);
switchTab('error');
})();