v0.2.6-pre.006

This commit is contained in:
2026-08-21 10:01:11 +02:00
parent 63e711629f
commit a207cf7161
21 changed files with 1136 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 5
// version: 6
import "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -14,6 +14,7 @@ import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/w
import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts";
import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts";
import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts";
import type { WalletUnlockRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletUnlockRequestDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
import "../sass/main.scss";
@@ -22,7 +23,8 @@ import "../sass/main.scss";
installFrontendConsoleBridge("main");
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
type ActiveSessionState = "no_selection" | "locked" | "privileged_operation" | "view_open" | "owner_open";
type UnlockCapability = "view" | "owner";
const viewTitles: Record<ViewId, string> = {
dashboard: "Dashboard",
@@ -35,6 +37,8 @@ const viewTitles: Record<ViewId, string> = {
let activeWalletId: string | null = null;
let activeSessionState: ActiveSessionState = "no_selection";
let activeViewEnabled = false;
let configuredSecretCandidateCount = 0;
function isViewId(value: string): value is ViewId {
return value in viewTitles;
@@ -121,8 +125,8 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
row.dataset.inspectionStatus = entry.inspectionStatus;
const state = document.createElement("td");
if (entry.state === "locked") {
if (activeWalletId === entry.walletId && activeSessionState === "owner_open") {
appendIconText(state, "fa-lock-open", "OWNER");
if (activeWalletId === entry.walletId && (activeSessionState === "view_open" || activeSessionState === "owner_open")) {
appendIconText(state, "fa-lock-open", activeSessionState === "view_open" ? "VIEW" : "OWNER");
} else {
appendIconText(state, "fa-lock", "Locked");
}
@@ -165,7 +169,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.006-wallet-unlock" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -192,11 +196,33 @@ function updateSessionActions(): void {
const lock = document.querySelector<HTMLButtonElement>('[data-shell-action="lock"]');
const deselect = document.querySelector<HTMLButtonElement>('[data-shell-action="deselect"]');
if (lock) {
lock.disabled = activeSessionState !== "owner_open";
lock.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open";
}
if (deselect) {
deselect.disabled = activeSessionState === "no_selection";
deselect.disabled = activeSessionState === "no_selection" || activeSessionState === "privileged_operation";
}
updateUnlockActions();
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
const ownerManual = document.querySelector<HTMLButtonElement>("#unlockOwnerManual");
const viewConfigured = document.querySelector<HTMLButtonElement>("#unlockViewConfigured");
const ownerConfigured = document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured");
if (viewManual) {
viewManual.disabled = !locked || !activeViewEnabled;
}
if (ownerManual) {
ownerManual.disabled = !locked;
}
if (viewConfigured) {
viewConfigured.disabled = !locked || !activeViewEnabled || configuredSecretCandidateCount === 0;
}
if (ownerConfigured) {
ownerConfigured.disabled = !locked || configuredSecretCandidateCount === 0;
}
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
}
function clearCreateFormSensitiveInputs(): void {
@@ -218,16 +244,30 @@ function clearCreateFormSensitiveInputs(): void {
}
}
function clearUnlockSensitiveInputs(): void {
const viewPassword = document.querySelector<HTMLInputElement>("#unlockWalletViewPassword");
const ownerPassword = document.querySelector<HTMLInputElement>("#unlockWalletOwnerPassword");
if (viewPassword) {
viewPassword.value = "";
}
if (ownerPassword) {
ownerPassword.value = "";
}
}
function clearAuthorizedProjection(): void {
setText("#currentWalletPubkey", "—");
setText("#currentWalletAlias", "—");
setText("#currentWalletNotes", "—");
clearCreateFormSensitiveInputs();
clearUnlockSensitiveInputs();
}
function clearSelectedWallet(): void {
activeWalletId = null;
activeSessionState = "no_selection";
activeViewEnabled = false;
configuredSecretCandidateCount = 0;
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
@@ -236,6 +276,8 @@ function clearSelectedWallet(): void {
setText("#currentWalletState", "—");
setText("#currentWalletFormat", "—");
setText("#currentWalletView", "—");
setText("#currentWalletSecretCandidates", "0");
setText("#unlockStatus", "Sélectionner un wallet verrouillé.");
clearAuthorizedProjection();
updateSessionActions();
}
@@ -243,6 +285,8 @@ function clearSelectedWallet(): void {
function renderLockedWallet(wallet: LockedWalletDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "locked";
activeViewEnabled = wallet.viewEnabled;
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
clearAuthorizedProjection();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
@@ -252,26 +296,45 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
setText("#currentWalletState", "Locked");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
frontendDebug("main", "Locked Wallet selection rendered", {
configuredSecretCandidateCount,
formatVersion: wallet.formatVersion,
viewEnabled: wallet.viewEnabled,
walletId: wallet.walletId,
});
}
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "owner_open";
activeSessionState = wallet.capability === "view" ? "view_open" : "owner_open";
activeViewEnabled = wallet.viewEnabled;
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
clearUnlockSensitiveInputs();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
}
const capabilityLabel = wallet.capability === "view" ? "VIEW" : "OWNER";
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "OWNER open");
setText("#currentWalletState", `${capabilityLabel} open`);
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#currentWalletPubkey", wallet.pubkey);
setText("#currentWalletAlias", wallet.alias ?? "—");
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
updateSessionActions();
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
frontendDebug("main", "Authorized Wallet session rendered", {
capability: wallet.capability,
configuredSecretCandidateCount,
noteCount: wallet.notes.length,
viewEnabled: wallet.viewEnabled,
walletId: wallet.walletId,
});
}
async function selectWallet(walletId: string): Promise<void> {
@@ -395,6 +458,94 @@ function bindCreateWalletForm(): void {
frontendTrace("main", "Create Wallet form handlers installed");
}
function beginFrontendUnlock(capability: UnlockCapability, provider: "manual" | "configured"): string | null {
if (activeSessionState !== "locked" || !activeWalletId) {
return null;
}
const walletId = activeWalletId;
activeSessionState = "privileged_operation";
setText("#currentWalletState", `Unlock ${capability.toUpperCase()}`);
setText("#unlockStatus", provider === "manual" ? "Dérivation Argon2 en cours…" : `Tentative explicite sur ${configuredSecretCandidateCount} secret(s) configuré(s)…`);
updateSessionActions();
return walletId;
}
function restoreFrontendLockedAfterUnlockFailure(walletId: string): void {
if (activeWalletId !== walletId) {
return;
}
activeSessionState = "locked";
clearAuthorizedProjection();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3";
}
setText("#currentWalletState", "Locked");
setText("#unlockStatus", "Unlock refusé. Le wallet reste verrouillé.");
updateSessionActions();
}
async function unlockManual(capability: UnlockCapability): Promise<void> {
const input = document.querySelector<HTMLInputElement>(capability === "view" ? "#unlockWalletViewPassword" : "#unlockWalletOwnerPassword");
if (!input || input.value.length === 0) {
setText("#unlockStatus", "Password manuel requis.");
return;
}
const request: WalletUnlockRequestDto = { password: input.value };
const walletId = beginFrontendUnlock(capability, "manual");
if (!walletId) {
clearUnlockSensitiveInputs();
return;
}
const command = capability === "view" ? "unlock_wallet_view_manual" : "unlock_wallet_owner_manual";
frontendDebug("main", "Manual Wallet unlock requested", { capability, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command, { request });
clearUnlockSensitiveInputs();
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
clearUnlockSensitiveInputs();
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Manual Wallet unlock failed", { capability, walletId });
}
}
async function unlockConfigured(capability: UnlockCapability): Promise<void> {
const walletId = beginFrontendUnlock(capability, "configured");
if (!walletId) {
return;
}
const command = capability === "view" ? "unlock_wallet_view_configured" : "unlock_wallet_owner_configured";
frontendDebug("main", "Configured-secret Wallet unlock requested", { capability, configuredSecretCandidateCount, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command);
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Configured-secret Wallet unlock failed", { capability, configuredSecretCandidateCount, walletId });
}
}
function bindUnlockActions(): void {
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
void unlockManual("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerManual")?.addEventListener("click", () => {
void unlockManual("owner");
});
document.querySelector<HTMLButtonElement>("#unlockViewConfigured")?.addEventListener("click", () => {
void unlockConfigured("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured")?.addEventListener("click", () => {
void unlockConfigured("owner");
});
frontendTrace("main", "Wallet unlock action handlers installed");
}
async function lockCurrentWallet(): Promise<void> {
clearAuthorizedProjection();
try {
@@ -431,7 +582,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
setText("#runtimeShellPhase", status.shellPhase);
setText("#shellStatus", "Config résolue ; création et session Wallet prêtes.");
setText("#shellStatus", "Config résolue ; création et unlock VIEW/OWNER prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -477,6 +628,7 @@ async function initializeMain(): Promise<void> {
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindUnlockActions();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");