Files
mc-bot-town/nodejs/controller/log-web.js
T
2026-02-22 20:27:09 -05:00

197 lines
6.5 KiB
JavaScript

'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: `
<div id="logArea">
<div class="log-controls">
<label class="log-filter"><input type="checkbox" checked onchange="updateLogFilter()" id="logShowLog"> Log</label>
<label class="log-filter"><input type="checkbox" checked onchange="updateLogFilter()" id="logShowError"> Error</label>
<label class="log-filter"><input type="checkbox" checked onchange="updateLogFilter()" id="logShowWarn"> Warn</label>
<input type="text" id="logSearch" placeholder="Search logs..." oninput="rerenderLogs()" style="flex:1;padding:8px 12px;border:1px solid #374151;border-radius:6px;background:#111827;color:#e5e7eb;font-size:.85em">
</div>
<div class="log-feed" id="logFeed">
<div style="padding:20px;color:#6b7280;text-align:center">Loading logs...</div>
</div>
</div>
`,
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 '<div class="log-line level-' + entry.level + '">' +
'<span class="log-time">' + formatLogTime(entry.timestamp) + '</span>' +
'<span class="log-level ' + entry.level + '">' + entry.level.toUpperCase() + '</span>' +
escHtml(entry.text) +
'</div>';
}
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 = '<div style="padding:20px;color:#6b7280;text-align:center">No log entries</div>';
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 };