v0.2.6-pre.005

This commit is contained in:
2026-08-21 09:33:04 +02:00
parent 2132dfd884
commit 7aa38b0daf
19 changed files with 980 additions and 126 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 4
// version: 5
import "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -11,6 +11,9 @@ import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common
import type { LockedWalletDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts";
import type { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts";
import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts";
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 { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
import "../sass/main.scss";
@@ -19,6 +22,7 @@ import "../sass/main.scss";
installFrontendConsoleBridge("main");
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
const viewTitles: Record<ViewId, string> = {
dashboard: "Dashboard",
@@ -29,6 +33,9 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics",
};
let activeWalletId: string | null = null;
let activeSessionState: ActiveSessionState = "no_selection";
function isViewId(value: string): value is ViewId {
return value in viewTitles;
}
@@ -114,7 +121,11 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
row.dataset.inspectionStatus = entry.inspectionStatus;
const state = document.createElement("td");
if (entry.state === "locked") {
appendIconText(state, "fa-lock", "Locked");
if (activeWalletId === entry.walletId && activeSessionState === "owner_open") {
appendIconText(state, "fa-lock-open", "OWNER");
} else {
appendIconText(state, "fa-lock", "Locked");
}
} else {
appendIconText(state, "fa-triangle-exclamation", "Erreur");
}
@@ -154,7 +165,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.004-wallet-inventory" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -170,58 +181,105 @@ function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
frontendDebug("main", "Wallet inventory rendered", { entryCount: entries.length, invalidCount });
}
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
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";
}
if (deselect) {
deselect.disabled = activeSessionState === "no_selection";
}
}
function clearCreateFormSensitiveInputs(): void {
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#createWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#createWalletInitialNote");
if (ownerPassword) {
ownerPassword.value = "";
}
if (viewPassword) {
viewPassword.value = "";
}
if (alias) {
alias.value = "";
}
if (note) {
note.value = "";
}
}
function clearAuthorizedProjection(): void {
setText("#currentWalletPubkey", "—");
setText("#currentWalletAlias", "—");
setText("#currentWalletNotes", "—");
clearCreateFormSensitiveInputs();
}
function clearSelectedWallet(): void {
activeWalletId = null;
activeSessionState = "no_selection";
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
const state = document.querySelector<HTMLElement>("#currentWalletState");
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
const view = document.querySelector<HTMLElement>("#currentWalletView");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
}
if (filename) {
filename.textContent = "Aucun wallet sélectionné.";
}
if (state) {
state.textContent = "—";
}
if (format) {
format.textContent = "—";
}
if (view) {
view.textContent = "—";
}
setText("#currentWalletFilename", "Aucun wallet sélectionné.");
setText("#currentWalletState", "—");
setText("#currentWalletFormat", "—");
setText("#currentWalletView", "—");
clearAuthorizedProjection();
updateSessionActions();
}
function renderSelectedWallet(wallet: LockedWalletDto): void {
function renderLockedWallet(wallet: LockedWalletDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "locked";
clearAuthorizedProjection();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
const state = document.querySelector<HTMLElement>("#currentWalletState");
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
const view = document.querySelector<HTMLElement>("#currentWalletView");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3";
}
if (filename) {
filename.textContent = wallet.filename;
}
if (state) {
state.textContent = "Locked";
}
if (format) {
format.textContent = wallet.formatVersion.toString();
}
if (view) {
view.textContent = wallet.viewEnabled ? "enabled" : "disabled";
}
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "Locked");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
}
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "owner_open";
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
}
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "OWNER open");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletPubkey", wallet.pubkey);
setText("#currentWalletAlias", wallet.alias ?? "—");
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
updateSessionActions();
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
}
async function selectWallet(walletId: string): Promise<void> {
clearSelectedWallet();
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
renderSelectedWallet(wallet);
renderLockedWallet(wallet);
} catch {
clearSelectedWallet();
frontendWarn("main", "Locked Wallet selection failed", { walletId });
@@ -256,62 +314,124 @@ function bindWalletTableSelection(): void {
frontendTrace("main", "Wallet inventory row selection handler installed");
}
async function loadWalletInventory(command: "list_wallets" | "refresh_wallets"): Promise<void> {
async function loadWalletInventory(command: "list_wallets" | "refresh_wallets", clearSession: boolean): Promise<void> {
frontendDebug("main", "Wallet inventory load requested", { command });
const entries = await invokeKsp<WalletInventoryEntryDto[]>("main", command);
clearSelectedWallet();
if (clearSession) {
clearSelectedWallet();
}
renderWalletInventory(entries);
}
function createRequestFromForm(): WalletCreateRequestDto | null {
const filename = document.querySelector<HTMLInputElement>("#createWalletFilename");
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const enableView = document.querySelector<HTMLInputElement>("#createWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#createWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#createWalletInitialNote");
if (!filename || !ownerPassword || !enableView || !viewPassword || !alias || !note) {
return null;
}
const normalizedFilename = filename.value.trim();
if (!normalizedFilename.endsWith(".kspwallet") || normalizedFilename.length <= ".kspwallet".length || ownerPassword.value.length === 0) {
return null;
}
if (enableView.checked && viewPassword.value.length === 0) {
return null;
}
return {
alias: alias.value.length === 0 ? null : alias.value,
filename: normalizedFilename,
initialNote: note.value.length === 0 ? null : note.value,
ownerPassword: ownerPassword.value,
viewPassword: enableView.checked ? viewPassword.value : null,
};
}
function bindCreateWalletForm(): void {
const form = document.querySelector<HTMLFormElement>("#createWalletForm");
const enableView = document.querySelector<HTMLInputElement>("#createWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
if (enableView && viewPassword) {
enableView.addEventListener("change", () => {
viewPassword.disabled = !enableView.checked;
if (!enableView.checked) {
viewPassword.value = "";
}
frontendTrace("main", "Create Wallet VIEW option changed", { enabled: enableView.checked });
});
}
if (!form) {
return;
}
form.addEventListener("submit", event => {
event.preventDefault();
const request = createRequestFromForm();
if (!request) {
setText("#createWalletStatus", "Filename/passwords invalides ou incomplets.");
frontendWarn("main", "Create Wallet form validation rejected request");
return;
}
const requestedWalletId = request.filename;
const requestedViewEnabled = request.viewPassword !== null;
setText("#createWalletStatus", "Création en cours…");
frontendDebug("main", "Create Wallet request submitted", { walletId: requestedWalletId, viewEnabled: requestedViewEnabled });
clearSelectedWallet();
void invokeKsp<WalletAuthorizedDto>("main", "create_wallet", { request })
.then(async wallet => {
clearCreateFormSensitiveInputs();
renderAuthorizedWallet(wallet);
setText("#createWalletStatus", "Wallet créé ; session OWNER ouverte.");
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
})
.catch(() => {
clearCreateFormSensitiveInputs();
setText("#createWalletStatus", "Création impossible. Consulter le diagnostic et les logs.");
frontendWarn("main", "Create Wallet operation failed", { walletId: requestedWalletId });
});
});
frontendTrace("main", "Create Wallet form handlers installed");
}
async function lockCurrentWallet(): Promise<void> {
clearAuthorizedProjection();
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "lock_wallet");
renderLockedWallet(wallet);
await loadWalletInventory("list_wallets", false);
} catch {
clearSelectedWallet();
frontendWarn("main", "Wallet lock operation failed");
}
}
async function deselectCurrentWallet(): Promise<void> {
try {
const session = await invokeKsp<WalletSessionDto>("main", "deselect_wallet");
clearSelectedWallet();
frontendDebug("main", "Wallet session deselected", { state: session.state });
await loadWalletInventory("list_wallets", false);
} catch {
clearSelectedWallet();
frontendWarn("main", "Wallet deselect operation failed");
}
}
function renderRuntimeStatus(status: RuntimeStatusDto): void {
const version = document.querySelector<HTMLElement>("#runtimeVersion");
const compositeProfile = document.querySelector<HTMLElement>("#runtimeCompositeProfile");
const loggingProfile = document.querySelector<HTMLElement>("#runtimeLoggingProfile");
const walletProfile = document.querySelector<HTMLElement>("#runtimeWalletProfile");
const fallback = document.querySelector<HTMLElement>("#runtimeLoggingFallback");
const documents = document.querySelector<HTMLElement>("#runtimeConfigDocuments");
const walletsRoot = document.querySelector<HTMLElement>("#runtimeWalletsDirectory");
const walletsSubdirectory = document.querySelector<HTMLElement>("#runtimeWalletsSubdirectory");
const effectiveWalletsDirectory = document.querySelector<HTMLElement>("#runtimeEffectiveWalletsDirectory");
const walletDirectoryCreated = document.querySelector<HTMLElement>("#runtimeWalletDirectoryCreated");
const phase = document.querySelector<HTMLElement>("#runtimeShellPhase");
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (version) {
version.textContent = status.applicationVersion;
}
if (compositeProfile) {
compositeProfile.textContent = status.activeCompositeProfile;
}
if (loggingProfile) {
loggingProfile.textContent = status.activeLoggingProfile ?? "fallback transitoire";
}
if (walletProfile) {
walletProfile.textContent = status.activeWalletProfile;
}
if (fallback) {
fallback.textContent = status.fallbackLoggingActive ? "oui" : "non";
}
if (documents) {
documents.textContent = status.configDocumentCount.toString();
}
if (walletsRoot) {
walletsRoot.textContent = status.walletsDirectory;
}
if (walletsSubdirectory) {
walletsSubdirectory.textContent = status.walletsSubdirectory ?? "— (racine globale)";
}
if (effectiveWalletsDirectory) {
effectiveWalletsDirectory.textContent = status.effectiveWalletsDirectory;
}
if (walletDirectoryCreated) {
walletDirectoryCreated.textContent = status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent";
}
if (phase) {
phase.textContent = status.shellPhase;
}
if (shellStatus) {
shellStatus.textContent = "Config résolue ; inventaire Wallet prêt.";
}
setText("#runtimeVersion", status.applicationVersion);
setText("#runtimeCompositeProfile", status.activeCompositeProfile);
setText("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback transitoire");
setText("#runtimeWalletProfile", status.activeWalletProfile);
setText("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
setText("#runtimeConfigDocuments", status.configDocumentCount.toString());
setText("#runtimeWalletsDirectory", status.walletsDirectory);
setText("#runtimeWalletsSubdirectory", status.walletsSubdirectory ?? "— (racine globale)");
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.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -331,10 +451,19 @@ function bindShellActions(): void {
button.addEventListener("click", () => {
const action = button.dataset.shellAction ?? "unknown";
frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled });
if (action === "refresh-wallets" && !button.disabled) {
void loadWalletInventory("refresh_wallets").catch(() => {
if (button.disabled) {
return;
}
if (action === "refresh-wallets") {
clearSelectedWallet();
void loadWalletInventory("refresh_wallets", true).catch(() => {
clearSelectedWallet();
frontendWarn("main", "Wallet inventory refresh failed");
});
} else if (action === "lock") {
void lockCurrentWallet();
} else if (action === "deselect") {
void deselectCurrentWallet();
}
});
});
@@ -347,17 +476,16 @@ async function initializeMain(): Promise<void> {
bindFrontendInteractions();
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");
try {
await loadRuntimeStatus();
await loadWalletInventory("list_wallets");
await loadWalletInventory("list_wallets", false);
} catch {
renderWalletInventory([]);
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (shellStatus) {
shellStatus.textContent = "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.";
}
setText("#shellStatus", "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.");
frontendWarn("main", "Wallet Desk startup data load failed");
}
}