v0.1.4-pre.015
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -300,110 +300,144 @@
|
||||
<section id="loggingPanel" hidden aria-label="Logging Config">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">Logging editor — lecture typée</h2>
|
||||
<p class="text-body-secondary small mb-0">Projection read-only de std.logging.json via les types publics de ksp-config-lib ; aucune mutation runtime dans cette tranche.</p>
|
||||
<h2 class="h5 mb-1">Logging editor — mutations/persistence</h2>
|
||||
<p class="text-body-secondary small mb-0">Brouillon typé de <code>std.logging.json</code>, validé et persisté atomiquement par <code>ksp-config-lib</code>. Le runtime Logging n'est pas rechargé dans cette tranche.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="refreshLoggingDocument" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
|
||||
Recharger
|
||||
</button>
|
||||
<button id="saveLoggingDocument" class="btn btn-primary btn-sm" type="button" disabled>
|
||||
<i class="fa-solid fa-floppy-disk me-1" aria-hidden="true"></i>
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
<button id="refreshLoggingDocument" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<dl class="row small mb-4 app-profile-metadata">
|
||||
<dl class="row small mb-3 app-profile-metadata">
|
||||
<dt class="col-lg-3">file_id</dt>
|
||||
<dd id="loggingFileId" class="col-lg-9 font-monospace">—</dd>
|
||||
<dt class="col-lg-3">Path résolu</dt>
|
||||
<dd id="loggingPath" class="col-lg-9 font-monospace text-break">—</dd>
|
||||
<dt class="col-lg-3">format_version</dt>
|
||||
<dd id="loggingFormatVersion" class="col-lg-9 font-monospace">—</dd>
|
||||
<dt class="col-lg-3">logs_directory</dt>
|
||||
<dd id="loggingLogsDirectory" class="col-lg-9 font-monospace text-break">—</dd>
|
||||
<dt class="col-lg-3 mb-0">default_profile</dt>
|
||||
<dd id="loggingDefaultProfile" class="col-lg-9 mb-0 font-monospace">—</dd>
|
||||
<dt class="col-lg-3 mb-0">format_version</dt>
|
||||
<dd id="loggingFormatVersion" class="col-lg-9 mb-0 font-monospace">—</dd>
|
||||
</dl>
|
||||
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-xl-5">
|
||||
<label class="form-label" for="loggingProfileSelect">Profil inspecté</label>
|
||||
<select id="loggingProfileSelect" class="form-select" disabled></select>
|
||||
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Globals Logging">
|
||||
<h3 class="h6 mb-3">Globals</h3>
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7">
|
||||
<label class="form-label" for="loggingLogsDirectoryInput">logs_directory</label>
|
||||
<input id="loggingLogsDirectoryInput" class="form-control font-monospace" type="text" autocomplete="off" disabled>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<label class="form-label" for="loggingDefaultProfileSelect">default_profile</label>
|
||||
<select id="loggingDefaultProfileSelect" class="form-select" disabled></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3">
|
||||
<span class="form-label d-block">default_filter</span>
|
||||
<code id="loggingProfileDefaultFilter">—</code>
|
||||
</section>
|
||||
|
||||
<section class="border rounded p-3 mb-4" aria-label="Gestion des profils Logging">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-xl-4">
|
||||
<label class="form-label" for="loggingProfileSelect">Profil édité</label>
|
||||
<select id="loggingProfileSelect" class="form-select" disabled></select>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<label class="form-label" for="loggingProfileIdInput">ID cible create/clone/rename</label>
|
||||
<input id="loggingProfileIdInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="local_test" disabled>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button id="createLoggingProfile" class="btn btn-outline-primary btn-sm" type="button" disabled>Créer</button>
|
||||
<button id="cloneLoggingProfile" class="btn btn-outline-primary btn-sm" type="button" disabled>Cloner</button>
|
||||
<button id="renameLoggingProfile" class="btn btn-outline-secondary btn-sm" type="button" disabled>Renommer</button>
|
||||
<button id="deleteLoggingProfile" class="btn btn-outline-danger btn-sm" type="button" disabled>Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<span class="form-label d-block">span_events</span>
|
||||
<code id="loggingProfileSpanEvents">—</code>
|
||||
</section>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-xl-6">
|
||||
<label class="form-label" for="loggingProfileDefaultFilter">default_filter</label>
|
||||
<select id="loggingProfileDefaultFilter" class="form-select" disabled>
|
||||
<option value="off">off</option><option value="error">error</option><option value="warn">warn</option><option value="info">info</option><option value="debug">debug</option><option value="trace">trace</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<label class="form-label" for="loggingProfileSpanEvents">span_events</label>
|
||||
<select id="loggingProfileSpanEvents" class="form-select" disabled>
|
||||
<option value="off">off</option><option value="new_and_close">new_and_close</option><option value="full">full</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Console Logging">
|
||||
<h3 class="h6 mb-3">Console</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Enabled</th>
|
||||
<th scope="col">Output</th>
|
||||
<th scope="col">ANSI</th>
|
||||
<th scope="col">Format</th>
|
||||
<th scope="col">Level</th>
|
||||
<th scope="col">Targets</th>
|
||||
<th scope="col">Domains</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td id="loggingConsoleEnabled">—</td>
|
||||
<td id="loggingConsoleOutput">—</td>
|
||||
<td id="loggingConsoleAnsi">—</td>
|
||||
<td id="loggingConsoleFormat">—</td>
|
||||
<td id="loggingConsoleLevel">—</td>
|
||||
<td id="loggingConsoleTargets" class="font-monospace text-break">—</td>
|
||||
<td id="loggingConsoleDomains" class="font-monospace text-break">—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-2 form-check ms-2">
|
||||
<input id="loggingConsoleEnabled" class="form-check-input" type="checkbox" disabled>
|
||||
<label class="form-check-label" for="loggingConsoleEnabled">Enabled</label>
|
||||
</div>
|
||||
<div class="col-md-2 form-check ms-2">
|
||||
<input id="loggingConsoleAnsi" class="form-check-input" type="checkbox" disabled>
|
||||
<label class="form-check-label" for="loggingConsoleAnsi">ANSI</label>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="loggingConsoleOutput">Output</label>
|
||||
<select id="loggingConsoleOutput" class="form-select" disabled><option value="stdout">stdout</option><option value="stderr">stderr</option></select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="loggingConsoleFormat">Format</label>
|
||||
<select id="loggingConsoleFormat" class="form-select" disabled><option value="human">human</option><option value="compact">compact</option><option value="pretty">pretty</option><option value="json">json</option></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="loggingConsoleLevel">Level</label>
|
||||
<select id="loggingConsoleLevel" class="form-select" disabled><option value="off">off</option><option value="error">error</option><option value="warn">warn</option><option value="info">info</option><option value="debug">debug</option><option value="trace">trace</option></select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="loggingConsoleTargets">Targets</label>
|
||||
<input id="loggingConsoleTargets" class="form-control font-monospace" type="text" autocomplete="off" disabled>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="loggingConsoleDomains">Domains</label>
|
||||
<input id="loggingConsoleDomains" class="form-control font-monospace" type="text" autocomplete="off" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4" aria-label="Fichiers Logging">
|
||||
<h3 class="h6 mb-3">Fichiers</h3>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||
<div>
|
||||
<h3 class="h6 mb-0">Fichiers</h3>
|
||||
<div class="form-text">0, 1 ou plusieurs sinks persistants. Les suppressions restent dans le brouillon jusqu'à Sauvegarder.</div>
|
||||
</div>
|
||||
<button id="addLoggingFile" class="btn btn-outline-primary btn-sm" type="button" disabled>Ajouter un fichier</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="loggingFilesTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">output_id</th>
|
||||
<th scope="col">Enabled</th>
|
||||
<th scope="col">Path</th>
|
||||
<th scope="col">Rotation</th>
|
||||
<th scope="col">Format</th>
|
||||
<th scope="col">ANSI</th>
|
||||
<th scope="col">Level</th>
|
||||
<th scope="col">Targets</th>
|
||||
<th scope="col">Domains</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<table id="loggingFilesTable" class="table table-sm align-middle w-100">
|
||||
<thead><tr><th>output_id</th><th>On</th><th>Path</th><th>Rotation</th><th>Format</th><th>Level</th><th>Targets</th><th>Domains</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4" aria-label="Target filters Logging">
|
||||
<h3 class="h6 mb-3">Target filters globaux</h3>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
|
||||
<h3 class="h6 mb-0">Target filters globaux</h3>
|
||||
<button id="addLoggingTargetFilter" class="btn btn-outline-primary btn-sm" type="button" disabled>Ajouter un target filter</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="loggingTargetFiltersTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">target_prefix</th>
|
||||
<th scope="col">Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<table id="loggingTargetFiltersTable" class="table table-sm align-middle w-100">
|
||||
<thead><tr><th>target_prefix</th><th>Level</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="loggingSaveReport" class="small text-body-secondary mb-2">Aucune sauvegarde effectuée pendant ce lancement.</div>
|
||||
<div id="loggingStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du document Logging typé...</div>
|
||||
</section>
|
||||
|
||||
@@ -417,6 +451,43 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="loggingDiscardDraftModal" class="modal fade" tabindex="-1" aria-labelledby="loggingDiscardDraftTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="loggingDiscardDraftTitle" class="modal-title fs-5">Abandonner le brouillon Logging</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Annuler"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-0">Des modifications non sauvegardées existent. Recharger la source les abandonnera.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Annuler</button>
|
||||
<button id="loggingDiscardDraftConfirm" class="btn btn-danger" type="button">Abandonner et recharger</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loggingProfileDeleteModal" class="modal fade" tabindex="-1" aria-labelledby="loggingProfileDeleteTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="loggingProfileDeleteTitle" class="modal-title fs-5">Supprimer le profil du brouillon</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Annuler"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-2">Retirer le profil <code id="loggingProfileDeleteName" class="font-monospace"></code> du brouillon Logging ?</p>
|
||||
<p class="text-body-secondary small mb-0">Le fichier n'est pas modifié tant que Sauvegarder n'est pas déclenché.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Annuler</button>
|
||||
<button id="loggingProfileDeleteConfirm" class="btn btn-danger" type="button">Retirer du brouillon</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="environmentRemoveConfirmModal" class="modal fade" tabindex="-1" aria-labelledby="environmentRemoveConfirmTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/logging.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Read-only typed Logging editor backed exclusively by ConfigManagement.
|
||||
//! Typed Logging editor backed exclusively by ConfigManagement validation and persistence.
|
||||
|
||||
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 { LoggingOutputFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto";
|
||||
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 { frontendDebug, frontendTrace } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
let loggingDocument: LoggingDocumentDto | null = null;
|
||||
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;
|
||||
|
||||
function setLoggingStatus(message: string, tone: "primary" | "success" | "danger" = "primary"): void {
|
||||
let draft: LoggingDocumentCandidateDto | 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;
|
||||
@@ -28,58 +38,177 @@ function setText(selector: string, value: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function boolLabel(value: boolean): string {
|
||||
return value ? "oui" : "non";
|
||||
function inputValue(selector: string): string {
|
||||
return document.querySelector<HTMLInputElement>(selector)?.value ?? "";
|
||||
}
|
||||
|
||||
function selectorsLabel(values: string[]): string {
|
||||
return values.length > 0 ? values.join(", ") : "—";
|
||||
function selectedValue(selector: string): string {
|
||||
return document.querySelector<HTMLSelectElement>(selector)?.value ?? "";
|
||||
}
|
||||
|
||||
function filterSummary(filter: LoggingOutputFilterDto): { level: string; targets: string; domains: string } {
|
||||
return { level: filter.level, targets: selectorsLabel(filter.targets), domains: selectorsLabel(filter.domains) };
|
||||
function checkboxValue(selector: string): boolean {
|
||||
return document.querySelector<HTMLInputElement>(selector)?.checked ?? false;
|
||||
}
|
||||
|
||||
function appendCell(row: HTMLTableRowElement, value: string, className?: string): void {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = value;
|
||||
if (className) {
|
||||
cell.className = className;
|
||||
function setInputValue(selector: string, value: string): void {
|
||||
const input = document.querySelector<HTMLInputElement>(selector);
|
||||
if (input) {
|
||||
input.value = value;
|
||||
}
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function renderProfileSelect(documentDto: LoggingDocumentDto): void {
|
||||
const select = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
|
||||
if (!select) {
|
||||
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");
|
||||
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;
|
||||
}
|
||||
const previous = select.value;
|
||||
select.replaceChildren();
|
||||
for (const profile of documentDto.profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.profileId;
|
||||
option.textContent = profile.profileId === documentDto.defaultProfile ? `${profile.profileId} (default)` : profile.profileId;
|
||||
select.append(option);
|
||||
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);
|
||||
}
|
||||
const previousExists = documentDto.profiles.some(profile => profile.profileId === previous);
|
||||
select.value = previousExists ? previous : documentDto.defaultProfile;
|
||||
if (!documentDto.profiles.some(profile => profile.profileId === select.value) && documentDto.profiles.length > 0) {
|
||||
select.value = documentDto.profiles[0].profileId;
|
||||
if (!draft.profiles.some(profile => profile.profileId === selectedProfileId)) {
|
||||
selectedProfileId = draft.profiles[0]?.profileId ?? "";
|
||||
}
|
||||
select.disabled = documentDto.profiles.length === 0;
|
||||
frontendTrace("main", "Logging editor profile select replaced", { profileCount: documentDto.profiles.length });
|
||||
profileSelect.value = selectedProfileId;
|
||||
defaultSelect.value = draft.defaultProfile;
|
||||
frontendTrace("main", "Logging profile selectors replaced", { profileCount: draft.profiles.length });
|
||||
}
|
||||
|
||||
function renderConsole(profile: LoggingProfileDto): void {
|
||||
const summary = filterSummary(profile.console.filter);
|
||||
setText("#loggingConsoleEnabled", boolLabel(profile.console.enabled));
|
||||
setText("#loggingConsoleOutput", profile.console.output);
|
||||
setText("#loggingConsoleAnsi", boolLabel(profile.console.ansi));
|
||||
setText("#loggingConsoleFormat", profile.console.format);
|
||||
setText("#loggingConsoleLevel", summary.level);
|
||||
setText("#loggingConsoleTargets", summary.targets);
|
||||
setText("#loggingConsoleDomains", summary.domains);
|
||||
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 {
|
||||
@@ -88,21 +217,30 @@ function renderFiles(profile: LoggingProfileDto): void {
|
||||
return;
|
||||
}
|
||||
body.replaceChildren();
|
||||
for (const file of profile.files) {
|
||||
const summary = filterSummary(file.filter);
|
||||
profile.files.forEach((file, index) => {
|
||||
const row = document.createElement("tr");
|
||||
appendCell(row, file.outputId, "font-monospace");
|
||||
appendCell(row, boolLabel(file.enabled));
|
||||
appendCell(row, file.path, "font-monospace text-break");
|
||||
appendCell(row, file.rotation);
|
||||
appendCell(row, file.format);
|
||||
appendCell(row, boolLabel(file.ansi));
|
||||
appendCell(row, summary.level);
|
||||
appendCell(row, summary.targets, "font-monospace text-break");
|
||||
appendCell(row, summary.domains, "font-monospace text-break");
|
||||
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 table DOM replaced", { fileCount: profile.files.length });
|
||||
});
|
||||
frontendTrace("main", "Logging file sink editor DOM replaced", { fileCount: profile.files.length });
|
||||
}
|
||||
|
||||
function renderTargetFilters(profile: LoggingProfileDto): void {
|
||||
@@ -111,32 +249,45 @@ function renderTargetFilters(profile: LoggingProfileDto): void {
|
||||
return;
|
||||
}
|
||||
body.replaceChildren();
|
||||
for (const filter of profile.targetFilters) {
|
||||
profile.targetFilters.forEach((filter, index) => {
|
||||
const row = document.createElement("tr");
|
||||
appendCell(row, filter.targetPrefix, "font-monospace");
|
||||
appendCell(row, filter.level);
|
||||
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 override table DOM replaced", { targetFilterCount: profile.targetFilters.length });
|
||||
});
|
||||
frontendTrace("main", "Logging target filter editor DOM replaced", { targetFilterCount: profile.targetFilters.length });
|
||||
}
|
||||
|
||||
function renderSelectedProfile(source: "load" | "selection"): void {
|
||||
const documentDto = loggingDocument;
|
||||
const select = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
|
||||
if (!documentDto || !select) {
|
||||
return;
|
||||
}
|
||||
const profile = documentDto.profiles.find(candidate => candidate.profileId === select.value);
|
||||
function renderSelectedProfile(source: "load" | "selection" | "mutation"): void {
|
||||
const profile = selectedProfile();
|
||||
if (!profile) {
|
||||
setLoggingStatus("Le profil Logging sélectionné n'existe plus dans le document chargé.", "danger");
|
||||
setEditorEnabled(false);
|
||||
return;
|
||||
}
|
||||
setText("#loggingProfileDefaultFilter", profile.defaultFilter);
|
||||
setText("#loggingProfileSpanEvents", profile.spanEvents);
|
||||
renderConsole(profile);
|
||||
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);
|
||||
frontendTrace("main", "Logging profile read-only view rendered", {
|
||||
setEditorEnabled(true);
|
||||
frontendTrace("main", "Logging profile editor rendered", {
|
||||
profileId: profile.profileId,
|
||||
source,
|
||||
fileCount: profile.files.length,
|
||||
@@ -145,46 +296,332 @@ function renderSelectedProfile(source: "load" | "selection"): void {
|
||||
}
|
||||
|
||||
function renderDocument(documentDto: LoggingDocumentDto): void {
|
||||
loggingDocument = 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());
|
||||
setText("#loggingLogsDirectory", documentDto.logsDirectory);
|
||||
setText("#loggingDefaultProfile", documentDto.defaultProfile);
|
||||
renderProfileSelect(documentDto);
|
||||
setInputValue("#loggingLogsDirectoryInput", draft.logsDirectory);
|
||||
renderProfileSelects();
|
||||
renderSelectedProfile("load");
|
||||
frontendTrace("main", "Logging document read-only view rendered", { profileCount: documentDto.profiles.length });
|
||||
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 et sauvegarde atomique du document Logging...");
|
||||
frontendDebug("main", "Typed Logging document persistence 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);
|
||||
setText("#loggingSaveReport", `source_changed=${result.sourceChanged} · reload_required=${result.reloadRequired} · runtime Logging inchangé`);
|
||||
setLoggingStatus(result.sourceChanged ? "Document Logging validé et persisté. Le runtime sera traité en pre.016." : "Document Logging valide mais source inchangée.", "success");
|
||||
frontendDebug("main", "Typed Logging document persistence completed", {
|
||||
sourceChanged: result.sourceChanged,
|
||||
reloadRequired: result.reloadRequired,
|
||||
profileCount: result.document.profiles.length,
|
||||
});
|
||||
} catch {
|
||||
setEditorEnabled(true);
|
||||
setLoggingStatus("Candidat Logging refusé. std.logging.json reste inchangé ; corrige le brouillon puis réessaie.", "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 refresh requested");
|
||||
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) en lecture seule.`, "success");
|
||||
frontendDebug("main", "Logging document refresh completed", {
|
||||
setLoggingStatus(`${documentDto.profiles.length} profil(s) Logging chargé(s). Le brouillon est synchronisé avec la source.`, "success");
|
||||
frontendDebug("main", "Logging document reload completed", {
|
||||
profileCount: documentDto.profiles.length,
|
||||
defaultProfile: documentDto.defaultProfile,
|
||||
});
|
||||
} catch {
|
||||
loggingDocument = null;
|
||||
draft = null;
|
||||
setEditorEnabled(false);
|
||||
setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger");
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the read-only Logging editor surface.
|
||||
export function initializeLoggingPanel(): void {
|
||||
document.querySelector<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", () => {
|
||||
void refreshLoggingDocument("user");
|
||||
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<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
|
||||
const profileId = (event.currentTarget as HTMLSelectElement).value;
|
||||
frontendDebug("main", "Logging read-only profile selection changed", { profileId });
|
||||
syncStaticProfileFields();
|
||||
selectedProfileId = (event.currentTarget as HTMLSelectElement).value;
|
||||
frontendDebug("main", "Logging profile selection changed", { profileId: selectedProfileId });
|
||||
renderSelectedProfile("selection");
|
||||
});
|
||||
frontendTrace("main", "Logging editor handlers installed");
|
||||
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);
|
||||
frontendTrace("main", "Logging editor mutation handlers installed");
|
||||
void refreshLoggingDocument("startup");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -38,7 +38,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
|
||||
},
|
||||
logging: {
|
||||
title: "Logging",
|
||||
description: "Lecture typée des profils, sinks et filtres Logging ; mutations et hot reload restent dans les tranches suivantes.",
|
||||
description: "Édition typée et persistence atomique des profils, sinks et filtres Logging ; le hot reload runtime reste dans la tranche suivante.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user