// file: crates/ksp-app-config-desk/frontend/ts/environment.ts // version: 4 //! Safe environment report and `.env` management panel backed exclusively by ConfigManagement. import { Modal } from "bootstrap"; 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"; import { initializeSecretReveal, requestSecretReveal } from "./secret_reveal"; interface EnvironmentDataTable { destroy(): unknown; } let environmentTable: EnvironmentDataTable | null = null; let currentReports: ConfigEnvironmentReportDto[] = []; let mutationControlsDisabled = false; function setEnvironmentStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void { const status = document.querySelector("#environmentStatus"); if (!status) { return; } status.className = `alert alert-${tone} mb-0`; status.textContent = message; frontendTrace("main", "Environment panel status replaced", { tone }); } function setMutationStatus(message: string, tone: "secondary" | "success" | "warning" | "danger" = "secondary"): void { const status = document.querySelector("#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"; } if (value === "internal") { return "Internal"; } if (value === "secret") { return "Secret"; } return value; } function sourceLabel(value: string): string { if (value === "process") { return "process"; } if (value === "dotenv") { return "dotenv"; } if (value === "fallback") { return "fallback"; } return "—"; } function safeValue(value: string | null): string { return value ?? "—"; } function appendCell(row: HTMLTableRowElement, text: string, className?: string): void { const cell = document.createElement("td"); cell.textContent = text; if (className) { cell.className = className; } row.append(cell); } function populateTableRows(reports: ConfigEnvironmentReportDto[]): void { const body = document.querySelector("#environmentReportTable tbody"); if (!body) { return; } 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)); appendCell(row, safeValue(report.desiredSafeValue), "font-monospace text-break"); appendCell(row, safeValue(report.effectiveSafeValue), "font-monospace text-break"); appendCell(row, sourceLabel(report.effectiveSource)); appendCell(row, report.shadowedByProcessEnvironment ? "oui" : "non"); body.append(row); } frontendTrace("main", "Environment report table DOM replaced", { variableCount: reports.length }); } function populateVariableSuggestions(reports: ConfigEnvironmentReportDto[]): void { const list = document.querySelector("#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, language: { emptyTable: "Aucune variable KSP/KSPB présente dans process ou .env.", info: "_START_ à _END_ sur _TOTAL_ variable(s)", infoEmpty: "0 variable", lengthMenu: "Afficher _MENU_", search: "Filtrer :", zeroRecords: "Aucune variable correspondante.", }, }) as unknown as EnvironmentDataTable; 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("#environmentVariableName"); const value = document.querySelector("#environmentVariableValue"); const note = document.querySelector("#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.`; updateRevealButtons(sensitivity); frontendTrace("main", "Environment mutation editor sensitivity updated", { sensitivity }); } function updateRevealButtons(sensitivity = classifyEditorSensitivity(document.querySelector("#environmentVariableName")?.value.trim() ?? "")): void { const revealDisabled = mutationControlsDisabled || sensitivity !== "secret"; document.querySelector("#revealEnvironmentEffective")?.toggleAttribute("disabled", revealDisabled); document.querySelector("#revealEnvironmentDotenv")?.toggleAttribute("disabled", revealDisabled); } function selectVariableForMutation(variableName: string, source: "table" | "input"): void { const variable = document.querySelector("#environmentVariableName"); const value = document.querySelector("#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("#environmentMutationOperation"); const variable = document.querySelector("#environmentMutationVariable"); const sourceChanged = document.querySelector("#environmentMutationSourceChanged"); const effectiveChanged = document.querySelector("#environmentMutationEffectiveChanged"); const shadowed = document.querySelector("#environmentMutationShadowed"); const reload = document.querySelector("#environmentMutationReload"); const panel = document.querySelector("#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 { mutationControlsDisabled = disabled; document.querySelector("#setEnvironmentValue")?.toggleAttribute("disabled", disabled); document.querySelector("#removeEnvironmentValue")?.toggleAttribute("disabled", disabled); updateRevealButtons(); } function confirmEnvironmentRemoval(variableName: string): Promise { const modalElement = document.querySelector("#environmentRemoveConfirmModal"); const variable = document.querySelector("#environmentRemoveConfirmVariable"); const confirmButton = document.querySelector("#environmentRemoveConfirmAction"); if (!modalElement || !variable || !confirmButton) { frontendDebug("main", "Environment removal confirmation modal is unavailable", { variableName }); return Promise.resolve(false); } variable.textContent = variableName; const modal = Modal.getOrCreateInstance(modalElement); frontendDebug("main", "Environment .env removal confirmation opened", { variableName }); return new Promise(resolve => { let confirmed = false; const onConfirm = (): void => { confirmed = true; modal.hide(); }; const onHidden = (): void => { confirmButton.removeEventListener("click", onConfirm); variable.textContent = ""; frontendDebug("main", "Environment .env removal confirmation answered", { variableName, confirmed }); resolve(confirmed); }; confirmButton.addEventListener("click", onConfirm, { once: true }); modalElement.addEventListener("hidden.bs.modal", onHidden, { once: true }); modal.show(); }); } async function refreshEnvironmentReport(): Promise { frontendDebug("main", "Config environment report refresh requested"); setEnvironmentStatus("Chargement du rapport environnement sûr...", "primary"); try { const reports = await invokeKsp("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"); frontendDebug("main", "Config environment report refresh completed", { variableCount: reports.length, shadowedCount, }); } catch { setEnvironmentStatus("Le rapport environnement n'a pas pu être chargé par Config.", "danger"); } } async function setEnvironmentValue(): Promise { const variable = document.querySelector("#environmentVariableName"); const value = document.querySelector("#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("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 { const variable = document.querySelector("#environmentVariableName"); const value = document.querySelector("#environmentVariableValue"); if (!variable || !value) { return; } const variableName = variable.value.trim(); if (!variableName) { setMutationStatus("Saisis ou sélectionne la variable .env à supprimer.", "warning"); return; } setMutationControlsDisabled(true); const confirmed = await confirmEnvironmentRemoval(variableName); if (!confirmed) { setMutationControlsDisabled(false); return; } const sensitivity = classifyEditorSensitivity(variableName); frontendDebug("main", "Environment .env mutation requested", { operation: "remove", variableName, sensitivity }); setMutationStatus("Suppression atomique par Config...", "secondary"); try { const result = await invokeKsp("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("#refreshEnvironment")?.addEventListener("click", () => { frontendTrace("main", "Environment refresh button clicked"); void refreshEnvironmentReport(); }); document.querySelector("#environmentVariableName")?.addEventListener("input", () => { updateEditorSensitivity(); }); document.querySelector("#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("#environmentReportTable")?.addEventListener("click", event => { const target = event.target as HTMLElement; const row = target.closest("tbody tr"); const variableName = row?.dataset.variableName; if (variableName) { selectVariableForMutation(variableName, "table"); } }); document.querySelector("#setEnvironmentValue")?.addEventListener("click", () => { frontendTrace("main", "Environment set value button clicked"); void setEnvironmentValue(); }); document.querySelector("#removeEnvironmentValue")?.addEventListener("click", () => { frontendTrace("main", "Environment remove value button clicked"); void removeEnvironmentValue(); }); document.querySelector("#revealEnvironmentEffective")?.addEventListener("click", () => { const variableName = document.querySelector("#environmentVariableName")?.value.trim() ?? ""; requestSecretReveal(variableName, "effective"); }); document.querySelector("#revealEnvironmentDotenv")?.addEventListener("click", () => { const variableName = document.querySelector("#environmentVariableName")?.value.trim() ?? ""; requestSecretReveal(variableName, "dotenv"); }); document.querySelector("#clearEnvironmentEditor")?.addEventListener("click", () => { const variable = document.querySelector("#environmentVariableName"); const value = document.querySelector("#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 { initializeSecretReveal(); bindEnvironmentActions(); updateEditorSensitivity(); void refreshEnvironmentReport(); }