v0.1.4-pre.014

This commit is contained in:
2026-08-16 17:03:07 +02:00
parent e0f2586400
commit 789ffb16ce
17 changed files with 771 additions and 58 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 9 -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -297,10 +297,114 @@
</section>
</section>
<section id="placeholderPanel" hidden aria-live="polite">
<div class="alert alert-secondary mb-4" role="status">
Cette route du shell est prête ; son contenu fonctionnel sera ajouté dans la prerelease prévue par le plan 0.1.4.
<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>
</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">
<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>
</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>
</div>
<div class="col-xl-3">
<span class="form-label d-block">default_filter</span>
<code id="loggingProfileDefaultFilter"></code>
</div>
<div class="col-xl-4">
<span class="form-label d-block">span_events</span>
<code id="loggingProfileSpanEvents"></code>
</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>
</section>
<section class="mb-4" aria-label="Fichiers Logging">
<h3 class="h6 mb-3">Fichiers</h3>
<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>
<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="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>
<tbody></tbody>
</table>
</div>
</section>
<div id="loggingStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du document Logging typé...</div>
</section>
<div class="alert alert-primary mb-0" role="status">

View File

@@ -0,0 +1,190 @@
// file: crates/ksp-app-config-desk/frontend/ts/logging.ts
// version: 1
//! Read-only typed Logging editor backed exclusively by ConfigManagement.
import type { LoggingDocumentDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentDto";
import type { LoggingOutputFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto";
import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
let loggingDocument: LoggingDocumentDto | null = null;
function setLoggingStatus(message: string, tone: "primary" | "success" | "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 boolLabel(value: boolean): string {
return value ? "oui" : "non";
}
function selectorsLabel(values: string[]): string {
return values.length > 0 ? values.join(", ") : "—";
}
function filterSummary(filter: LoggingOutputFilterDto): { level: string; targets: string; domains: string } {
return { level: filter.level, targets: selectorsLabel(filter.targets), domains: selectorsLabel(filter.domains) };
}
function appendCell(row: HTMLTableRowElement, value: string, className?: string): void {
const cell = document.createElement("td");
cell.textContent = value;
if (className) {
cell.className = className;
}
row.append(cell);
}
function renderProfileSelect(documentDto: LoggingDocumentDto): void {
const select = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
if (!select) {
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);
}
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;
}
select.disabled = documentDto.profiles.length === 0;
frontendTrace("main", "Logging editor profile select replaced", { profileCount: documentDto.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 renderFiles(profile: LoggingProfileDto): void {
const body = document.querySelector<HTMLTableSectionElement>("#loggingFilesTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const file of profile.files) {
const summary = filterSummary(file.filter);
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");
body.append(row);
}
frontendTrace("main", "Logging file sink table DOM replaced", { fileCount: profile.files.length });
}
function renderTargetFilters(profile: LoggingProfileDto): void {
const body = document.querySelector<HTMLTableSectionElement>("#loggingTargetFiltersTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const filter of profile.targetFilters) {
const row = document.createElement("tr");
appendCell(row, filter.targetPrefix, "font-monospace");
appendCell(row, filter.level);
body.append(row);
}
frontendTrace("main", "Logging target override table 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);
if (!profile) {
setLoggingStatus("Le profil Logging sélectionné n'existe plus dans le document chargé.", "danger");
return;
}
setText("#loggingProfileDefaultFilter", profile.defaultFilter);
setText("#loggingProfileSpanEvents", profile.spanEvents);
renderConsole(profile);
renderFiles(profile);
renderTargetFilters(profile);
frontendTrace("main", "Logging profile read-only view rendered", {
profileId: profile.profileId,
source,
fileCount: profile.files.length,
targetFilterCount: profile.targetFilters.length,
});
}
function renderDocument(documentDto: LoggingDocumentDto): void {
loggingDocument = documentDto;
setText("#loggingFileId", documentDto.fileId);
setText("#loggingPath", documentDto.path);
setText("#loggingFormatVersion", documentDto.formatVersion.toString());
setText("#loggingLogsDirectory", documentDto.logsDirectory);
setText("#loggingDefaultProfile", documentDto.defaultProfile);
renderProfileSelect(documentDto);
renderSelectedProfile("load");
frontendTrace("main", "Logging document read-only view rendered", { profileCount: documentDto.profiles.length });
}
async function refreshLoggingDocument(source: "startup" | "user"): Promise<void> {
if (source === "user") {
frontendDebug("main", "Logging document refresh requested");
}
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", {
profileCount: documentDto.profiles.length,
defaultProfile: documentDto.defaultProfile,
});
} catch {
loggingDocument = null;
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");
});
document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
const profileId = (event.currentTarget as HTMLSelectElement).value;
frontendDebug("main", "Logging read-only profile selection changed", { profileId });
renderSelectedProfile("selection");
});
frontendTrace("main", "Logging editor handlers installed");
void refreshLoggingDocument("startup");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 8
// version: 9
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -9,6 +9,7 @@ import type { AppSnapshotDto } from "./bindings/ksp_app_config_desk/dto_common/A
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { initializeDocumentsPanel } from "./documents";
import { initializeEnvironmentPanel } from "./environment";
import { initializeLoggingPanel } from "./logging";
import { initializeProfilesPanel } from "./profiles";
import { invokeKsp } from "./invoke";
import { clearTransientSecretReveal } from "./secret_reveal";
@@ -37,7 +38,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
},
logging: {
title: "Logging",
description: "Édition des profils et hot reload du runtime Logging — surfaces fonctionnelles prévues en pre.014 à pre.017.",
description: "Lecture typée des profils, sinks et filtres Logging ; mutations et hot reload restent dans les tranches suivantes.",
},
};
@@ -88,9 +89,9 @@ function activateView(viewId: ViewId, source: "startup" | "user"): void {
if (environment) {
environment.hidden = viewId !== "environment";
}
const placeholder = document.querySelector<HTMLElement>("#placeholderPanel");
if (placeholder) {
placeholder.hidden = viewId !== "logging";
const logging = document.querySelector<HTMLElement>("#loggingPanel");
if (logging) {
logging.hidden = viewId !== "logging";
}
frontendTrace("main", "Main view DOM updated", { viewId, source });
}
@@ -152,6 +153,7 @@ async function initializeMain(): Promise<void> {
initializeDocumentsPanel();
initializeProfilesPanel();
initializeEnvironmentPanel();
initializeLoggingPanel();
activateView("overview", "startup");
try {
await loadSnapshot();