Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/frontend_log.ts
2026-07-26 01:55:35 +02:00

172 lines
6.2 KiB
TypeScript

// file: kb-app-demo-desktop/frontend/ts/frontend_log.ts
// version: 2
//! 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<void> {
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<HTMLTextAreaElement>("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<HTMLTextAreaElement>("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 = "";
});
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);
}