v0.1.4-pre.012

This commit is contained in:
2026-08-16 16:26:03 +02:00
parent 8609d335a3
commit 13a2e0038a
13 changed files with 614 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 6 -->
<!-- version: 7 -->
<!DOCTYPE html>
<html lang="fr">
@@ -208,7 +208,7 @@
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3">
<div>
<h2 class="h5 mb-1">Rapport Environnement / .env</h2>
<p class="text-body-secondary small mb-0">Valeurs desired/effective sûres, provenance externe et shadowing calculés exclusivement par ksp-config-lib.</p>
<p class="text-body-secondary small mb-0">Valeurs desired/effective sûres, provenance externe, shadowing et mutations persistées exclusivement par ksp-config-lib.</p>
</div>
<button id="refreshEnvironment" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
@@ -217,7 +217,7 @@
</div>
<div class="alert alert-light border small" role="note">
Les valeurs Secret restent redacted. Cette tranche est en lecture seule : création, modification et suppression de valeurs `.env` arrivent en pre.012.
Priorité effective : <code>process &gt; .env &gt; fallback</code>. Une mutation ne modifie jamais l'environnement process déjà hérité. Les valeurs Secret restent redacted dans le rapport.
</div>
<div class="table-responsive mb-4">
@@ -237,7 +237,57 @@
</table>
</div>
<div id="environmentStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du rapport environnement...</div>
<div id="environmentStatus" class="alert alert-primary mb-4" role="status" aria-live="polite">Chargement du rapport environnement...</div>
<section class="border rounded p-3 bg-body-tertiary" aria-label="Management .env">
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3">
<div>
<h2 class="h5 mb-1">Management / test .env</h2>
<p class="text-body-secondary small mb-0">Cliquer une ligne du rapport sélectionne son nom uniquement. Aucune ancienne valeur réelle n'est préchargée dans le formulaire.</p>
</div>
<button id="clearEnvironmentEditor" class="btn btn-outline-secondary btn-sm" type="button">Vider le formulaire</button>
</div>
<div class="row g-3 align-items-end mb-3">
<div class="col-xl-5">
<label class="form-label" for="environmentVariableName">Variable KSP/KSPB</label>
<input id="environmentVariableName" class="form-control font-monospace" type="text" list="environmentVariableSuggestions" autocomplete="off" placeholder="KSP_... ou KSPB_...">
<datalist id="environmentVariableSuggestions"></datalist>
</div>
<div class="col-xl-5">
<label class="form-label" for="environmentVariableValue">Nouvelle valeur</label>
<input id="environmentVariableValue" class="form-control font-monospace" type="text" autocomplete="new-password" spellcheck="false">
<div id="environmentEditorSensitivity" class="form-text">La valeur saisie n'est jamais préchargée depuis .env.</div>
</div>
<div class="col-xl-2 d-grid gap-2">
<button id="setEnvironmentValue" class="btn btn-primary btn-sm" type="button">Créer / Modifier</button>
<button id="removeEnvironmentValue" class="btn btn-outline-danger btn-sm" type="button">Supprimer</button>
</div>
</div>
<div id="environmentMutationStatus" class="alert alert-secondary mb-3" role="status" aria-live="polite">
Aucune mutation effectuée pendant ce lancement.
</div>
<div id="environmentMutationResult" hidden>
<h3 class="h6">Dernier résultat ConfigEnvironmentChangeReport</h3>
<dl class="row small mb-0 app-profile-metadata">
<dt class="col-md-4">Opération</dt>
<dd id="environmentMutationOperation" class="col-md-8"></dd>
<dt class="col-md-4">Variable</dt>
<dd id="environmentMutationVariable" class="col-md-8 font-monospace"></dd>
<dt class="col-md-4">source_changed</dt>
<dd id="environmentMutationSourceChanged" class="col-md-8 font-monospace"></dd>
<dt class="col-md-4">effective_changed</dt>
<dd id="environmentMutationEffectiveChanged" class="col-md-8 font-monospace"></dd>
<dt class="col-md-4">shadowed_by_process</dt>
<dd id="environmentMutationShadowed" class="col-md-8 font-monospace"></dd>
<dt class="col-md-4 mb-0">reload_required</dt>
<dd id="environmentMutationReload" class="col-md-8 mb-0 font-monospace"></dd>
</dl>
<div class="form-text mt-2">Le rapport est rechargé après chaque mutation. Un consumer qui conserve son propre snapshot doit respecter <code>reload_required</code>.</div>
</div>
</section>
</section>
<section id="placeholderPanel" hidden aria-live="polite">

View File

@@ -1,10 +1,11 @@
// file: crates/ksp-app-config-desk/frontend/ts/environment.ts
// version: 1
// version: 2
//! Safe environment report panel backed exclusively by ConfigManagement::environment_report().
//! Safe environment report and `.env` management panel backed exclusively by ConfigManagement.
import DataTable from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import type { ConfigEnvironmentChangeDto } from "./bindings/ksp_app_config_desk/environment/ConfigEnvironmentChangeDto";
import type { ConfigEnvironmentReportDto } from "./bindings/ksp_app_config_desk/environment/ConfigEnvironmentReportDto";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
@@ -14,6 +15,7 @@ interface EnvironmentDataTable {
}
let environmentTable: EnvironmentDataTable | null = null;
let currentReports: ConfigEnvironmentReportDto[] = [];
function setEnvironmentStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
const status = document.querySelector<HTMLElement>("#environmentStatus");
@@ -25,6 +27,16 @@ function setEnvironmentStatus(message: string, tone: "primary" | "success" | "wa
frontendTrace("main", "Environment panel status replaced", { tone });
}
function setMutationStatus(message: string, tone: "secondary" | "success" | "warning" | "danger" = "secondary"): void {
const status = document.querySelector<HTMLElement>("#environmentMutationStatus");
if (!status) {
return;
}
status.className = `alert alert-${tone} mb-0`;
status.textContent = message;
frontendTrace("main", "Environment mutation status replaced", { tone });
}
function sensitivityLabel(value: string): string {
if (value === "public") {
return "Public";
@@ -72,6 +84,7 @@ function populateTableRows(reports: ConfigEnvironmentReportDto[]): void {
body.replaceChildren();
for (const report of reports) {
const row = document.createElement("tr");
row.dataset.variableName = report.variableName;
appendCell(row, report.variableName, "font-monospace");
appendCell(row, report.namespace);
appendCell(row, sensitivityLabel(report.sensitivity));
@@ -84,11 +97,29 @@ function populateTableRows(reports: ConfigEnvironmentReportDto[]): void {
frontendTrace("main", "Environment report table DOM replaced", { variableCount: reports.length });
}
function initializeDataTable(): void {
function populateVariableSuggestions(reports: ConfigEnvironmentReportDto[]): void {
const list = document.querySelector<HTMLDataListElement>("#environmentVariableSuggestions");
if (!list) {
return;
}
list.replaceChildren();
for (const report of reports) {
const option = document.createElement("option");
option.value = report.variableName;
list.append(option);
}
frontendTrace("main", "Environment variable suggestions replaced", { variableCount: reports.length });
}
function destroyEnvironmentDataTable(): void {
if (environmentTable) {
environmentTable.destroy();
environmentTable = null;
frontendTrace("main", "DataTables Environment destroyed before report replacement");
}
}
function initializeDataTable(): void {
environmentTable = new DataTable("#environmentReportTable", {
order: [[0, "asc"]],
pageLength: 10,
@@ -104,12 +135,101 @@ function initializeDataTable(): void {
frontendTrace("main", "DataTables Environment initialized");
}
function classifyEditorSensitivity(variableName: string): "public" | "internal" | "secret" | "invalid" {
if (variableName.startsWith("KSP_SECRET_") || variableName.startsWith("KSPB_SECRET_")) {
return "secret";
}
if (variableName.startsWith("KSP_PUBLIC_") || variableName.startsWith("KSPB_PUBLIC_")) {
return "public";
}
if (variableName.startsWith("KSP_") || variableName.startsWith("KSPB_")) {
return "internal";
}
return "invalid";
}
function updateEditorSensitivity(): void {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
const note = document.querySelector<HTMLElement>("#environmentEditorSensitivity");
if (!variable || !value || !note) {
return;
}
const sensitivity = classifyEditorSensitivity(variable.value.trim());
value.type = sensitivity === "secret" ? "password" : "text";
note.textContent = sensitivity === "secret"
? "Secret : la nouvelle saisie est masquée et l'ancienne valeur réelle n'est jamais préchargée."
: sensitivity === "invalid"
? "Saisis un nom KSP_* ou KSPB_* ; Config reste l'autorité de validation."
: `${sensitivityLabel(sensitivity)} : la valeur saisie n'est jamais préchargée depuis .env.`;
frontendTrace("main", "Environment mutation editor sensitivity updated", { sensitivity });
}
function selectVariableForMutation(variableName: string, source: "table" | "input"): void {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
if (!variable || !value) {
return;
}
variable.value = variableName;
value.value = "";
updateEditorSensitivity();
frontendDebug("main", "Environment mutation variable selected", { variableName, source });
}
function renderMutationResult(result: ConfigEnvironmentChangeDto): void {
const operation = document.querySelector<HTMLElement>("#environmentMutationOperation");
const variable = document.querySelector<HTMLElement>("#environmentMutationVariable");
const sourceChanged = document.querySelector<HTMLElement>("#environmentMutationSourceChanged");
const effectiveChanged = document.querySelector<HTMLElement>("#environmentMutationEffectiveChanged");
const shadowed = document.querySelector<HTMLElement>("#environmentMutationShadowed");
const reload = document.querySelector<HTMLElement>("#environmentMutationReload");
const panel = document.querySelector<HTMLElement>("#environmentMutationResult");
if (operation) {
operation.textContent = result.operation === "remove" ? "suppression" : "création / modification";
}
if (variable) {
variable.textContent = result.variableName;
}
if (sourceChanged) {
sourceChanged.textContent = result.sourceChanged ? "true" : "false";
}
if (effectiveChanged) {
effectiveChanged.textContent = result.effectiveChanged ? "true" : "false";
}
if (shadowed) {
shadowed.textContent = result.shadowedByProcessEnvironment ? "true" : "false";
}
if (reload) {
reload.textContent = result.reloadRequired ? "true" : "false";
}
if (panel) {
panel.hidden = false;
}
frontendTrace("main", "Environment mutation result rendered", {
operation: result.operation,
variableName: result.variableName,
sourceChanged: result.sourceChanged,
effectiveChanged: result.effectiveChanged,
shadowedByProcessEnvironment: result.shadowedByProcessEnvironment,
reloadRequired: result.reloadRequired,
});
}
function setMutationControlsDisabled(disabled: boolean): void {
document.querySelector<HTMLButtonElement>("#setEnvironmentValue")?.toggleAttribute("disabled", disabled);
document.querySelector<HTMLButtonElement>("#removeEnvironmentValue")?.toggleAttribute("disabled", disabled);
}
async function refreshEnvironmentReport(): Promise<void> {
frontendDebug("main", "Config environment report refresh requested");
setEnvironmentStatus("Chargement du rapport environnement sûr...", "primary");
try {
const reports = await invokeKsp<ConfigEnvironmentReportDto[]>("main", "get_environment_report");
currentReports = reports;
destroyEnvironmentDataTable();
populateTableRows(reports);
populateVariableSuggestions(reports);
initializeDataTable();
const shadowedCount = reports.filter(report => report.shadowedByProcessEnvironment).length;
setEnvironmentStatus(`${reports.length} variable(s) KSP/KSPB évaluée(s) par Config ; ${shadowedCount} valeur(s) .env shadowed.`, "success");
@@ -122,15 +242,131 @@ async function refreshEnvironmentReport(): Promise<void> {
}
}
async function setEnvironmentValue(): Promise<void> {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
if (!variable || !value) {
return;
}
const variableName = variable.value.trim();
if (!variableName) {
setMutationStatus("Saisis un nom de variable KSP_* ou KSPB_*.", "warning");
return;
}
const sensitivity = classifyEditorSensitivity(variableName);
frontendDebug("main", "Environment .env mutation requested", { operation: "set", variableName, sensitivity });
setMutationControlsDisabled(true);
setMutationStatus("Validation et persistence atomique par Config...", "secondary");
try {
const result = await invokeKsp<ConfigEnvironmentChangeDto>("main", "set_environment_value", { variableName, value: value.value });
renderMutationResult(result);
value.value = "";
setMutationStatus("Mutation .env acceptée par Config ; rapport rechargé depuis la source persistée.", "success");
await refreshEnvironmentReport();
frontendDebug("main", "Environment .env mutation completed", {
operation: result.operation,
variableName: result.variableName,
sourceChanged: result.sourceChanged,
effectiveChanged: result.effectiveChanged,
shadowedByProcessEnvironment: result.shadowedByProcessEnvironment,
reloadRequired: result.reloadRequired,
});
} catch {
setMutationStatus("La mutation .env a été refusée ou n'a pas pu être persistée par Config.", "danger");
} finally {
setMutationControlsDisabled(false);
}
}
async function removeEnvironmentValue(): Promise<void> {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
if (!variable || !value) {
return;
}
const variableName = variable.value.trim();
if (!variableName) {
setMutationStatus("Saisis ou sélectionne la variable .env à supprimer.", "warning");
return;
}
const confirmed = window.confirm(`Supprimer l'entrée .env ${variableName} ? L'environnement process hérité ne sera pas modifié.`);
frontendTrace("main", "Environment .env removal confirmation answered", { variableName, confirmed });
if (!confirmed) {
return;
}
const sensitivity = classifyEditorSensitivity(variableName);
frontendDebug("main", "Environment .env mutation requested", { operation: "remove", variableName, sensitivity });
setMutationControlsDisabled(true);
setMutationStatus("Suppression atomique par Config...", "secondary");
try {
const result = await invokeKsp<ConfigEnvironmentChangeDto>("main", "remove_environment_value", { variableName });
renderMutationResult(result);
value.value = "";
setMutationStatus("Suppression .env traitée par Config ; rapport rechargé depuis la source persistée.", "success");
await refreshEnvironmentReport();
frontendDebug("main", "Environment .env mutation completed", {
operation: result.operation,
variableName: result.variableName,
sourceChanged: result.sourceChanged,
effectiveChanged: result.effectiveChanged,
shadowedByProcessEnvironment: result.shadowedByProcessEnvironment,
reloadRequired: result.reloadRequired,
});
} catch {
setMutationStatus("La suppression .env a été refusée ou n'a pas pu être persistée par Config.", "danger");
} finally {
setMutationControlsDisabled(false);
}
}
function bindEnvironmentActions(): void {
document.querySelector<HTMLButtonElement>("#refreshEnvironment")?.addEventListener("click", () => {
frontendTrace("main", "Environment refresh button clicked");
void refreshEnvironmentReport();
});
document.querySelector<HTMLInputElement>("#environmentVariableName")?.addEventListener("input", () => {
updateEditorSensitivity();
});
document.querySelector<HTMLInputElement>("#environmentVariableName")?.addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
const exact = currentReports.find(report => report.variableName === input.value.trim());
if (exact) {
selectVariableForMutation(exact.variableName, "input");
}
});
document.querySelector<HTMLTableElement>("#environmentReportTable")?.addEventListener("click", event => {
const target = event.target as HTMLElement;
const row = target.closest<HTMLTableRowElement>("tbody tr");
const variableName = row?.dataset.variableName;
if (variableName) {
selectVariableForMutation(variableName, "table");
}
});
document.querySelector<HTMLButtonElement>("#setEnvironmentValue")?.addEventListener("click", () => {
frontendTrace("main", "Environment set value button clicked");
void setEnvironmentValue();
});
document.querySelector<HTMLButtonElement>("#removeEnvironmentValue")?.addEventListener("click", () => {
frontendTrace("main", "Environment remove value button clicked");
void removeEnvironmentValue();
});
document.querySelector<HTMLButtonElement>("#clearEnvironmentEditor")?.addEventListener("click", () => {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
if (variable) {
variable.value = "";
}
if (value) {
value.value = "";
}
updateEditorSensitivity();
frontendTrace("main", "Environment mutation editor cleared");
});
frontendTrace("main", "Environment panel handlers installed");
}
export function initializeEnvironmentPanel(): void {
bindEnvironmentActions();
updateEditorSensitivity();
void refreshEnvironmentReport();
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 6
// version: 7
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -32,7 +32,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
},
environment: {
title: "Environnement / .env",
description: "Rapport sûr desired/effective/source/shadow des variables KSP/KSPB ; mutations .env prévues en pre.012.",
description: "Rapport sûr desired/effective/source/shadow et management atomique des variables .env KSP/KSPB.",
},
logging: {
title: "Logging",