// Gateway log viewer — the in-memory ring served by /admin/api/logs, for // everyone who never sees the terminal (Docker, autostart, double-clicked exe). import { h, mount, toast } from "../ui.js"; import { t } from "../i18n.js"; import { api } from "../api.js"; export function renderLogs() { const root = h("div"); let timer = null; let paused = false; let filter = ""; let level = ""; const pre = h("pre", { class: "conv-log logs-view" }, t("common.loading")); const filterIn = h("input", { placeholder: t("logs.filterPlaceholder"), style: "max-width:260px" }); const levelSel = h( "select", { style: "max-width:130px" }, h("option", { value: "" }, t("common.all")), ...["ERROR", "WARN", "INFO", "DEBUG"].map((l) => h("option", { value: l }, l)) ); const pauseBtn = h("button", { class: "btn ghost sm" }, "⏸ " + t("logs.pause")); const copyBtn = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy")); const clearBtn = h("button", { class: "btn ghost sm danger" }, "🗑 " + t("dash.clear")); clearBtn.addEventListener("click", async () => { try { await api.clearLogs(); lines = []; paint(); toast(t("dash.cleared"), "ok"); } catch (e) { toast(String(e.message || e), "err"); } }); let lines = []; function paint() { let shown = lines; if (level) shown = shown.filter((l) => l.includes(` ${level} `) || l.includes(`${level} `)); if (filter) shown = shown.filter((l) => l.toLowerCase().includes(filter)); pre.textContent = shown.length ? shown.join("\n") : t("logs.empty"); if (!paused) pre.scrollTop = pre.scrollHeight; } async function refresh() { if (paused) return; try { lines = (await api.logs(500)).lines || []; paint(); } catch { /* transient */ } } filterIn.addEventListener("input", () => { filter = filterIn.value.trim().toLowerCase(); paint(); }); levelSel.addEventListener("change", () => { level = levelSel.value; paint(); }); pauseBtn.addEventListener("click", () => { paused = !paused; pauseBtn.textContent = (paused ? "▶ " : "⏸ ") + t(paused ? "logs.resume" : "logs.pause"); if (!paused) refresh(); }); copyBtn.addEventListener("click", async () => { try { await navigator.clipboard.writeText(pre.textContent); toast(t("common.copied"), "ok"); } catch (e) { toast(String(e.message || e), "err"); } }); timer = setInterval(() => { if (!root.isConnected) { clearInterval(timer); return; } refresh(); }, 2000); refresh(); mount( root, h("div", { class: "view-head" }, h("h2", {}, t("nav.logs")), h("div", { class: "muted" }, t("logs.subtitle"))), h( "div", { class: "card" }, h("div", { class: "flex", style: "gap:8px; flex-wrap:wrap; margin-bottom:10px" }, filterIn, levelSel, pauseBtn, copyBtn, clearBtn), pre ) ); return root; }