// 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 = '
Loading error log…
'; meta.textContent = ''; try { const data = await fetchJson('/api/v1/error-logs?' + qs); const logs = data.logs || []; if (logs.length === 0) { out.innerHTML = '
✅ No errors logged
'; } 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 `
${escapeHtml(date)} ${escapeHtml(lvl)}
${escapeHtml(log.context || '')}: ${escapeHtml(log.message || '')} ${details ? `
stack + request context
${details}
` : ''}
`; }).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 = `
❌ ${escapeHtml(err.message)}
`; } } 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 = '
Loading containers…
'; 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 ``; }).join(''); if (containers.length === 0) { out.innerHTML = '
No containers running
'; return; } loadContainerLog(); } catch (err) { out.innerHTML = `
❌ ${escapeHtml(err.message)}
`; } } 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 = '
Select a container
'; return; } out.innerHTML = '
Loading container logs…
'; document.getElementById('container-meta').textContent = ''; try { const data = await fetchJson(`/api/v1/logs/container/${encodeURIComponent(id)}?tail=${encodeURIComponent(tail)}×tamps=true`); const logs = data.logs || []; if (logs.length === 0) { out.innerHTML = '
No log lines
'; } else { out.innerHTML = logs.map(l => { const cls = l.stream === 'stderr' ? 'error' : 'info'; const ts = l.timestamp || (data.logs.length ? '' : ''); return `
${ts ? `${escapeHtml(new Date(ts).toLocaleString())}` : ''} ${l.stream === 'stderr' ? 'ERR' : 'OUT'}
${escapeHtml(l.text)}
`; }).join(''); } const containerName = data.containerName || ''; document.getElementById('container-meta').textContent = `${escapeHtml(containerName)} · ${logs.length} lines`; } catch (err) { out.innerHTML = `
❌ ${escapeHtml(err.message)}
`; } } 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'); })();