223 lines
6.3 KiB
JavaScript
223 lines
6.3 KiB
JavaScript
// 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;
|
|
let loading = 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,
|
|
logButton,
|
|
modal,
|
|
modalClose,
|
|
} = elements;
|
|
|
|
const setBusy = (on) => {
|
|
loading = on;
|
|
[refreshBtn, clearBtn, copyBtn, downloadBtn, enableToggle, debugToggle].forEach((el) => {
|
|
if (el) el.disabled = !!on;
|
|
});
|
|
};
|
|
|
|
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";
|
|
if (logButton) logButton.classList.toggle("hidden", !uiEnabled);
|
|
if (modal && !uiEnabled) modal.classList.add("hidden");
|
|
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() {
|
|
if (loading) return;
|
|
setBusy(true);
|
|
try {
|
|
const entries = await syncState();
|
|
render(entries);
|
|
toast?.("Diagnostics refreshed", "success");
|
|
} catch (e) {
|
|
toast?.(e.error || "Failed to load diagnostics", "error");
|
|
// retry once if failed
|
|
try {
|
|
const entries = await syncState();
|
|
render(entries);
|
|
toast?.("Diagnostics refreshed (after retry)", "success");
|
|
} catch (err2) {
|
|
toast?.(err2.error || "Diagnostics still failing", "error");
|
|
}
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
enableToggle?.addEventListener("change", async () => {
|
|
try {
|
|
setBusy(true);
|
|
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;
|
|
setBusy(false);
|
|
return;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
if (logButton) logButton.classList.toggle("hidden", !uiEnabled);
|
|
if (!uiEnabled && modal) modal.classList.add("hidden");
|
|
});
|
|
|
|
debugToggle?.addEventListener("change", async () => {
|
|
try {
|
|
setBusy(true);
|
|
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";
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
});
|
|
|
|
refreshBtn?.addEventListener("click", refresh);
|
|
|
|
clearBtn?.addEventListener("click", async () => {
|
|
try {
|
|
setBusy(true);
|
|
await clearDiagLog();
|
|
uiBuffer.length = 0;
|
|
appendUi("info", "Cleared diagnostics");
|
|
await refresh();
|
|
} catch (e) {
|
|
toast?.(e.error || "Failed to clear log", "error");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
});
|
|
|
|
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();
|
|
|
|
logButton?.addEventListener("click", () => {
|
|
if (!uiEnabled) return;
|
|
modal?.classList.remove("hidden");
|
|
});
|
|
modalClose?.addEventListener("click", () => modal?.classList.add("hidden"));
|
|
modal?.addEventListener("click", (e) => {
|
|
if (e.target === modal) e.stopPropagation(); // prevent accidental close
|
|
});
|
|
|
|
return {
|
|
logUi,
|
|
refresh,
|
|
};
|
|
}
|