Add diagnostics logging (RAM), UI viewer, and toggles
This commit is contained in:
@@ -117,3 +117,15 @@ export const removeService = ({ port }) =>
|
||||
method: "POST",
|
||||
body: JSON.stringify({ port }),
|
||||
});
|
||||
|
||||
// Diagnostics
|
||||
export const getDiagLog = () => api("/api/diag/log");
|
||||
export const setDiagLevel = ({ enabled, level }) =>
|
||||
api("/api/diag/log/level", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ enabled, level }),
|
||||
});
|
||||
export const clearDiagLog = () =>
|
||||
api("/api/diag/log/clear", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
167
pikit-web/assets/diaglog.js
Normal file
167
pikit-web/assets/diaglog.js
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { placeholderStatus, renderStats } from "./status.js";
|
||||
import { initServiceControls, renderServices } from "./services.js";
|
||||
import { initSettings } from "./settings.js";
|
||||
import { initUpdateSettings, isUpdatesDirty } from "./update-settings.js";
|
||||
import { initReleaseUI } from "./releases.js?v=20251213g";
|
||||
import { initReleaseUI } from "./releases.js?v=20251213h";
|
||||
import { initDiagUI, logUi } from "./diaglog.js?v=20251213h";
|
||||
|
||||
const servicesGrid = document.getElementById("servicesGrid");
|
||||
const heroStats = document.getElementById("heroStats");
|
||||
@@ -97,6 +98,14 @@ const changelogModal = document.getElementById("changelogModal");
|
||||
const changelogTitle = document.getElementById("changelogTitle");
|
||||
const changelogBody = document.getElementById("changelogBody");
|
||||
const changelogClose = document.getElementById("changelogClose");
|
||||
const diagEnableToggle = document.getElementById("diagEnableToggle");
|
||||
const diagDebugToggle = document.getElementById("diagDebugToggle");
|
||||
const diagRefreshBtn = document.getElementById("diagRefreshBtn");
|
||||
const diagClearBtn = document.getElementById("diagClearBtn");
|
||||
const diagCopyBtn = document.getElementById("diagCopyBtn");
|
||||
const diagDownloadBtn = document.getElementById("diagDownloadBtn");
|
||||
const diagLogBox = document.getElementById("diagLogBox");
|
||||
const diagStatus = document.getElementById("diagStatus");
|
||||
|
||||
const TOAST_POS_KEY = "pikit-toast-pos";
|
||||
const TOAST_ANIM_KEY = "pikit-toast-anim";
|
||||
@@ -495,6 +504,7 @@ function main() {
|
||||
showBusy,
|
||||
hideBusy,
|
||||
confirmAction,
|
||||
logUi,
|
||||
});
|
||||
loadToastSettings();
|
||||
|
||||
@@ -558,6 +568,23 @@ function main() {
|
||||
},
|
||||
});
|
||||
|
||||
// Diagnostics
|
||||
initDiagUI({
|
||||
elements: {
|
||||
enableToggle: diagEnableToggle,
|
||||
debugToggle: diagDebugToggle,
|
||||
refreshBtn: diagRefreshBtn,
|
||||
clearBtn: diagClearBtn,
|
||||
copyBtn: diagCopyBtn,
|
||||
downloadBtn: diagDownloadBtn,
|
||||
logBox: diagLogBox,
|
||||
statusEl: diagStatus,
|
||||
},
|
||||
toast: showToast,
|
||||
}).catch((e) => {
|
||||
console.error("Diag init failed", e);
|
||||
});
|
||||
|
||||
// Toast controls
|
||||
toastPosSelect?.addEventListener("change", () => {
|
||||
const val = toastPosSelect.value;
|
||||
|
||||
@@ -15,7 +15,7 @@ function shorten(text, max = 90) {
|
||||
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
||||
}
|
||||
|
||||
export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction }) {
|
||||
export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, logUi = () => {} }) {
|
||||
const releaseFlagTop = document.getElementById("releaseFlagTop");
|
||||
const releaseBtn = document.getElementById("releaseBtn");
|
||||
const releaseModal = document.getElementById("releaseModal");
|
||||
@@ -236,6 +236,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction })
|
||||
releaseCheckBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
logRelease("Checking for updates…");
|
||||
logUi("Update check requested");
|
||||
await checkRelease();
|
||||
await loadReleaseStatus(true);
|
||||
const state = window.__lastReleaseState || {};
|
||||
@@ -254,6 +255,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction })
|
||||
releaseApplyBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
lastReleaseToastKey = null;
|
||||
logUi("Update apply requested");
|
||||
const state = window.__lastReleaseState || {};
|
||||
const { current_version, latest_version } = state;
|
||||
const sameVersion =
|
||||
@@ -287,6 +289,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction })
|
||||
releaseRollbackBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
lastReleaseToastKey = null;
|
||||
logUi("Rollback requested");
|
||||
releaseBusyActive = true;
|
||||
showBusy("Rolling back…", "Restoring previous backup.");
|
||||
logRelease("Starting rollback…");
|
||||
|
||||
Reference in New Issue
Block a user