// Diagnostic logging (frontend side) // Maintains a client-side ring buffer, fetches server logs, and wires UI controls. import { getDiagLog, setDiagLevel, clearDiagLog } from "./api.js"; const UI_MAX = 500; const uiBuffer = []; let uiEnabled = false; let uiLevel = "normal"; let clickListenerAttached = false; function appendUi(level, msg, meta = null) { if (!uiEnabled) return; if (level === "debug" && uiLevel !== "debug") return; const ts = new Date().toISOString(); const entry = { ts, level, msg, meta, source: "ui" }; uiBuffer.unshift(entry); if (uiBuffer.length > UI_MAX) uiBuffer.length = UI_MAX; } function attachClickTracker() { if (clickListenerAttached) return; clickListenerAttached = true; document.addEventListener( "click", (e) => { if (!uiEnabled || uiLevel !== "debug") return; const el = e.target.closest("button,input,select,textarea,label"); if (!el) return; const label = el.getAttribute("aria-label") || el.getAttribute("title") || el.textContent?.trim()?.slice(0, 60) || el.id || el.tagName.toLowerCase(); appendUi("debug", `UI click: ${label || el.tagName}`, { id: el.id || null, type: el.tagName.toLowerCase(), }); }, true, ); } export function logUi(msg, level = "info", meta) { appendUi(level, msg, meta); } export async function initDiagUI({ elements, toast }) { const { enableToggle, debugToggle, refreshBtn, clearBtn, copyBtn, downloadBtn, logBox, statusEl } = elements; async function syncState() { const data = await getDiagLog(); const state = data.state || {}; uiEnabled = !!state.enabled; uiLevel = state.level || "normal"; if (enableToggle) enableToggle.checked = uiEnabled; if (debugToggle) debugToggle.checked = uiLevel === "debug"; return data.entries || []; } function render(entries) { if (!logBox) return; const merged = [ ...(entries || []).map((e) => ({ ...e, source: "api" })), ...uiBuffer, ].sort((a, b) => (a.ts < b.ts ? 1 : -1)); logBox.textContent = merged .map((e) => `${new Date(e.ts).toLocaleTimeString()} [${e.source || "api"} ${e.level}] ${e.msg}`) .join("\n"); if (statusEl) statusEl.textContent = `${merged.length} entries`; } async function refresh() { try { const entries = await syncState(); render(entries); toast?.("Diagnostics refreshed", "success"); } catch (e) { toast?.(e.error || "Failed to load diagnostics", "error"); } } enableToggle?.addEventListener("change", async () => { try { uiEnabled = enableToggle.checked; await setDiagLevel({ enabled: uiEnabled, level: uiLevel }); appendUi("info", `Diagnostics ${uiEnabled ? "enabled" : "disabled"}`); if (uiEnabled) attachClickTracker(); await refresh(); } catch (e) { toast?.(e.error || "Failed to save diagnostics setting", "error"); enableToggle.checked = !enableToggle.checked; } }); debugToggle?.addEventListener("change", async () => { try { uiLevel = debugToggle.checked ? "debug" : "normal"; await setDiagLevel({ enabled: uiEnabled, level: uiLevel }); appendUi("info", `Diagnostics level set to ${uiLevel}`); if (uiEnabled) attachClickTracker(); await refresh(); } catch (e) { toast?.(e.error || "Failed to save level", "error"); debugToggle.checked = uiLevel === "debug"; } }); refreshBtn?.addEventListener("click", refresh); clearBtn?.addEventListener("click", async () => { try { await clearDiagLog(); uiBuffer.length = 0; appendUi("info", "Cleared diagnostics"); await refresh(); } catch (e) { toast?.(e.error || "Failed to clear log", "error"); } }); copyBtn?.addEventListener("click", async () => { try { const text = logBox?.textContent || ""; if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text || "No log entries."); } else { const ta = document.createElement("textarea"); ta.value = text || "No log entries."; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } toast?.("Diagnostics copied", "success"); } catch (e) { toast?.("Copy failed", "error"); } }); downloadBtn?.addEventListener("click", () => { try { const blob = new Blob([logBox?.textContent || "No log entries."], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "pikit-diagnostics.txt"; a.click(); URL.revokeObjectURL(url); } catch (e) { toast?.("Download failed", "error"); } }); // initial load attachClickTracker(); await refresh(); return { logUi, refresh, }; }