forked from wmantly/mc-bot-town
197 lines
6.5 KiB
JavaScript
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 };
|