v0.2.6-pre.012

This commit is contained in:
2026-08-21 16:22:16 +02:00
parent 8ba554306a
commit ecbd6b746d
21 changed files with 634 additions and 94 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 12
// version: 13
import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -10,9 +10,11 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts";
import type { CommandErrorDto } from "./bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts";
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
import type { WalletExportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportRequestDto.ts";
import type { WalletExportResultDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportResultDto.ts";
import type { WalletImportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts";
import type { WalletImportSourceRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts";
import type { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferFormatDto.ts";
import type { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_transfer/WalletTransferFormatDto.ts";
import type { WalletTransferInspectionDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts";
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";
@@ -54,6 +56,7 @@ let authorizedWalletProjection: WalletAuthorizedDto | null = null;
let configuredSecretCandidateCount = 0;
let deleteNoteModal: Modal | null = null;
let disableViewModal: Modal | null = null;
let exportWalletModal: Modal | null = null;
let recreateViewModal: Modal | null = null;
let pendingDeleteNoteId: string | null = null;
let stagedImportSource: WalletTransferInspectionDto | null = null;
@@ -187,7 +190,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.011-strong-view-administration" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.012-owner-transfer-export" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -227,6 +230,7 @@ function updateSessionActions(): void {
updateOwnerMetadataActions();
updateSecurityRotationActions();
updateStrongViewSecurityActions();
updateWalletExportActions();
}
function updateOwnerMetadataActions(): void {
@@ -259,6 +263,13 @@ function updateStrongViewSecurityActions(): void {
});
}
function updateWalletExportActions(): void {
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
document.querySelectorAll<HTMLSelectElement | HTMLButtonElement>("[data-owner-export-control]").forEach(control => {
control.disabled = !ownerOpen;
});
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
@@ -392,6 +403,7 @@ function clearAuthorizedProjection(): void {
clearUnlockSensitiveInputs();
setText("#rotationStatus", "Session OWNER requise.");
setText("#viewSecurityStatus", "Session OWNER requise pour administrer fortement VIEW.");
setText("#walletExportStatus", "Session OWNER requise. Aucun export texte ou clipboard nest disponible.");
}
function clearSelectedWallet(): void {
@@ -430,6 +442,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
setText("#viewSecurityStatus", "Unlock OWNER requis pour administrer fortement VIEW.");
setText("#walletExportStatus", "Unlock OWNER requis pour exporter la keypair Solana.");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", {
configuredSecretCandidateCount,
@@ -481,6 +494,12 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
: "OWNER ouvert : VIEW disabled. Strong recreate disponible."
: "VIEW ouvert : ladministration forte VIEW est réservée à OWNER.",
);
setText(
"#walletExportStatus",
wallet.capability === "owner"
? "OWNER ouvert : export fichier Solana CLI JSON ou Base58 disponible via save picker natif."
: "VIEW ouvert : export de keypair interdit ; OWNER requis.",
);
updateSessionActions();
frontendDebug("main", "Authorized Wallet session rendered", {
capability: wallet.capability,
@@ -1244,6 +1263,67 @@ function bindStrongViewSecurityActions(): void {
frontendTrace("main", "Strong VIEW security action handlers installed");
}
async function runWalletExport(walletId: string, format: WalletTransferFormatDto): Promise<void> {
activeSessionState = "privileged_operation";
setText("#currentWalletState", "OWNER export…");
setText("#walletExportStatus", "Save picker natif / export secret en cours…");
updateSessionActions();
frontendDebug("main", "Wallet OWNER transfer export requested", { format, walletId });
const request: WalletExportRequestDto = { format };
try {
const result = await invokeKsp<WalletExportResultDto | null>("main", "export_wallet_transfer", { request });
if (activeWalletId !== walletId || authorizedWalletProjection?.capability !== "owner") {
frontendWarn("main", "Stale Wallet export response ignored", { format, walletId });
return;
}
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
if (result === null) {
setText("#walletExportStatus", "Export annulé dans le save picker natif ; aucun fichier créé.");
updateSessionActions();
frontendDebug("main", "Wallet OWNER transfer export cancelled", { format, walletId });
return;
}
setText("#walletExportStatus", `Export ${importFormatLabel(result.format)} créé : ${result.destinationName}. Le chemin complet et le contenu secret nont pas traversé IPC.`);
updateSessionActions();
frontendInfo("main", "Wallet OWNER transfer export completed", { format: result.format, walletId });
} catch {
if (activeWalletId === walletId && authorizedWalletProjection?.capability === "owner") {
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
setText("#walletExportStatus", "Export refusé ou impossible ; aucun écrasement nest autorisé et la session OWNER reste ouverte.");
updateSessionActions();
}
frontendWarn("main", "Wallet OWNER transfer export failed", { format, walletId });
}
}
function bindWalletExportActions(): void {
const modalElement = document.querySelector<HTMLElement>("#exportWalletKeypairModal");
if (modalElement) {
exportWalletModal = new Modal(modalElement);
}
document.querySelector<HTMLButtonElement>("#exportWalletKeypair")?.addEventListener("click", () => {
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
setText("#walletExportStatus", "Session OWNER requise.");
return;
}
frontendDebug("main", "Wallet OWNER transfer export confirmation opened", { walletId: activeWalletId });
exportWalletModal?.show();
});
document.querySelector<HTMLButtonElement>("#confirmExportWalletKeypair")?.addEventListener("click", () => {
const walletId = activeWalletId;
const formatControl = document.querySelector<HTMLSelectElement>("#walletExportFormat");
exportWalletModal?.hide();
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !formatControl) {
return;
}
const format = formatControl.value as WalletTransferFormatDto;
void runWalletExport(walletId, format);
});
frontendTrace("main", "Wallet OWNER transfer export handlers installed");
}
function bindRotationActions(): void {
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
void rotateWalletPassword("owner");
@@ -1357,7 +1437,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/Transport résolus ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
setText("#shellStatus", "Config/Transport résolus ; import/export, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -1411,6 +1491,7 @@ async function initializeMain(): Promise<void> {
bindOwnerMetadataActions();
bindRotationActions();
bindStrongViewSecurityActions();
bindWalletExportActions();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");