v0.1.4-pre.014
This commit is contained in:
@@ -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">
|
||||
|
||||
190
crates/ksp-app-config-desk/frontend/ts/logging.ts
Normal file
190
crates/ksp-app-config-desk/frontend/ts/logging.ts
Normal 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");
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user