// file: kb-app-demo-desktop/frontend/ts/frontend_log.ts // version: 3 //! Frontend logging helpers that preserve explicit tracing targets. import "@andypf/json-viewer"; import { invoke } from "@tauri-apps/api/core"; import type { FrontendLogPayload } from "./bindings/kb_app_demo_desktop/frontend_log/FrontendLogPayload.ts"; type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error"; type ConsoleMethod = (...items: unknown[]) => void; const originalConsole = { trace: console.trace.bind(console), debug: console.debug.bind(console), log: console.log.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), }; function stringifyItem(item: unknown): string { if (item instanceof Error) { return item.stack ?? item.message; } if (typeof item === "string") { return item; } try { return JSON.stringify(item); } catch (caughtError) { return String(item); } } function formatMessage(items: unknown[]): string { return items.map(item => stringifyItem(item)).join(" "); } async function sendFrontendLog(level: FrontendLogLevel, target: string, message: string): Promise { const payload: FrontendLogPayload = { level, target, message, }; try { await invoke("emit_frontend_log", { payload }); } catch (caughtError) { originalConsole.error("frontend tracing bridge failed", caughtError); } } export function frontendTrace(target: string, ...items: unknown[]): void { const message = formatMessage(items); originalConsole.trace(message); void sendFrontendLog("trace", target, message); } export function frontendDebug(target: string, ...items: unknown[]): void { const message = formatMessage(items); originalConsole.debug(message); void sendFrontendLog("debug", target, message); } export function frontendInfo(target: string, ...items: unknown[]): void { const message = formatMessage(items); originalConsole.info(message); void sendFrontendLog("info", target, message); } export function frontendWarn(target: string, ...items: unknown[]): void { const message = formatMessage(items); originalConsole.warn(message); void sendFrontendLog("warn", target, message); } export function frontendError(target: string, ...items: unknown[]): void { const message = formatMessage(items); originalConsole.error(message); void sendFrontendLog("error", target, message); } function buildConsoleBridge(level: FrontendLogLevel, target: string, original: ConsoleMethod): ConsoleMethod { return (...items: unknown[]) => { original(...items); void sendFrontendLog(level, target, formatMessage(items)); }; } function upgradeJsonTextareas(): void { document.querySelectorAll("textarea[data-json-viewer]").forEach(textarea => { if (textarea.dataset.jsonViewerInstalled === "true") { return; } textarea.dataset.jsonViewerInstalled = "true"; const container = document.createElement("div"); container.className = "app-json-viewer"; const viewer = document.createElement("andypf-json-viewer"); viewer.setAttribute("indent", "2"); viewer.setAttribute("expanded", "1"); viewer.setAttribute("theme", "default-light"); viewer.setAttribute("show-data-types", "true"); viewer.setAttribute("show-toolbar", "false"); viewer.setAttribute("show-copy", "true"); const descriptor = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value"); let currentValue = textarea.value; const render = (value: string): void => { currentValue = value; let parsed: unknown = value; try { parsed = JSON.parse(value) as unknown; } catch { parsed = { message: value }; } viewer.setAttribute("data", JSON.stringify(parsed, null, 2)); }; Object.defineProperty(textarea, "value", { configurable: true, get: () => currentValue, set: (value: string) => { descriptor?.set?.call(textarea, value); render(value); }, }); textarea.classList.add("d-none"); textarea.insertAdjacentElement("afterend", container); container.appendChild(viewer); render(currentValue); }); } function upgradeLogOutputs(): void { document.querySelectorAll("textarea[id$='LogOutput'], textarea[data-log-output]").forEach(textarea => { if (textarea.dataset.logOutputInstalled === "true") { return; } textarea.dataset.logOutputInstalled = "true"; textarea.classList.add("app-log-output"); const toolbar = document.createElement("div"); toolbar.className = "d-flex justify-content-end gap-2 mb-2"; const copyButton = document.createElement("button"); copyButton.type = "button"; copyButton.className = "btn btn-sm btn-outline-secondary"; copyButton.textContent = "Copier"; copyButton.addEventListener("click", () => { void navigator.clipboard.writeText(textarea.value); }); const clearButton = document.createElement("button"); clearButton.type = "button"; clearButton.className = "btn btn-sm btn-outline-secondary"; clearButton.textContent = "Effacer"; clearButton.addEventListener("click", () => { textarea.value = ""; textarea.dispatchEvent(new CustomEvent("app-log-clear")); }); toolbar.append(copyButton, clearButton); textarea.insertAdjacentElement("beforebegin", toolbar); }); } export function installFrontendConsoleBridge(target: string): void { upgradeJsonTextareas(); upgradeLogOutputs(); console.trace = buildConsoleBridge("trace", target, originalConsole.trace); console.debug = buildConsoleBridge("debug", target, originalConsole.debug); console.log = buildConsoleBridge("info", target, originalConsole.log); console.info = buildConsoleBridge("info", target, originalConsole.info); console.warn = buildConsoleBridge("warn", target, originalConsole.warn); console.error = buildConsoleBridge("error", target, originalConsole.error); }