780 lines
34 KiB
TypeScript
780 lines
34 KiB
TypeScript
// 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<HTMLElement>("#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<HTMLElement>(selector);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
}
|
|
|
|
function inputValue(selector: string): string {
|
|
return document.querySelector<HTMLInputElement>(selector)?.value ?? "";
|
|
}
|
|
|
|
function selectedValue(selector: string): string {
|
|
return document.querySelector<HTMLSelectElement>(selector)?.value ?? "";
|
|
}
|
|
|
|
function checkboxValue(selector: string): boolean {
|
|
return document.querySelector<HTMLInputElement>(selector)?.checked ?? false;
|
|
}
|
|
|
|
function setInputValue(selector: string, value: string): void {
|
|
const input = document.querySelector<HTMLInputElement>(selector);
|
|
if (input) {
|
|
input.value = value;
|
|
}
|
|
}
|
|
|
|
function setSelectValue(selector: string, value: string): void {
|
|
const select = document.querySelector<HTMLSelectElement>(selector);
|
|
if (select) {
|
|
select.value = value;
|
|
}
|
|
}
|
|
|
|
function setCheckboxValue(selector: string, value: boolean): void {
|
|
const input = document.querySelector<HTMLInputElement>(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<HTMLButtonElement>("#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<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(selector);
|
|
if (element) {
|
|
element.disabled = !enabled;
|
|
}
|
|
}
|
|
for (const element of document.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
|
|
"#loggingFilesTable input, #loggingFilesTable select, #loggingFilesTable button, #loggingTargetFiltersTable input, #loggingTargetFiltersTable select, #loggingTargetFiltersTable button",
|
|
)) {
|
|
element.disabled = !enabled;
|
|
}
|
|
const save = document.querySelector<HTMLButtonElement>("#saveLoggingDocument");
|
|
if (save) {
|
|
save.disabled = !enabled || !dirty;
|
|
}
|
|
}
|
|
|
|
function renderProfileSelects(): void {
|
|
const profileSelect = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
|
|
const defaultSelect = document.querySelector<HTMLSelectElement>("#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<HTMLTableSectionElement>("#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<HTMLTableSectionElement>("#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<HTMLSelectElement>("#loggingRuntimeProfileSelect");
|
|
const apply = document.querySelector<HTMLButtonElement>("#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<HTMLSelectElement>("#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<HTMLTableSectionElement>("#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<HTMLSelectElement>("#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<void> {
|
|
if (source === "user") {
|
|
frontendDebug("main", "Logging runtime metadata refresh requested");
|
|
}
|
|
try {
|
|
const status = await invokeKsp<LoggingRuntimeStatusDto>("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<void> {
|
|
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<HTMLButtonElement>("#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<LoggingRuntimeStatusDto>("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<HTMLElement>("#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<HTMLElement>("#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<void> {
|
|
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<LoggingDocumentSaveResultDto>("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<HTMLElement>("#loggingDiscardDraftModal");
|
|
if (!modalElement) {
|
|
return;
|
|
}
|
|
frontendDebug("main", "Logging dirty draft discard confirmation opened");
|
|
Modal.getOrCreateInstance(modalElement).show();
|
|
}
|
|
|
|
function confirmReload(): void {
|
|
const modalElement = document.querySelector<HTMLElement>("#loggingDiscardDraftModal");
|
|
if (modalElement) {
|
|
Modal.getOrCreateInstance(modalElement).hide();
|
|
}
|
|
frontendDebug("main", "Logging dirty draft discard confirmed");
|
|
void refreshLoggingDocument("user");
|
|
}
|
|
|
|
async function refreshLoggingDocument(source: "startup" | "user"): Promise<void> {
|
|
if (source === "user") {
|
|
frontendDebug("main", "Logging document reload requested", { dirty });
|
|
}
|
|
setEditorEnabled(false);
|
|
setLoggingStatus("Chargement du document Logging typé...");
|
|
try {
|
|
const documentDto = await invokeKsp<LoggingDocumentDto>("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<HTMLInputElement>("#loggingLogsDirectoryInput")?.addEventListener("input", () => {
|
|
if (draft) {
|
|
draft.logsDirectory = inputValue("#loggingLogsDirectoryInput");
|
|
markDirty("logs_directory");
|
|
}
|
|
});
|
|
document.querySelector<HTMLSelectElement>("#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<HTMLSelectElement>(selector)?.addEventListener("change", () => {
|
|
syncStaticProfileFields();
|
|
markDirty(selector);
|
|
});
|
|
}
|
|
for (const selector of ["#loggingConsoleEnabled", "#loggingConsoleAnsi"]) {
|
|
document.querySelector<HTMLInputElement>(selector)?.addEventListener("change", () => {
|
|
syncStaticProfileFields();
|
|
markDirty(selector);
|
|
});
|
|
}
|
|
for (const selector of ["#loggingConsoleTargets", "#loggingConsoleDomains"]) {
|
|
document.querySelector<HTMLInputElement>(selector)?.addEventListener("input", () => {
|
|
syncStaticProfileFields();
|
|
markDirty(selector);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Initializes the typed Logging editor surface.
|
|
export function initializeLoggingPanel(): void {
|
|
bindStaticDraftFields();
|
|
document.querySelector<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", requestReload);
|
|
document.querySelector<HTMLButtonElement>("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument());
|
|
document.querySelector<HTMLButtonElement>("#refreshLoggingRuntime")?.addEventListener("click", () => void refreshRuntimeStatus("user"));
|
|
document.querySelector<HTMLButtonElement>("#applyLoggingRuntimeProfile")?.addEventListener("click", () => void applyRuntimeProfile());
|
|
document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect")?.addEventListener("change", updateRuntimeActionState);
|
|
document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
|
|
syncStaticProfileFields();
|
|
selectedProfileId = (event.currentTarget as HTMLSelectElement).value;
|
|
frontendDebug("main", "Logging profile selection changed", { profileId: selectedProfileId });
|
|
renderSelectedProfile("selection");
|
|
});
|
|
document.querySelector<HTMLButtonElement>("#createLoggingProfile")?.addEventListener("click", createProfile);
|
|
document.querySelector<HTMLButtonElement>("#cloneLoggingProfile")?.addEventListener("click", cloneSelectedProfile);
|
|
document.querySelector<HTMLButtonElement>("#renameLoggingProfile")?.addEventListener("click", renameSelectedProfile);
|
|
document.querySelector<HTMLButtonElement>("#deleteLoggingProfile")?.addEventListener("click", confirmDeleteSelectedProfile);
|
|
document.querySelector<HTMLButtonElement>("#loggingProfileDeleteConfirm")?.addEventListener("click", deleteConfirmedProfile);
|
|
document.querySelector<HTMLButtonElement>("#loggingDiscardDraftConfirm")?.addEventListener("click", confirmReload);
|
|
document.querySelector<HTMLButtonElement>("#addLoggingFile")?.addEventListener("click", addFile);
|
|
document.querySelector<HTMLButtonElement>("#addLoggingTargetFilter")?.addEventListener("click", addTargetFilter);
|
|
initializeLoggingTestPanel();
|
|
frontendTrace("main", "Logging editor mutation handlers installed");
|
|
void refreshLoggingDocument("startup");
|
|
}
|