Files
automatic/ui/logMonitor.ts
Vladimir Mandic 8ce3313f9a improve log viewer
Signed-off-by: Vladimir Mandic <mandic00@live.com>
2026-07-10 13:14:27 +02:00

209 lines
8.2 KiB
TypeScript

import { authFetch } from './authWrap';
import { log, error, xhrPost } from './logger';
import { timer } from './timers';
interface LogLine {
created: number;
level: string;
module: string;
facility: string;
msg: string;
}
let logMonitorEl: HTMLElement | null = null;
let logMonitorStatus = true;
let logWarnings = 0;
let logErrors = 0;
let logConnected = false;
function dateToStr(ts: number): string {
const dt = new Date(1000 * ts);
// const year = dt.getFullYear();
// const mo = String(dt.getMonth() + 1).padStart(2, '0');
// const day = String(dt.getDate()).padStart(2, '0');
const hour = String(dt.getHours()).padStart(2, '0');
const min = String(dt.getMinutes()).padStart(2, '0');
const sec = String(dt.getSeconds()).padStart(2, '0');
const ms = String(dt.getMilliseconds()).padStart(3, '0');
// const s = `${year}-${mo}-${day} ${hour}:${min}:${sec}.${ms}`;
const s = `${hour}:${min}:${sec}.${ms}`;
return s;
}
function htmlEscape(text: string): string {
return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
function parseLogLine(line: string): LogLine {
let str = line.replaceAll('\n', ' ').replaceAll('\\', '\\\\');
const tracebackIndex = str.indexOf('Traceback');
if (tracebackIndex !== -1) str = str.substring(0, tracebackIndex);
const parsed = JSON.parse(str) as Partial<LogLine>;
return {
created: Number(parsed.created ?? Date.now()),
level: String(parsed.level ?? 'INFO'),
module: String(parsed.module ?? 'logMonitor'),
facility: String(parsed.facility ?? 'ui'),
msg: String(parsed.msg ?? ''),
};
}
async function clearErrors(): Promise<void> {
logWarnings = 0;
logErrors = 0;
log('clearErrors');
}
export async function initClearErrorsButton() {
const btnServerClear = document.getElementById('btn_console_log_server_clear');
if (btnServerClear) {
btnServerClear.onclick = async (evt) => {
evt.preventDefault();
evt.stopPropagation();
clearErrors();
};
}
}
async function logMonitor() {
const addLogLine = (line: string): void => {
if (!logMonitorEl) logMonitorEl = document.getElementById('logMonitorData');
if (!logMonitorEl) return;
try {
const l = parseLogLine(line);
const row = document.createElement('tr');
// row.style = 'padding: 10px; margin: 0;';
const level = `<td style="color: var(--color-${l.level.toLowerCase()})">${l.level}</td>`;
if (l.level === 'WARNING') logWarnings++;
if (l.level === 'ERROR') logErrors++;
const module = `<td style="color: #ffca68">${l.module}</td>`;
const facilityText = l.facility.length > 20 ? `${l.facility.substring(0, 20)}...` : l.facility;
const facility = l.facility !== 'sd' ? `<td style="color: #ffca68">${facilityText}</td>` : '<td></td>';
row.innerHTML = `<td>${dateToStr(l.created)}</td>${level}${facility}${module}<td>${htmlEscape(l.msg)}</td>`;
logMonitorEl.appendChild(row);
} catch (err) {
error('logMonitor', { error: String(err), line });
}
};
const cleanupLog = (atBottom: boolean): void => {
if (!logMonitorEl) return;
while (logMonitorEl.childElementCount > 100 && logMonitorEl.firstElementChild) {
logMonitorEl.removeChild(logMonitorEl.firstElementChild);
}
if (atBottom) logMonitorEl.scrollTop = logMonitorEl.scrollHeight;
else if (logMonitorEl.parentElement) logMonitorEl.parentElement.style.cssText = 'border-bottom: 2px solid var(--highlight-color);';
const elWarn = document.getElementById('logWarnings');
const elErr = document.getElementById('logErrors');
const modenUIBtn = document.getElementById('btn_console');
if (elWarn) elWarn.innerText = String(logWarnings);
if (elErr) elErr.innerText = String(logErrors);
if (modenUIBtn) {
modenUIBtn.setAttribute('error-count', logErrors > 0 ? String(logErrors) : '');
modenUIBtn.style.backgroundColor = logErrors > 0 ? 'var(--color-error)' : '';
modenUIBtn.title = `Log\nErrors ${logErrors}\nWarnings ${logWarnings}`;
}
};
const txtGallery = document.getElementById('txt2img_gallery');
if (txtGallery) txtGallery.style.height = window.opts.logmonitor_show ? '50vh' : '55vh';
const imgGallery = document.getElementById('img2img_gallery');
if (imgGallery) imgGallery.style.height = window.opts.logmonitor_show ? '50vh' : '55vh';
if (!window.opts.logmonitor_show) {
Array.from<any>(document.getElementsByClassName('log-monitor')).forEach((el) => {
if (el instanceof HTMLElement) el.style.display = 'none';
});
return;
}
if (logMonitorStatus) setTimeout(logMonitor, window.opts.logmonitor_refresh_period);
else setTimeout(logMonitor, 10 * 1000); // on failure try to reconnect every 10sec
logMonitorStatus = false;
if (!logMonitorEl) {
logMonitorEl = document.getElementById('logMonitorData');
if (logMonitorEl) {
logMonitorEl.addEventListener('scroll', () => {
const atBottom = logMonitorEl.scrollHeight <= (logMonitorEl.scrollTop + logMonitorEl.clientHeight);
if (atBottom && logMonitorEl.parentElement) logMonitorEl.parentElement.style.cssText = '';
});
}
}
if (!logMonitorEl) return;
const atBottom = logMonitorEl.scrollHeight <= (logMonitorEl.scrollTop + logMonitorEl.clientHeight);
try {
const res = await authFetch(`${window.api}/log?clear=True`);
if (res?.ok) {
logMonitorStatus = true;
const lines = (await res.json()) as string[];
if (logMonitorEl && lines?.length > 0 && logMonitorEl.parentElement?.parentElement instanceof HTMLElement) {
logMonitorEl.parentElement.parentElement.style.display = window.opts.logmonitor_show ? 'block' : 'none';
}
if (!logConnected) {
logConnected = true;
xhrPost(`${window.api}/log`, { debug: 'connected' });
logErrors = 0; // reset error count on reconnect
}
for (const line of lines) addLogLine(line);
} else {
logConnected = false;
logErrors++;
if (res) addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: ${res?.status} ${res?.statusText}" }`);
else addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Server unreachable" }`);
}
cleanupLog(atBottom);
} catch {
logConnected = false;
logErrors++;
addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Server unreachable" }`);
cleanupLog(atBottom);
}
}
export async function initLogMonitor() {
let el = document.getElementById('logMonitorPlaceholder');
const modernUi = Boolean(el);
if (!el) el = document.getElementsByTagName('footer')[0];
if (!el) return;
const t0 = performance.now();
el.classList.add('log-monitor');
const uiDisabled = Array.isArray(window.opts.ui_disabled) ? window.opts.ui_disabled : [];
if (uiDisabled.includes('logs')) return;
if (modernUi) {
el.style.overflow = 'auto';
el.innerHTML = `
<table id="logMonitor" style="width: 100%;">
<tbody id="logMonitorData" style="white-space: nowrap; display: block">
</tbody>
</table>
`;
} else {
el.innerHTML = `
<table id="logMonitor" style="width: 100%;">
<thead style="display: block; text-align: left; border-bottom: solid 1px var(--button-primary-border-color)">
<tr>
<th style="width: 144px">Time</th>
<th>Level</th>
<th style="width: 0"></th>
<th style="width: 154px">Module</th>
<th>Message</th>
<th style="position: absolute; right: 7em">Warnings <span id="logWarnings">0</span></th>
<th style="position: absolute; right: 1em">Errors <span id="logErrors">0</span></th>
</tr>
</thead>
<tbody id="logMonitorData" style="white-space: nowrap; display: block; color: var(--neutral-400)">
</tbody>
</table>
`;
}
el.style.display = 'none';
authFetch(`${window.api}/start?agent=${encodeURI(navigator.userAgent)}`);
logMonitor();
initClearErrorsButton();
const t1 = performance.now();
log('initLogMonitor', { show: window.opts.logmonitor_show, time: Math.round(t1 - t0) });
timer('initLogMonitor', t1 - t0);
}