// file: crates/ksp-app-config-desk/frontend/ts/logging.ts // version: 6 //! Typed Logging editor backed by ConfigManagement persistence and KSP-owned runtime hot reload. import { Modal } from "bootstrap"; import type { LoggingDocumentCandidateDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentCandidateDto"; import type { LoggingDocumentDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentDto"; import type { LoggingDocumentSaveResultDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentSaveResultDto"; import type { LoggingFileDto } from "./bindings/ksp_app_config_desk/logging/LoggingFileDto"; import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto"; import type { LoggingTargetFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto"; import type { LoggingRuntimeStatusDto } from "./bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto"; import { frontendDebug, frontendTrace } from "./frontend_log"; import { invokeKsp } from "./invoke"; import { initializeLoggingTestPanel } from "./logging_test"; const LEVELS = ["off", "error", "warn", "info", "debug", "trace"] as const; const ROTATIONS = ["never", "hourly", "daily"] as const; const FORMATS = ["human", "compact", "pretty", "json"] as const; let draft: LoggingDocumentCandidateDto | null = null; let persistedDocument: LoggingDocumentDto | null = null; let selectedProfileId = ""; let dirty = false; function setLoggingStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void { const status = document.querySelector("#loggingStatus"); if (!status) { return; } status.className = `alert alert-${tone} mb-0`; status.textContent = message; frontendTrace("main", "Logging editor status replaced", { tone }); } function setText(selector: string, value: string): void { const element = document.querySelector(selector); if (element) { element.textContent = value; } } function inputValue(selector: string): string { return document.querySelector(selector)?.value ?? ""; } function selectedValue(selector: string): string { return document.querySelector(selector)?.value ?? ""; } function checkboxValue(selector: string): boolean { return document.querySelector(selector)?.checked ?? false; } function setInputValue(selector: string, value: string): void { const input = document.querySelector(selector); if (input) { input.value = value; } } function setSelectValue(selector: string, value: string): void { const select = document.querySelector(selector); if (select) { select.value = value; } } function setCheckboxValue(selector: string, value: boolean): void { const input = document.querySelector(selector); if (input) { input.checked = value; } } function selectorText(values: string[]): string { return values.join(", "); } function parseSelectors(value: string): string[] { const result: string[] = []; for (const part of value.split(",")) { const trimmed = part.trim(); if (trimmed.length > 0 && !result.includes(trimmed)) { result.push(trimmed); } } return result; } function cloneProfile(profile: LoggingProfileDto): LoggingProfileDto { return structuredClone(profile); } function selectedProfile(): LoggingProfileDto | null { if (!draft) { return null; } return draft.profiles.find(profile => profile.profileId === selectedProfileId) ?? null; } function markDirty(reason: string): void { dirty = true; const saveButton = document.querySelector("#saveLoggingDocument"); if (saveButton) { saveButton.disabled = false; } setLoggingStatus("Brouillon modifié. Sauvegarde nécessaire pour persister std.logging.json.", "warning"); updateRuntimeActionState(); frontendTrace("main", "Logging draft changed", { reason, selectedProfileId }); } function setEditorEnabled(enabled: boolean): void { const selectors = [ "#loggingLogsDirectoryInput", "#loggingDefaultProfileSelect", "#loggingProfileSelect", "#loggingProfileIdInput", "#createLoggingProfile", "#cloneLoggingProfile", "#renameLoggingProfile", "#deleteLoggingProfile", "#loggingProfileDefaultFilter", "#loggingProfileSpanEvents", "#loggingConsoleEnabled", "#loggingConsoleAnsi", "#loggingConsoleOutput", "#loggingConsoleFormat", "#loggingConsoleLevel", "#loggingConsoleTargets", "#loggingConsoleDomains", "#addLoggingFile", "#addLoggingTargetFilter", ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { element.disabled = !enabled; } } for (const element of document.querySelectorAll( "#loggingFilesTable input, #loggingFilesTable select, #loggingFilesTable button, #loggingTargetFiltersTable input, #loggingTargetFiltersTable select, #loggingTargetFiltersTable button", )) { element.disabled = !enabled; } const save = document.querySelector("#saveLoggingDocument"); if (save) { save.disabled = !enabled || !dirty; } } function renderProfileSelects(): void { const profileSelect = document.querySelector("#loggingProfileSelect"); const defaultSelect = document.querySelector("#loggingDefaultProfileSelect"); if (!draft || !profileSelect || !defaultSelect) { return; } profileSelect.replaceChildren(); defaultSelect.replaceChildren(); for (const profile of draft.profiles) { const profileOption = document.createElement("option"); profileOption.value = profile.profileId; profileOption.textContent = profile.profileId === draft.defaultProfile ? `${profile.profileId} (default)` : profile.profileId; profileSelect.append(profileOption); const defaultOption = document.createElement("option"); defaultOption.value = profile.profileId; defaultOption.textContent = profile.profileId; defaultSelect.append(defaultOption); } if (!draft.profiles.some(profile => profile.profileId === selectedProfileId)) { selectedProfileId = draft.profiles[0]?.profileId ?? ""; } profileSelect.value = selectedProfileId; defaultSelect.value = draft.defaultProfile; frontendTrace("main", "Logging profile selectors replaced", { profileCount: draft.profiles.length }); } function createSelect(values: readonly string[], value: string, onChange: (next: string) => void): HTMLSelectElement { const select = document.createElement("select"); select.className = "form-select form-select-sm"; for (const item of values) { const option = document.createElement("option"); option.value = item; option.textContent = item; select.append(option); } select.value = value; select.addEventListener("change", () => onChange(select.value)); return select; } function createTextInput(value: string, onInput: (next: string) => void, monospace = false): HTMLInputElement { const input = document.createElement("input"); input.type = "text"; input.autocomplete = "off"; input.className = `form-control form-control-sm${monospace ? " font-monospace" : ""}`; input.value = value; input.addEventListener("input", () => onInput(input.value)); return input; } function createCheckbox(value: boolean, onChange: (next: boolean) => void): HTMLInputElement { const input = document.createElement("input"); input.type = "checkbox"; input.className = "form-check-input"; input.checked = value; input.addEventListener("change", () => onChange(input.checked)); return input; } function appendControlCell(row: HTMLTableRowElement, control: HTMLElement): void { const cell = document.createElement("td"); cell.append(control); row.append(cell); } function renderFiles(profile: LoggingProfileDto): void { const body = document.querySelector("#loggingFilesTable tbody"); if (!body) { return; } body.replaceChildren(); profile.files.forEach((file, index) => { const row = document.createElement("tr"); appendControlCell(row, createTextInput(file.outputId, value => { file.outputId = value; markDirty("file.output_id"); }, true)); appendControlCell(row, createCheckbox(file.enabled, value => { file.enabled = value; markDirty("file.enabled"); })); appendControlCell(row, createTextInput(file.path, value => { file.path = value; markDirty("file.path"); }, true)); appendControlCell(row, createSelect(ROTATIONS, file.rotation, value => { file.rotation = value; markDirty("file.rotation"); })); appendControlCell(row, createSelect(FORMATS, file.format, value => { file.format = value; markDirty("file.format"); })); appendControlCell(row, createSelect(LEVELS, file.filter.level, value => { file.filter.level = value; markDirty("file.filter.level"); })); appendControlCell(row, createTextInput(selectorText(file.filter.targets), value => { file.filter.targets = parseSelectors(value); markDirty("file.filter.targets"); }, true)); appendControlCell(row, createTextInput(selectorText(file.filter.domains), value => { file.filter.domains = parseSelectors(value); markDirty("file.filter.domains"); }, true)); const remove = document.createElement("button"); remove.type = "button"; remove.className = "btn btn-outline-danger btn-sm"; remove.textContent = "Retirer"; remove.addEventListener("click", () => { profile.files.splice(index, 1); markDirty("file.remove"); renderFiles(profile); frontendDebug("main", "Logging file sink removed from draft", { profileId: profile.profileId, fileIndex: index }); }); appendControlCell(row, remove); body.append(row); }); frontendTrace("main", "Logging file sink editor DOM replaced", { fileCount: profile.files.length }); } function renderTargetFilters(profile: LoggingProfileDto): void { const body = document.querySelector("#loggingTargetFiltersTable tbody"); if (!body) { return; } body.replaceChildren(); profile.targetFilters.forEach((filter, index) => { const row = document.createElement("tr"); appendControlCell(row, createTextInput(filter.targetPrefix, value => { filter.targetPrefix = value; markDirty("target_filter.prefix"); }, true)); appendControlCell(row, createSelect(LEVELS, filter.level, value => { filter.level = value; markDirty("target_filter.level"); })); const remove = document.createElement("button"); remove.type = "button"; remove.className = "btn btn-outline-danger btn-sm"; remove.textContent = "Retirer"; remove.addEventListener("click", () => { profile.targetFilters.splice(index, 1); markDirty("target_filter.remove"); renderTargetFilters(profile); frontendDebug("main", "Logging target filter removed from draft", { profileId: profile.profileId, filterIndex: index }); }); appendControlCell(row, remove); body.append(row); }); frontendTrace("main", "Logging target filter editor DOM replaced", { targetFilterCount: profile.targetFilters.length }); } function renderSelectedProfile(source: "load" | "selection" | "mutation"): void { const profile = selectedProfile(); if (!profile) { setEditorEnabled(false); return; } setSelectValue("#loggingProfileDefaultFilter", profile.defaultFilter); setSelectValue("#loggingProfileSpanEvents", profile.spanEvents); setCheckboxValue("#loggingConsoleEnabled", profile.console.enabled); setCheckboxValue("#loggingConsoleAnsi", profile.console.ansi); setSelectValue("#loggingConsoleOutput", profile.console.output); setSelectValue("#loggingConsoleFormat", profile.console.format); setSelectValue("#loggingConsoleLevel", profile.console.filter.level); setInputValue("#loggingConsoleTargets", selectorText(profile.console.filter.targets)); setInputValue("#loggingConsoleDomains", selectorText(profile.console.filter.domains)); renderFiles(profile); renderTargetFilters(profile); setEditorEnabled(true); frontendTrace("main", "Logging profile editor rendered", { profileId: profile.profileId, source, fileCount: profile.files.length, targetFilterCount: profile.targetFilters.length, }); } function updateRuntimeActionState(): void { const select = document.querySelector("#loggingRuntimeProfileSelect"); const apply = document.querySelector("#applyLoggingRuntimeProfile"); if (select) { select.disabled = persistedDocument === null || dirty; } if (apply) { apply.disabled = persistedDocument === null || dirty || !select || select.value.length === 0; } } function renderRuntimeProfileSelect(): void { const select = document.querySelector("#loggingRuntimeProfileSelect"); if (!select || !persistedDocument) { updateRuntimeActionState(); return; } const previous = select.value; select.replaceChildren(); for (const profile of persistedDocument.profiles) { const option = document.createElement("option"); option.value = profile.profileId; option.textContent = profile.profileId === persistedDocument.defaultProfile ? `${profile.profileId} (default)` : profile.profileId; select.append(option); } if (persistedDocument.profiles.some(profile => profile.profileId === previous)) { select.value = previous; } else { select.value = persistedDocument.defaultProfile; } updateRuntimeActionState(); } function renderRuntimeFiles(status: LoggingRuntimeStatusDto): void { const body = document.querySelector("#loggingRuntimeFilesTable tbody"); if (!body) { return; } body.replaceChildren(); for (const file of status.files) { const row = document.createElement("tr"); const values: Array<{ value: string; monospace: boolean }> = [ { value: file.outputId, monospace: true }, { value: file.directory, monospace: true }, { value: file.fileNamePrefix, monospace: true }, { value: file.rotation, monospace: false }, ]; for (const item of values) { const cell = document.createElement("td"); cell.textContent = item.value; if (item.monospace) { cell.classList.add("font-monospace"); } row.append(cell); } body.append(row); } } function renderRuntimeStatus(status: LoggingRuntimeStatusDto): void { setText("#loggingRuntimeActiveProfile", status.activeProfile ?? "fallback transitoire"); setText("#loggingRuntimeSelectionSource", status.selectionSource); setText("#loggingRuntimeGeneration", status.generation.toString()); setText("#loggingRuntimeFallback", status.fallbackActive ? "oui" : "non"); setText("#loggingRuntimeApplicationId", status.applicationId); setText("#loggingRuntimeLaunchTimestamp", status.launchTimestamp); setText("#loggingRuntimeConsole", status.consoleEnabled ? "active" : "désactivée"); setText("#loggingRuntimeDropped", `${status.droppedTotalLines} total · console=${status.droppedConsoleLines} · fichiers=${status.droppedFileLines}`); renderRuntimeFiles(status); const select = document.querySelector("#loggingRuntimeProfileSelect"); if (select && status.activeProfile && persistedDocument?.profiles.some(profile => profile.profileId === status.activeProfile)) { select.value = status.activeProfile; } updateRuntimeActionState(); frontendTrace("main", "Logging runtime metadata rendered", { generation: status.generation, selectionSource: status.selectionSource, fileCount: status.files.length, consoleEnabled: status.consoleEnabled, }); } async function refreshRuntimeStatus(source: "startup" | "save" | "explicit" | "user"): Promise { if (source === "user") { frontendDebug("main", "Logging runtime metadata refresh requested"); } try { const status = await invokeKsp("main", "get_logging_runtime_status"); renderRuntimeStatus(status); frontendDebug("main", "Logging runtime metadata refresh completed", { generation: status.generation, activeProfile: status.activeProfile }); } catch { setText("#loggingRuntimeActiveProfile", "indisponible"); } } async function applyRuntimeProfile(): Promise { if (dirty) { setLoggingStatus("Sauvegarde ou abandonne d'abord le brouillon avant d'appliquer un profil runtime explicite.", "warning"); return; } const profileId = selectedValue("#loggingRuntimeProfileSelect"); if (profileId.length === 0) { return; } const apply = document.querySelector("#applyLoggingRuntimeProfile"); if (apply) { apply.disabled = true; } setLoggingStatus(`Application explicite du profil runtime ${profileId}...`); frontendDebug("main", "Explicit Logging runtime profile apply requested", { profileId }); try { const status = await invokeKsp("main", "apply_logging_profile", { profileId }); renderRuntimeStatus(status); window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated")); setLoggingStatus(`Profil ${profileId} appliqué au runtime sans modifier default_profile. Génération ${status.generation}.`, "success"); frontendDebug("main", "Explicit Logging runtime profile apply completed", { profileId, generation: status.generation }); } catch { updateRuntimeActionState(); setLoggingStatus("Application explicite refusée. Le runtime précédent et sa génération sont conservés.", "danger"); } } function renderDocument(documentDto: LoggingDocumentDto): void { persistedDocument = structuredClone(documentDto); draft = { logsDirectory: documentDto.logsDirectory, defaultProfile: documentDto.defaultProfile, profiles: documentDto.profiles.map(profile => cloneProfile(profile)), }; dirty = false; selectedProfileId = draft.profiles.some(profile => profile.profileId === selectedProfileId) ? selectedProfileId : draft.defaultProfile; if (!draft.profiles.some(profile => profile.profileId === selectedProfileId)) { selectedProfileId = draft.profiles[0]?.profileId ?? ""; } setText("#loggingFileId", documentDto.fileId); setText("#loggingPath", documentDto.path); setText("#loggingFormatVersion", documentDto.formatVersion.toString()); setInputValue("#loggingLogsDirectoryInput", draft.logsDirectory); renderProfileSelects(); renderRuntimeProfileSelect(); renderSelectedProfile("load"); setText("#loggingSaveReport", "Aucune sauvegarde effectuée depuis le dernier chargement."); frontendTrace("main", "Logging document editor rendered", { profileCount: documentDto.profiles.length }); } function defaultProfile(profileId: string): LoggingProfileDto { return { profileId, defaultFilter: "warn", spanEvents: "new_and_close", console: { enabled: true, output: "stderr", ansi: true, format: "compact", filter: { level: "info", targets: ["*"], domains: ["*"] }, }, files: [], targetFilters: [], }; } function targetProfileId(): string { return inputValue("#loggingProfileIdInput").trim(); } function profileIdAvailable(profileId: string, exceptProfileId?: string): boolean { if (!draft || profileId.length === 0) { return false; } return !draft.profiles.some(profile => profile.profileId === profileId && profile.profileId !== exceptProfileId); } function createProfile(): void { if (!draft) { return; } const profileId = targetProfileId(); if (!profileIdAvailable(profileId)) { setLoggingStatus("L'ID cible est vide ou existe déjà dans le brouillon.", "danger"); return; } draft.profiles.push(defaultProfile(profileId)); selectedProfileId = profileId; setInputValue("#loggingProfileIdInput", ""); markDirty("profile.create"); renderProfileSelects(); renderSelectedProfile("mutation"); frontendDebug("main", "Logging profile created in draft", { profileId }); } function cloneSelectedProfile(): void { if (!draft) { return; } const source = selectedProfile(); const profileId = targetProfileId(); if (!source || !profileIdAvailable(profileId)) { setLoggingStatus("Sélectionne un profil et fournis un ID cible libre pour le clone.", "danger"); return; } const clone = cloneProfile(source); clone.profileId = profileId; draft.profiles.push(clone); selectedProfileId = profileId; setInputValue("#loggingProfileIdInput", ""); markDirty("profile.clone"); renderProfileSelects(); renderSelectedProfile("mutation"); frontendDebug("main", "Logging profile cloned in draft", { sourceProfileId: source.profileId, profileId }); } function renameSelectedProfile(): void { if (!draft) { return; } const profile = selectedProfile(); const profileId = targetProfileId(); if (!profile || !profileIdAvailable(profileId, profile.profileId)) { setLoggingStatus("Fournis un nouvel ID de profil libre avant de renommer.", "danger"); return; } const previousId = profile.profileId; profile.profileId = profileId; if (draft.defaultProfile === previousId) { draft.defaultProfile = profileId; } selectedProfileId = profileId; setInputValue("#loggingProfileIdInput", ""); markDirty("profile.rename"); renderProfileSelects(); renderSelectedProfile("mutation"); frontendDebug("main", "Logging profile renamed in draft", { previousId, profileId }); } function confirmDeleteSelectedProfile(): void { if (!draft || draft.profiles.length <= 1) { setLoggingStatus("Le document Logging doit conserver au moins un profil.", "danger"); return; } const profile = selectedProfile(); const modalElement = document.querySelector("#loggingProfileDeleteModal"); if (!profile || !modalElement) { return; } setText("#loggingProfileDeleteName", profile.profileId); modalElement.dataset.profileId = profile.profileId; frontendDebug("main", "Logging profile draft deletion confirmation opened", { profileId: profile.profileId }); Modal.getOrCreateInstance(modalElement).show(); } function deleteConfirmedProfile(): void { if (!draft) { return; } const modalElement = document.querySelector("#loggingProfileDeleteModal"); const profileId = modalElement?.dataset.profileId ?? ""; const index = draft.profiles.findIndex(profile => profile.profileId === profileId); if (index < 0 || draft.profiles.length <= 1) { return; } draft.profiles.splice(index, 1); if (draft.defaultProfile === profileId) { draft.defaultProfile = draft.profiles[0].profileId; } selectedProfileId = draft.profiles[Math.min(index, draft.profiles.length - 1)].profileId; markDirty("profile.delete"); renderProfileSelects(); renderSelectedProfile("mutation"); if (modalElement) { Modal.getOrCreateInstance(modalElement).hide(); delete modalElement.dataset.profileId; } frontendDebug("main", "Logging profile removed from draft", { profileId }); } function addFile(): void { const profile = selectedProfile(); if (!profile) { return; } const file: LoggingFileDto = { outputId: `file.new.${profile.files.length + 1}`, enabled: true, path: `new/output-${profile.files.length + 1}.log`, rotation: "daily", format: "human", ansi: false, filter: { level: "info", targets: ["*"], domains: ["*"] }, }; profile.files.push(file); markDirty("file.add"); renderFiles(profile); frontendDebug("main", "Logging file sink added to draft", { profileId: profile.profileId, fileCount: profile.files.length }); } function addTargetFilter(): void { const profile = selectedProfile(); if (!profile) { return; } const filter: LoggingTargetFilterDto = { targetPrefix: "ksp-", level: "info" }; profile.targetFilters.push(filter); markDirty("target_filter.add"); renderTargetFilters(profile); frontendDebug("main", "Logging target filter added to draft", { profileId: profile.profileId, targetFilterCount: profile.targetFilters.length }); } function syncStaticProfileFields(): void { const profile = selectedProfile(); if (!profile) { return; } profile.defaultFilter = selectedValue("#loggingProfileDefaultFilter"); profile.spanEvents = selectedValue("#loggingProfileSpanEvents"); profile.console.enabled = checkboxValue("#loggingConsoleEnabled"); profile.console.ansi = checkboxValue("#loggingConsoleAnsi"); profile.console.output = selectedValue("#loggingConsoleOutput"); profile.console.format = selectedValue("#loggingConsoleFormat"); profile.console.filter.level = selectedValue("#loggingConsoleLevel"); profile.console.filter.targets = parseSelectors(inputValue("#loggingConsoleTargets")); profile.console.filter.domains = parseSelectors(inputValue("#loggingConsoleDomains")); } async function saveLoggingDocument(): Promise { if (!draft) { return; } syncStaticProfileFields(); draft.logsDirectory = inputValue("#loggingLogsDirectoryInput"); draft.defaultProfile = selectedValue("#loggingDefaultProfileSelect"); setEditorEnabled(false); setLoggingStatus("Validation, sauvegarde atomique et application hot reload du Logging..."); frontendDebug("main", "Typed Logging document persistence and runtime apply requested", { profileCount: draft.profiles.length, defaultProfile: draft.defaultProfile, fileCount: draft.profiles.reduce((count, profile) => count + profile.files.length, 0), }); try { const result = await invokeKsp("main", "save_logging_document", { candidate: draft }); renderDocument(result.document); await refreshRuntimeStatus("save"); window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated")); setText( "#loggingSaveReport", `source_changed=${result.sourceChanged} · reload_required=${result.reloadRequired} · runtime_applied=${result.runtimeApplied} · generation=${result.loggingGeneration} · active_profile=${result.activeProfile}`, ); setLoggingStatus( `Document Logging validé, persisté et appliqué sans redémarrage. Runtime génération ${result.loggingGeneration}, profil ${result.activeProfile}.`, "success", ); frontendDebug("main", "Typed Logging document persistence and runtime apply completed", { sourceChanged: result.sourceChanged, reloadRequired: result.reloadRequired, runtimeApplied: result.runtimeApplied, loggingGeneration: result.loggingGeneration, activeProfile: result.activeProfile, profileCount: result.document.profiles.length, }); } catch { setEditorEnabled(true); setLoggingStatus( "Sauvegarde/application Logging refusée. Le runtime précédent est conservé ; si la persistence avait commencé, la transaction restaure la source précédente ou remonte une erreur de rollback.", "danger", ); } } function requestReload(): void { if (!dirty) { void refreshLoggingDocument("user"); return; } const modalElement = document.querySelector("#loggingDiscardDraftModal"); if (!modalElement) { return; } frontendDebug("main", "Logging dirty draft discard confirmation opened"); Modal.getOrCreateInstance(modalElement).show(); } function confirmReload(): void { const modalElement = document.querySelector("#loggingDiscardDraftModal"); if (modalElement) { Modal.getOrCreateInstance(modalElement).hide(); } frontendDebug("main", "Logging dirty draft discard confirmed"); void refreshLoggingDocument("user"); } async function refreshLoggingDocument(source: "startup" | "user"): Promise { if (source === "user") { frontendDebug("main", "Logging document reload requested", { dirty }); } setEditorEnabled(false); setLoggingStatus("Chargement du document Logging typé..."); try { const documentDto = await invokeKsp("main", "get_logging_document"); renderDocument(documentDto); setLoggingStatus( `${documentDto.profiles.length} profil(s) Logging chargé(s). Le brouillon est synchronisé avec la source persistée ; cette action ne modifie pas le runtime actif.`, "success", ); frontendDebug("main", "Logging document reload completed", { profileCount: documentDto.profiles.length, defaultProfile: documentDto.defaultProfile, }); await refreshRuntimeStatus(source === "startup" ? "startup" : "user"); } catch { draft = null; persistedDocument = null; setEditorEnabled(false); updateRuntimeActionState(); setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger"); } } function bindStaticDraftFields(): void { document.querySelector("#loggingLogsDirectoryInput")?.addEventListener("input", () => { if (draft) { draft.logsDirectory = inputValue("#loggingLogsDirectoryInput"); markDirty("logs_directory"); } }); document.querySelector("#loggingDefaultProfileSelect")?.addEventListener("change", () => { if (draft) { draft.defaultProfile = selectedValue("#loggingDefaultProfileSelect"); markDirty("default_profile"); renderProfileSelects(); } }); const profileSelectors = ["#loggingProfileDefaultFilter", "#loggingProfileSpanEvents", "#loggingConsoleOutput", "#loggingConsoleFormat", "#loggingConsoleLevel"]; for (const selector of profileSelectors) { document.querySelector(selector)?.addEventListener("change", () => { syncStaticProfileFields(); markDirty(selector); }); } for (const selector of ["#loggingConsoleEnabled", "#loggingConsoleAnsi"]) { document.querySelector(selector)?.addEventListener("change", () => { syncStaticProfileFields(); markDirty(selector); }); } for (const selector of ["#loggingConsoleTargets", "#loggingConsoleDomains"]) { document.querySelector(selector)?.addEventListener("input", () => { syncStaticProfileFields(); markDirty(selector); }); } } /// Initializes the typed Logging editor surface. export function initializeLoggingPanel(): void { bindStaticDraftFields(); document.querySelector("#refreshLoggingDocument")?.addEventListener("click", requestReload); document.querySelector("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument()); document.querySelector("#refreshLoggingRuntime")?.addEventListener("click", () => void refreshRuntimeStatus("user")); document.querySelector("#applyLoggingRuntimeProfile")?.addEventListener("click", () => void applyRuntimeProfile()); document.querySelector("#loggingRuntimeProfileSelect")?.addEventListener("change", updateRuntimeActionState); document.querySelector("#loggingProfileSelect")?.addEventListener("change", event => { syncStaticProfileFields(); selectedProfileId = (event.currentTarget as HTMLSelectElement).value; frontendDebug("main", "Logging profile selection changed", { profileId: selectedProfileId }); renderSelectedProfile("selection"); }); document.querySelector("#createLoggingProfile")?.addEventListener("click", createProfile); document.querySelector("#cloneLoggingProfile")?.addEventListener("click", cloneSelectedProfile); document.querySelector("#renameLoggingProfile")?.addEventListener("click", renameSelectedProfile); document.querySelector("#deleteLoggingProfile")?.addEventListener("click", confirmDeleteSelectedProfile); document.querySelector("#loggingProfileDeleteConfirm")?.addEventListener("click", deleteConfirmedProfile); document.querySelector("#loggingDiscardDraftConfirm")?.addEventListener("click", confirmReload); document.querySelector("#addLoggingFile")?.addEventListener("click", addFile); document.querySelector("#addLoggingTargetFilter")?.addEventListener("click", addTargetFilter); initializeLoggingTestPanel(); frontendTrace("main", "Logging editor mutation handlers installed"); void refreshLoggingDocument("startup"); }