Upgrade logs
This commit is contained in:
@@ -256,23 +256,23 @@ const infoContent = {
|
||||
},
|
||||
'logs': {
|
||||
title: 'System Logs',
|
||||
description: 'View real-time system logs',
|
||||
description: 'View and tail split log files under logs/',
|
||||
sections: [
|
||||
{
|
||||
title: 'Overview',
|
||||
content: 'The Logs tab shows real-time logs from your P2NS instance. Logs are displayed in a terminal interface.'
|
||||
title: 'Log files',
|
||||
content: 'Choose a log from the dropdown: core.log (main process), proxy.log (Internal Proxy), dns.log (DNS resolver), plugins.log (plugin SDK), holesail.log (Holesail). Noisy proxy and DNS traffic no longer appears in core.'
|
||||
},
|
||||
{
|
||||
title: 'Log Levels',
|
||||
content: 'Logs are color-coded by level: INFO (normal), WARN (yellow), ERROR (red), and DEBUG (gray).'
|
||||
title: 'On disk',
|
||||
content: 'Each channel is appended to logs/<name>.log in the project directory (override with LOG_DIR). Files persist across restarts.'
|
||||
},
|
||||
{
|
||||
title: 'Log Buffer',
|
||||
content: 'The terminal maintains a buffer of the most recent log messages. Older logs are automatically removed to manage memory.'
|
||||
title: 'Live tail',
|
||||
content: 'Selecting a log subscribes via WebSocket for new lines. On connect, the last ~1000 lines are loaded as a snapshot.'
|
||||
},
|
||||
{
|
||||
title: 'Real-time Updates',
|
||||
content: 'Logs are updated in real-time via WebSocket connections. If the WebSocket is disconnected, the logs will pause until reconnection.'
|
||||
title: 'Console',
|
||||
content: 'Only core-channel logs are printed to the terminal console. Other channels are file + admin UI only.'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,9 +1,180 @@
|
||||
// Logs UI functions - terminal rendering
|
||||
// Logs UI — per-channel file tail via WebSocket + debounced filter
|
||||
|
||||
const LOG_CHANNEL_LABELS = {
|
||||
core: 'Core',
|
||||
proxy: 'Internal Proxy',
|
||||
dns: 'DNS',
|
||||
plugins: 'Plugins',
|
||||
holesail: 'Holesail'
|
||||
};
|
||||
|
||||
const LOG_FILTER_DEBOUNCE_MS = 200;
|
||||
|
||||
function getLogFilterDebounced() {
|
||||
if (!window._logFilterDebounced) {
|
||||
const debounce = window.sdk?.utils?.dom?.debounce;
|
||||
window._logFilterDebounced = debounce
|
||||
? debounce(() => {
|
||||
if (window.activeTab === 'logs') {
|
||||
paintLogTerminal(getActiveLogChannel());
|
||||
}
|
||||
}, LOG_FILTER_DEBOUNCE_MS)
|
||||
: () => {
|
||||
if (window.activeTab === 'logs') {
|
||||
paintLogTerminal(getActiveLogChannel());
|
||||
}
|
||||
};
|
||||
}
|
||||
return window._logFilterDebounced;
|
||||
}
|
||||
|
||||
function getActiveLogChannel() {
|
||||
return window.activeLogChannel || 'core';
|
||||
}
|
||||
|
||||
function getLogFilterQuery() {
|
||||
const el = document.getElementById('log-filter-input');
|
||||
return (el?.value || '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Match log line against filter (case-insensitive substring, or /pattern/flags regex).
|
||||
*/
|
||||
function lineMatchesLogFilter(line, query) {
|
||||
if (!query) return true;
|
||||
const text = String(line);
|
||||
if (query.length >= 2 && query.startsWith('/')) {
|
||||
const lastSlash = query.lastIndexOf('/');
|
||||
if (lastSlash > 0) {
|
||||
const pattern = query.slice(1, lastSlash);
|
||||
const flags = query.slice(lastSlash + 1) || 'i';
|
||||
try {
|
||||
return new RegExp(pattern, flags).test(text);
|
||||
} catch {
|
||||
// invalid regex — fall through to substring
|
||||
}
|
||||
}
|
||||
}
|
||||
return text.toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
function getFilteredLogLines(channel) {
|
||||
const buf = getLogBuffer(channel);
|
||||
const query = getLogFilterQuery();
|
||||
if (!query) {
|
||||
return { lines: buf, total: buf.length, shown: buf.length, query: '' };
|
||||
}
|
||||
const lines = buf.filter((line) => lineMatchesLogFilter(line, query));
|
||||
return { lines, total: buf.length, shown: lines.length, query };
|
||||
}
|
||||
|
||||
function updateLogFilterStats(channel) {
|
||||
const el = document.getElementById('log-filter-stats');
|
||||
if (!el) return;
|
||||
const { total, shown, query } = getFilteredLogLines(channel);
|
||||
if (!query) {
|
||||
el.textContent = total === 0 ? '—' : `${total} line${total === 1 ? '' : 's'}`;
|
||||
el.className = 'text-xs theme-text-tertiary tabular-nums whitespace-nowrap min-w-[5rem] text-right';
|
||||
return;
|
||||
}
|
||||
el.textContent = `${shown} / ${total}`;
|
||||
el.className = shown === 0
|
||||
? 'text-xs text-amber-400 tabular-nums whitespace-nowrap min-w-[5rem] text-right font-medium'
|
||||
: 'text-xs text-indigo-300 tabular-nums whitespace-nowrap min-w-[5rem] text-right font-medium';
|
||||
el.title = `${shown} of ${total} lines match “${query}”`;
|
||||
}
|
||||
|
||||
function updateLogFilterClearButton() {
|
||||
const btn = document.getElementById('log-filter-clear');
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('hidden', !getLogFilterQuery());
|
||||
}
|
||||
|
||||
function onLogFilterInput() {
|
||||
updateLogFilterClearButton();
|
||||
getLogFilterDebounced()();
|
||||
}
|
||||
|
||||
function clearLogFilter() {
|
||||
const input = document.getElementById('log-filter-input');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}
|
||||
updateLogFilterClearButton();
|
||||
paintLogTerminal(getActiveLogChannel());
|
||||
}
|
||||
|
||||
function getLogBuffer(channel) {
|
||||
if (!window.logBuffers) window.logBuffers = new Map();
|
||||
if (!window.logBuffers.has(channel)) {
|
||||
window.logBuffers.set(channel, []);
|
||||
}
|
||||
return window.logBuffers.get(channel);
|
||||
}
|
||||
|
||||
function pushLogLine(channel, line) {
|
||||
const buf = getLogBuffer(channel);
|
||||
buf.push(line);
|
||||
const max = window.maxLogLines || 1000;
|
||||
if (buf.length > max) buf.shift();
|
||||
}
|
||||
|
||||
function writelnToTerminal(line, channel) {
|
||||
if (!window.term) return;
|
||||
const ch = channel || getActiveLogChannel();
|
||||
if (!lineMatchesLogFilter(line, getLogFilterQuery())) return;
|
||||
window.term.writeln(line);
|
||||
}
|
||||
|
||||
function paintLogTerminal(channel) {
|
||||
if (!window.term) return;
|
||||
window.term.reset();
|
||||
const { lines } = getFilteredLogLines(channel);
|
||||
lines.forEach((line) => window.term.writeln(line));
|
||||
if (window.fitAddon) window.fitAddon.fit();
|
||||
updateLogFilterStats(channel);
|
||||
updateLogFilterClearButton();
|
||||
}
|
||||
|
||||
function subscribeLogChannel(channel) {
|
||||
window.activeLogChannel = channel;
|
||||
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
|
||||
window.ws.send(JSON.stringify({ type: 'subscribe-log', channel, lines: window.maxLogLines || 1000 }));
|
||||
}
|
||||
}
|
||||
|
||||
function onLogChannelChange() {
|
||||
const select = document.getElementById('log-channel-select');
|
||||
const channel = select?.value || 'core';
|
||||
subscribeLogChannel(channel);
|
||||
paintLogTerminal(channel);
|
||||
}
|
||||
|
||||
async function loadLogChannels() {
|
||||
const select = document.getElementById('log-channel-select');
|
||||
if (!select) return;
|
||||
try {
|
||||
const res = await fetch('/api/logs');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const channels = data.channels || [];
|
||||
select.innerHTML = channels
|
||||
.map((c) => `<option value="${c.id}">${c.label || LOG_CHANNEL_LABELS[c.id] || c.id}</option>`)
|
||||
.join('');
|
||||
const preferred = window.activeLogChannel || 'core';
|
||||
if (channels.some((c) => c.id === preferred)) {
|
||||
select.value = preferred;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load log channels:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
if (!window.terminalInitialized) {
|
||||
// Check if Terminal is available (from xterm.js CDN)
|
||||
if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') {
|
||||
console.error('Terminal or FitAddon not loaded. Make sure xterm.js scripts are loaded.');
|
||||
console.error('Terminal or FitAddon not loaded.');
|
||||
return;
|
||||
}
|
||||
window.term = new Terminal();
|
||||
@@ -18,18 +189,33 @@ function renderLogs() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (window.term) {
|
||||
window.term.reset();
|
||||
if (window.logBuffer && window.logBuffer.length > 0) {
|
||||
window.logBuffer.forEach(line => window.term.writeln(line));
|
||||
}
|
||||
if (window.fitAddon) {
|
||||
window.fitAddon.fit();
|
||||
}
|
||||
|
||||
loadLogChannels().then(() => {
|
||||
const channel = getActiveLogChannel();
|
||||
const select = document.getElementById('log-channel-select');
|
||||
if (select) select.value = channel;
|
||||
subscribeLogChannel(channel);
|
||||
paintLogTerminal(channel);
|
||||
});
|
||||
}
|
||||
|
||||
function applyLogSnapshot(data) {
|
||||
if (!data?.channel || !Array.isArray(data.lines)) return;
|
||||
window.logBuffers.set(data.channel, data.lines.slice(-(window.maxLogLines || 1000)));
|
||||
if (window.activeTab === 'logs' && data.channel === getActiveLogChannel()) {
|
||||
paintLogTerminal(data.channel);
|
||||
}
|
||||
}
|
||||
|
||||
function applyFileLog(data) {
|
||||
if (!data?.channel || !data.message) return;
|
||||
pushLogLine(data.channel, data.message);
|
||||
if (window.activeTab === 'logs' && data.channel === getActiveLogChannel()) {
|
||||
writelnToTerminal(data.message, data.channel);
|
||||
updateLogFilterStats(data.channel);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize for terminal
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.activeTab === 'logs' && window.fitAddon) {
|
||||
window.fitAddon.fit();
|
||||
@@ -41,4 +227,9 @@ window.addEventListener('resize', () => {
|
||||
});
|
||||
|
||||
window.renderLogs = renderLogs;
|
||||
|
||||
window.onLogChannelChange = onLogChannelChange;
|
||||
window.onLogFilterInput = onLogFilterInput;
|
||||
window.clearLogFilter = clearLogFilter;
|
||||
window.applyLogSnapshot = applyLogSnapshot;
|
||||
window.applyFileLog = applyFileLog;
|
||||
window.subscribeLogChannel = subscribeLogChannel;
|
||||
|
||||
@@ -6,6 +6,8 @@ window.wsReconnectAttempts = 0;
|
||||
window.wsPollingInterval = null;
|
||||
window.statusUpdateInterval = null;
|
||||
window.logBuffer = [];
|
||||
window.logBuffers = new Map();
|
||||
window.activeLogChannel = 'core';
|
||||
window.maxLogLines = 1000;
|
||||
window.terminalInitialized = false;
|
||||
window.holesailLogBuffers = new Map();
|
||||
|
||||
Reference in New Issue
Block a user