here
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user