v0.1.4-pre.013

This commit is contained in:
2026-08-16 16:51:11 +02:00
parent 2b343c25de
commit e0f2586400
16 changed files with 549 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 8 -->
<!-- version: 9 -->
<!DOCTYPE html>
<html lang="fr">
@@ -265,6 +265,13 @@
</div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<span class="small text-body-secondary me-1">Reveal Secret privilégié :</span>
<button id="revealEnvironmentEffective" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler effective</button>
<button id="revealEnvironmentDotenv" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler .env</button>
<span class="form-text mb-0">Disponible uniquement pour KSP_SECRET_* / KSPB_SECRET_* et jamais préchargé automatiquement.</span>
</div>
<div id="environmentMutationStatus" class="alert alert-secondary mb-3" role="status" aria-live="polite">
Aucune mutation effectuée pendant ce lancement.
</div>
@@ -325,6 +332,39 @@
</div>
</div>
<div id="secretRevealModal" class="modal fade" tabindex="-1" aria-labelledby="secretRevealModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 id="secretRevealModalLabel" class="modal-title fs-5">Reveal Secret privilégié</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div id="secretRevealConfirmPanel">
<p class="mb-2">Variable : <code id="secretRevealVariable" class="font-monospace"></code></p>
<p class="mb-3">Source demandée : <code id="secretRevealSource"></code></p>
<div class="alert alert-warning small mb-3" role="note">
Cette action expose temporairement la valeur réelle dans la WebView. Elle n'est ni journalisée, ni ajoutée à AppState, ni persistée par le frontend.
</div>
</div>
<div id="secretRevealResultPanel" hidden>
<label class="form-label fw-semibold" for="secretRevealValue">Valeur réelle transitoire</label>
<div class="input-group">
<input id="secretRevealValue" class="form-control font-monospace" type="password" readonly autocomplete="off" spellcheck="false">
<button id="secretRevealToggleVisibility" class="btn btn-outline-secondary" type="button">Afficher</button>
</div>
<div id="secretRevealValueAbsent" class="alert alert-secondary small mb-0" hidden>Aucune valeur n'existe dans cette source.</div>
</div>
<p id="secretRevealStatus" class="text-body-secondary small mt-3 mb-0" aria-live="polite"></p>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Fermer</button>
<button id="secretRevealConfirmAction" class="btn btn-warning" type="button">Révéler</button>
</div>
</div>
</div>
</div>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/environment.ts
// version: 3
// version: 4
//! Safe environment report and `.env` management panel backed exclusively by ConfigManagement.
@@ -10,6 +10,7 @@ import type { ConfigEnvironmentChangeDto } from "./bindings/ksp_app_config_desk/
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;
@@ -17,6 +18,7 @@ interface EnvironmentDataTable {
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<HTMLElement>("#environmentStatus");
@@ -163,9 +165,16 @@ function updateEditorSensitivity(): void {
: 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<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "")): void {
const revealDisabled = mutationControlsDisabled || sensitivity !== "secret";
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.toggleAttribute("disabled", revealDisabled);
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.toggleAttribute("disabled", revealDisabled);
}
function selectVariableForMutation(variableName: string, source: "table" | "input"): void {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
@@ -218,8 +227,10 @@ function renderMutationResult(result: ConfigEnvironmentChangeDto): void {
}
function setMutationControlsDisabled(disabled: boolean): void {
mutationControlsDisabled = disabled;
document.querySelector<HTMLButtonElement>("#setEnvironmentValue")?.toggleAttribute("disabled", disabled);
document.querySelector<HTMLButtonElement>("#removeEnvironmentValue")?.toggleAttribute("disabled", disabled);
updateRevealButtons();
}
function confirmEnvironmentRemoval(variableName: string): Promise<boolean> {
@@ -380,6 +391,14 @@ function bindEnvironmentActions(): void {
frontendTrace("main", "Environment remove value button clicked");
void removeEnvironmentValue();
});
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.addEventListener("click", () => {
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
requestSecretReveal(variableName, "effective");
});
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.addEventListener("click", () => {
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
requestSecretReveal(variableName, "dotenv");
});
document.querySelector<HTMLButtonElement>("#clearEnvironmentEditor")?.addEventListener("click", () => {
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
@@ -396,6 +415,7 @@ function bindEnvironmentActions(): void {
}
export function initializeEnvironmentPanel(): void {
initializeSecretReveal();
bindEnvironmentActions();
updateEditorSensitivity();
void refreshEnvironmentReport();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 7
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -11,6 +11,7 @@ import { initializeDocumentsPanel } from "./documents";
import { initializeEnvironmentPanel } from "./environment";
import { initializeProfilesPanel } from "./profiles";
import { invokeKsp } from "./invoke";
import { clearTransientSecretReveal } from "./secret_reveal";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
@@ -45,6 +46,9 @@ function isViewId(value: string): value is ViewId {
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
if (viewId !== "environment") {
clearTransientSecretReveal();
}
if (source === "user") {
frontendDebug("main", "Main navigation activated", { viewId });
}

View File

@@ -0,0 +1,206 @@
// file: crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
// version: 1
import { Modal } from "bootstrap";
import type { SecretRevealRequestDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealRequestDto";
import type { SecretRevealResponseDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealResponseDto";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
export type SecretRevealSource = "effective" | "dotenv";
let pendingRequest: SecretRevealRequestDto | null = null;
let revealInFlight = false;
let revealGeneration = 0;
function sourceLabel(source: SecretRevealSource): string {
return source === "effective" ? "effective (process prioritaire sur .env)" : ".env persisté";
}
function modalElement(): HTMLElement | null {
return document.querySelector<HTMLElement>("#secretRevealModal");
}
function clearSecretValue(): void {
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
if (input) {
input.value = "";
input.type = "password";
}
if (absent) {
absent.hidden = true;
}
if (result) {
result.hidden = true;
}
if (confirmation) {
confirmation.hidden = false;
}
if (confirm) {
confirm.hidden = false;
confirm.disabled = false;
}
if (toggle) {
toggle.hidden = true;
toggle.textContent = "Afficher";
}
}
function renderRevealResponse(response: SecretRevealResponseDto): void {
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
if (confirmation) {
confirmation.hidden = true;
}
if (confirm) {
confirm.hidden = true;
}
if (result) {
result.hidden = false;
}
if (response.value === null) {
if (input) {
input.value = "";
input.hidden = true;
}
if (absent) {
absent.hidden = false;
}
if (toggle) {
toggle.hidden = true;
}
} else {
if (input) {
input.hidden = false;
input.type = "password";
input.value = response.value;
}
if (absent) {
absent.hidden = true;
}
if (toggle) {
toggle.hidden = false;
toggle.textContent = "Afficher";
}
}
frontendDebug("main", "Privileged Secret reveal rendered", {
variableName: response.variableName,
source: response.source,
valuePresent: response.value !== null,
});
}
async function executeReveal(): Promise<void> {
if (!pendingRequest || revealInFlight) {
return;
}
const request = pendingRequest;
const requestGeneration = revealGeneration;
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
revealInFlight = true;
if (confirm) {
confirm.disabled = true;
}
if (status) {
status.textContent = "Reveal privilégié en cours via ksp-config-lib...";
}
frontendDebug("main", "Privileged Secret reveal requested", { variableName: request.variableName, source: request.source });
try {
const response = await invokeKsp<SecretRevealResponseDto>("main", "reveal_environment_value", { request });
if (requestGeneration !== revealGeneration || !pendingRequest) {
frontendDebug("main", "Privileged Secret reveal response discarded after modal state changed", {
variableName: request.variableName,
source: request.source,
});
return;
}
renderRevealResponse(response);
if (status) {
status.textContent = response.value === null ? "Aucune valeur n'existe dans la source demandée." : "Valeur révélée temporairement. Elle sera effacée à la fermeture.";
}
} catch {
if (status) {
status.textContent = "Le reveal a été refusé ou a échoué.";
}
if (confirm) {
confirm.disabled = false;
}
} finally {
if (requestGeneration === revealGeneration) {
revealInFlight = false;
}
}
}
function toggleRevealVisibility(): void {
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
if (!input || !toggle || input.hidden) {
return;
}
const show = input.type === "password";
input.type = show ? "text" : "password";
toggle.textContent = show ? "Masquer" : "Afficher";
frontendDebug("main", "Transient Secret reveal visibility changed", { visible: show });
}
export function requestSecretReveal(variableName: string, source: SecretRevealSource): void {
const modal = modalElement();
const variable = document.querySelector<HTMLElement>("#secretRevealVariable");
const sourceElement = document.querySelector<HTMLElement>("#secretRevealSource");
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
if (!modal || !variable || !sourceElement || !status) {
frontendDebug("main", "Privileged Secret reveal modal is unavailable", { variableName, source });
return;
}
revealGeneration += 1;
clearSecretValue();
pendingRequest = { variableName, source };
variable.textContent = variableName;
sourceElement.textContent = sourceLabel(source);
status.textContent = "Confirmation explicite requise avant tout accès à la valeur réelle.";
frontendDebug("main", "Privileged Secret reveal confirmation opened", { variableName, source });
Modal.getOrCreateInstance(modal).show();
}
export function clearTransientSecretReveal(): void {
const modal = modalElement();
const variableName = pendingRequest?.variableName ?? null;
revealGeneration += 1;
clearSecretValue();
pendingRequest = null;
revealInFlight = false;
if (modal) {
Modal.getOrCreateInstance(modal).hide();
}
frontendTrace("main", "Transient Secret reveal state cleared", { variableName });
}
export function initializeSecretReveal(): void {
const modal = modalElement();
document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction")?.addEventListener("click", () => {
void executeReveal();
});
document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility")?.addEventListener("click", () => {
toggleRevealVisibility();
});
modal?.addEventListener("hidden.bs.modal", () => {
const variableName = pendingRequest?.variableName ?? null;
revealGeneration += 1;
clearSecretValue();
pendingRequest = null;
revealInFlight = false;
frontendDebug("main", "Privileged Secret reveal closed and value cleared", { variableName });
});
frontendTrace("main", "Privileged Secret reveal handlers installed");
}