import { Controller } from "./main"; import {LiveLogResponse, LogArray, LogLocalisation} from "./api"; const LIVE_LOG_POLL_INTERVAL_MS = 2000; export class LogView { private readonly logpanel: HTMLElement; private readonly livelogpanel: HTMLElement; private readonly accordionHeader: HTMLElement; loglocale: LogLocalisation | undefined; private liveLogNextSeq: number | undefined = undefined; private liveLogTimer: ReturnType | 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.livelogpanel = document.getElementById("livelogpanel") as HTMLElement; this.accordionHeader = document.getElementById("logAccordionHeader") as HTMLElement; 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) { this.loglocale = loglocale; } setLog(logs: LogArray) { this.logpanel.textContent = ""; logs.forEach(entry => { let message = this.loglocale!![entry.message_id]; 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()); template = template.replace("${txt_long}", entry.txt_long.toString()); let ts = new Date(entry.timestamp); 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); }); } 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; } } }