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

144 lines
6.8 KiB
JavaScript

// ========== ERROR LOG VIEWER ==========
// DC-051: The /api/v1/error-logs route now parses the unified-logger
// ── (U+2500) separator (verified 2026-08-18 — previous '=' splitter
// returned ZERO entries and the modal always rendered "No errors logged").
// The modal now renders the captured `details` (stack trace + req context)
// and offers a Level filter + tail cap mirroring the audit-log viewer
// (DC-050). Mirrors the audit-log-viewer shape (5f95fdc).
(function() {
const MAX_LEVELS = ['ERR', 'WRN', 'INF', 'DBG'];
injectModal('error-log-modal', [
'<div id="error-log-modal" class="logs-modal">',
' <div class="logs-modal-content">',
' <div class="logs-header">',
' <h3>📋 Error Logs</h3>',
' <div class="logs-controls">',
' <select id="error-log-level" aria-label="Filter by level" style="padding:4px 8px!important;font-size:.85rem!important">',
' <option value="">All levels</option>',
' <option value="ERR">Errors</option>',
' <option value="WRN">Warnings</option>',
' <option value="INF">Info</option>',
' <option value="DBG">Debug</option>',
' </select>',
' <select id="error-log-tail" aria-label="Tail length" style="padding:4px 8px!important;font-size:.85rem!important">',
' <option value="50">Last 50</option>',
' <option value="100" selected>Last 100</option>',
' <option value="200">Last 200</option>',
' <option value="500">Last 500</option>',
' </select>',
' <button id="error-log-refresh" style="padding:4px 12px!important;font-size:.85rem!important">🔄 Refresh</button>',
' <button id="error-log-clear" style="padding:4px 12px!important;font-size:.85rem!important;background:color-mix(in srgb,var(--bad-fg) 15%,transparent)!important;border-color:var(--bad-fg)!important;color:var(--bad-fg)!important">🗑️ Clear</button>',
' <button id="error-log-close" class="close-btn">✕</button>',
' </div>',
' </div>',
' <div class="logs-container">',
' <div id="error-log-meta" class="logs-meta" style="padding:6px 12px;color:var(--muted);font-size:.8rem;border-bottom:1px solid var(--border)"></div>',
' <div id="error-log-content" class="logs-content"><div class="logs-loading">Loading error logs...</div></div>',
' </div>',
' </div>',
'</div>',
].join(''));
const modal = document.getElementById('error-log-modal');
const content = document.getElementById('error-log-content');
const meta = document.getElementById('error-log-meta');
const viewBtn = document.getElementById('view-error-logs');
const refreshBtn = document.getElementById('error-log-refresh');
const clearBtn = document.getElementById('error-log-clear');
const closeBtn = document.getElementById('error-log-close');
const levelSelect = /** @type {HTMLSelectElement|null} */ (document.getElementById('error-log-level'));
const tailSelect = /** @type {HTMLSelectElement|null} */ (document.getElementById('error-log-tail'));
function levelClass(level) {
const L = (level || '').toUpperCase();
if (L === 'ERR') return 'log-entry error';
if (L === 'WRN') return 'log-entry warn';
if (L === 'INF') return 'log-entry info';
if (L === 'DBG') return 'log-entry debug';
return 'log-entry';
}
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';
}
async function loadErrorLogs() {
content.innerHTML = '<div class="logs-loading">Loading error logs...</div>';
meta.textContent = '';
const tail = encodeURIComponent(tailSelect.value || '100');
const level = levelSelect.value || '';
const qs = `tail=${tail}` + (level ? `&level=${encodeURIComponent(level)}` : '');
try {
const response = await fetch('/api/v1/error-logs?' + qs);
const data = await response.json();
if (data.success && data.logs) {
if (data.logs.length === 0) {
content.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--muted);">✅ No errors logged! Everything is working smoothly.</div>';
} else {
content.innerHTML = data.logs.map((log, idx) => {
const date = new Date(log.timestamp).toLocaleString();
const lvl = (log.level || 'ERR').toUpperCase();
const detailsId = `error-log-details-${idx}`;
const details = log.details ? escapeHtml(log.details) : null;
const ctx = log.context ? `<strong>${escapeHtml(log.context)}</strong>: ` : '';
const msg = escapeHtml(log.message || '');
return `
<div class="${levelClass(log.level)}">
<span class="log-timestamp">${escapeHtml(date)}</span>
<span class="log-level">${escapeHtml(lvl)}</span>
<div class="log-message">
${ctx}${msg}
${details ? `<br><details id="${detailsId}"><summary style="cursor:pointer;opacity:.7">stack + request context</summary><pre style="margin:6px 0 0;font-size:.75rem;background:var(--card-bg);padding:8px;border-radius:4px;overflow-x:auto">${details}</pre></details>` : ''}
</div>
</div>
`;
}).join('');
}
const sizeStr = formatBytes(data.totalSize);
const truncStr = data.truncated ? ' (showing last 2 MiB)' : '';
const returnedStr = `${data.returned ?? data.logs.length}`;
meta.textContent = `${returnedStr} entries · ${sizeStr}${truncStr}`;
} else {
content.innerHTML = '<div style="padding: 20px; color: var(--bad-fg);">❌ Failed to load error logs</div>';
}
} catch (error) {
content.innerHTML = `<div style="padding: 20px; color: var(--bad-fg);">❌ Error loading logs: ${escapeHtml(error.message)}</div>`;
}
}
async function clearErrorLogs() {
if (!confirm('Clear all error logs?')) return;
try {
const response = await secureFetch('/api/v1/error-logs', { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showNotification('✅ Error logs cleared', 'success', 3000);
loadErrorLogs();
} else {
showNotification('❌ Failed to clear logs', 'error', 3000);
}
} catch (error) {
showNotification(`❌ Error: ${error.message}`, 'error', 3000);
}
}
viewBtn?.addEventListener('click', () => {
modal.classList.add('show');
loadErrorLogs();
});
refreshBtn?.addEventListener('click', loadErrorLogs);
clearBtn?.addEventListener('click', clearErrorLogs);
levelSelect?.addEventListener('change', loadErrorLogs);
tailSelect?.addEventListener('change', loadErrorLogs);
wireModal(modal, closeBtn);
})();