'use strict'; const express = require('express'); // In-memory ring buffer for log entries const MAX_ENTRIES = 1000; const logEntries = []; let logId = 0; // Monkey-patch console to capture output const origLog = console.log; const origError = console.error; const origWarn = console.warn; function captureLog(level, args) { const text = args.map(a => { if (typeof a === 'string') return a; try { return JSON.stringify(a); } catch(e) { return String(a); } }).join(' '); logEntries.push({ id: ++logId, level, text, timestamp: Date.now(), }); if (logEntries.length > MAX_ENTRIES) logEntries.splice(0, logEntries.length - MAX_ENTRIES); } console.log = function (...args) { captureLog('log', args); origLog.apply(console, args); }; console.error = function (...args) { captureLog('error', args); origError.apply(console, args); }; console.warn = function (...args) { captureLog('warn', args); origWarn.apply(console, args); }; function createRouter() { const router = express.Router(); router.get('/api/logs', (req, res) => { try { const since = parseInt(req.query.since) || 0; const level = req.query.level; // comma-separated: "log,error,warn" const allowedLevels = level ? new Set(level.split(',')) : null; let filtered = since ? logEntries.filter(e => e.id > since) : logEntries.slice(-200); if (allowedLevels) { filtered = filtered.filter(e => allowedLevels.has(e.level)); } res.json({ entries: filtered, lastId: logId }); } catch (error) { res.status(500).json({ error: error.message }); } }); return router; } const webUI = { tabId: 'logs', tabLabel: 'Logs', tabOrder: 25, html: `
Loading logs...
`, css: ` #logArea{display:flex;flex-direction:column;height:calc(100vh - 140px)} .log-controls{display:flex;gap:10px;align-items:center;margin-bottom:8px;flex-wrap:wrap} .log-filter{font-size:.85em;color:#9ca3af;display:flex;align-items:center;gap:4px;cursor:pointer} .log-filter input[type=checkbox]{accent-color:#2563eb} .log-feed{flex:1;overflow-y:auto;padding:8px;background:#0f172a;border:1px solid #374151;border-radius:8px;font-family:'Consolas','Monaco',monospace;font-size:.8em;line-height:1.5} .log-line{padding:2px 4px;word-wrap:break-word;border-bottom:1px solid #1e293b} .log-line .log-time{color:#4b5563;margin-right:6px;font-size:.85em} .log-line .log-level{font-weight:700;margin-right:6px;font-size:.8em;padding:1px 5px;border-radius:3px} .log-line .log-level.log{color:#60a5fa;background:rgba(96,165,250,.1)} .log-line .log-level.error{color:#ef4444;background:rgba(239,68,68,.1)} .log-line .log-level.warn{color:#f59e0b;background:rgba(245,158,11,.1)} .log-line.level-error{background:rgba(239,68,68,.05)} .log-line.level-warn{background:rgba(245,158,11,.05)} `, onTabActive: 'onLogTabActive', js: ` let logLastId=0, logInterval=null, logAutoScroll=true, allLogEntries=[]; function onLogTabActive() { loadLogs(); if (!logInterval) logInterval = setInterval(() => { if (currentTab === 'logs') pollLogs(); }, 2000); } function getLogLevels() { const levels = []; if (document.getElementById('logShowLog').checked) levels.push('log'); if (document.getElementById('logShowError').checked) levels.push('error'); if (document.getElementById('logShowWarn').checked) levels.push('warn'); return levels; } function updateLogFilter() { rerenderLogs(); } async function loadLogs() { try { const r = await fetch('/api/logs'); if (!r.ok) return; const d = await r.json(); logLastId = d.lastId || 0; allLogEntries = d.entries || []; rerenderLogs(); } catch(e) {} } async function pollLogs() { try { const r = await fetch('/api/logs?since=' + logLastId); if (!r.ok) return; const d = await r.json(); if (d.entries && d.entries.length > 0) { logLastId = d.lastId || logLastId; allLogEntries = allLogEntries.concat(d.entries); if (allLogEntries.length > 2000) allLogEntries = allLogEntries.slice(-1500); appendLogEntries(d.entries); } } catch(e) {} } function formatLogTime(ts) { const d = new Date(ts); return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); } function renderLogLine(entry) { return '
' + '' + formatLogTime(entry.timestamp) + '' + '' + entry.level.toUpperCase() + '' + escHtml(entry.text) + '
'; } function matchesLogFilter(entry) { const levels = getLogLevels(); if (!levels.includes(entry.level)) return false; const q = (document.getElementById('logSearch').value || '').toLowerCase(); if (q && !entry.text.toLowerCase().includes(q)) return false; return true; } function rerenderLogs() { const feed = document.getElementById('logFeed'); const filtered = allLogEntries.filter(matchesLogFilter); if (filtered.length === 0) { feed.innerHTML = '
No log entries
'; return; } feed.innerHTML = filtered.map(renderLogLine).join(''); if (logAutoScroll) feed.scrollTop = feed.scrollHeight; } function appendLogEntries(entries) { const feed = document.getElementById('logFeed'); const placeholder = feed.querySelector('div[style]'); if (placeholder && feed.children.length === 1 && placeholder.textContent.includes('No log')) { feed.innerHTML = ''; } const filtered = entries.filter(matchesLogFilter); for (const entry of filtered) { feed.insertAdjacentHTML('beforeend', renderLogLine(entry)); } while (feed.children.length > 1500) feed.removeChild(feed.firstChild); if (logAutoScroll) feed.scrollTop = feed.scrollHeight; } document.getElementById('logFeed').addEventListener('scroll', function() { logAutoScroll = this.scrollTop + this.clientHeight >= this.scrollHeight - 30; }); `, }; module.exports = { name: 'Logs', createRouter, webUI };