Add live log buffering support and endpoint; enhance log display functionality.
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
export interface LogArray extends Array<LogEntry> {
|
||||
}
|
||||
|
||||
export interface LiveLogEntry {
|
||||
seq: number,
|
||||
text: string,
|
||||
}
|
||||
|
||||
export interface LiveLogResponse {
|
||||
entries: LiveLogEntry[],
|
||||
dropped: boolean,
|
||||
next_seq: number,
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string,
|
||||
message_id: number,
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
<style>
|
||||
#livelogpanel {
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
background: #1a1a1a;
|
||||
color: #d4d4d4;
|
||||
padding: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #444;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.livelog-dropped {
|
||||
color: #f0a500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.log-accordion-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.log-accordion-header::before {
|
||||
content: "▶";
|
||||
font-size: 0.75em;
|
||||
transition: transform 0.15s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.log-accordion-header.open::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
#logpanel {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<button id="loadLog">Load Logs</button>
|
||||
<div id="logpanel">
|
||||
|
||||
</div>
|
||||
|
||||
<h4 id="logAccordionHeader" class="log-accordion-header">Application Log</h4>
|
||||
<div id="logpanel"></div>
|
||||
|
||||
<h4>Live Log</h4>
|
||||
<div id="livelogpanel"></div>
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import { Controller } from "./main";
|
||||
import {LogArray, LogLocalisation} from "./api";
|
||||
import {LiveLogResponse, LogArray, LogLocalisation} from "./api";
|
||||
|
||||
const LIVE_LOG_POLL_INTERVAL_MS = 2000;
|
||||
|
||||
export class LogView {
|
||||
private readonly logpanel: HTMLElement;
|
||||
private readonly loadLog: HTMLButtonElement;
|
||||
private readonly livelogpanel: HTMLElement;
|
||||
private readonly accordionHeader: HTMLElement;
|
||||
loglocale: LogLocalisation | undefined;
|
||||
|
||||
private liveLogNextSeq: number | undefined = undefined;
|
||||
private liveLogTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
private structuredLogLoaded = false;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
(document.getElementById("logview") as HTMLElement).innerHTML = require('./log.html') as string;
|
||||
this.logpanel = document.getElementById("logpanel") as HTMLElement
|
||||
this.loadLog = document.getElementById("loadLog") as HTMLButtonElement
|
||||
this.logpanel = document.getElementById("logpanel") as HTMLElement;
|
||||
this.livelogpanel = document.getElementById("livelogpanel") as HTMLElement;
|
||||
this.accordionHeader = document.getElementById("logAccordionHeader") as HTMLElement;
|
||||
|
||||
this.loadLog.onclick = () => {
|
||||
controller.loadLog();
|
||||
}
|
||||
this.accordionHeader.onclick = () => {
|
||||
const isOpen = this.logpanel.style.display !== "none";
|
||||
if (isOpen) {
|
||||
this.logpanel.style.display = "none";
|
||||
this.accordionHeader.classList.remove("open");
|
||||
} else {
|
||||
this.logpanel.style.display = "";
|
||||
this.accordionHeader.classList.add("open");
|
||||
if (!this.structuredLogLoaded) {
|
||||
this.structuredLogLoaded = true;
|
||||
controller.loadLog();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setLogLocalisation(loglocale: LogLocalisation) {
|
||||
@@ -21,10 +40,10 @@ export class LogView {
|
||||
}
|
||||
|
||||
setLog(logs: LogArray) {
|
||||
this.logpanel.textContent = ""
|
||||
this.logpanel.textContent = "";
|
||||
logs.forEach(entry => {
|
||||
let message = this.loglocale!![entry.message_id];
|
||||
let template = message.message
|
||||
let template = message.message;
|
||||
template = template.replace("${number_a}", entry.a.toString());
|
||||
template = template.replace("${number_b}", entry.b.toString());
|
||||
template = template.replace("${txt_short}", entry.txt_short.toString());
|
||||
@@ -32,15 +51,67 @@ export class LogView {
|
||||
|
||||
let ts = new Date(entry.timestamp);
|
||||
|
||||
let div = document.createElement("div")
|
||||
let timestampDiv = document.createElement("div")
|
||||
let messageDiv = document.createElement("div")
|
||||
let div = document.createElement("div");
|
||||
let timestampDiv = document.createElement("div");
|
||||
let messageDiv = document.createElement("div");
|
||||
timestampDiv.innerText = ts.toISOString();
|
||||
messageDiv.innerText = template;
|
||||
div.appendChild(timestampDiv)
|
||||
div.appendChild(messageDiv)
|
||||
this.logpanel.appendChild(div)
|
||||
}
|
||||
)
|
||||
div.appendChild(timestampDiv);
|
||||
div.appendChild(messageDiv);
|
||||
this.logpanel.appendChild(div);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startLivePoll(publicUrl: string) {
|
||||
if (this.liveLogTimer !== undefined) {
|
||||
return;
|
||||
}
|
||||
const poll = async () => {
|
||||
try {
|
||||
const url = this.liveLogNextSeq !== undefined
|
||||
? `${publicUrl}/live_log?after=${this.liveLogNextSeq}`
|
||||
: `${publicUrl}/live_log`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json() as LiveLogResponse;
|
||||
this.appendLiveLog(data);
|
||||
} catch (_e) {
|
||||
// network error — silently ignore, will retry next interval
|
||||
}
|
||||
this.liveLogTimer = setTimeout(poll, LIVE_LOG_POLL_INTERVAL_MS);
|
||||
};
|
||||
// Kick off immediately
|
||||
this.liveLogTimer = setTimeout(poll, 0);
|
||||
}
|
||||
|
||||
stopLivePoll() {
|
||||
if (this.liveLogTimer !== undefined) {
|
||||
clearTimeout(this.liveLogTimer);
|
||||
this.liveLogTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private appendLiveLog(data: LiveLogResponse) {
|
||||
const panel = this.livelogpanel;
|
||||
const wasAtBottom = panel.scrollHeight - panel.scrollTop <= panel.clientHeight + 4;
|
||||
|
||||
if (data.dropped) {
|
||||
const marker = document.createElement("div");
|
||||
marker.className = "livelog-dropped";
|
||||
marker.textContent = "[..]";
|
||||
panel.appendChild(marker);
|
||||
}
|
||||
|
||||
for (const entry of data.entries) {
|
||||
const line = document.createElement("div");
|
||||
line.textContent = entry.text;
|
||||
panel.appendChild(line);
|
||||
}
|
||||
|
||||
this.liveLogNextSeq = data.next_seq;
|
||||
|
||||
// Auto-scroll to bottom only if user was already at the bottom
|
||||
if (wasAtBottom) {
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +668,8 @@ async function executeTasksSequentially() {
|
||||
}
|
||||
|
||||
executeTasksSequentially().then(() => {
|
||||
controller.progressview.removeProgress("initial")
|
||||
controller.progressview.removeProgress("initial");
|
||||
controller.logView.startLivePoll(PUBLIC_URL);
|
||||
});
|
||||
|
||||
controller.progressview.removeProgress("rebooting");
|
||||
|
||||
Reference in New Issue
Block a user