// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // version: 16 import { Modal } from "bootstrap"; import DataTable from "datatables.net-bs5"; import "datatables.net-select-bs5"; import ResizeObserver from "resize-observer-polyfill"; import "simplebar"; 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_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"; import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts"; import type { WalletAliasUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletAliasUpdateRequestDto.ts"; import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteAddRequestDto.ts"; import type { WalletNoteDeleteRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts"; import type { WalletNoteUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.ts"; import type { WalletPasswordRotationRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletPasswordRotationRequestDto.ts"; import type { WalletViewSecurityStatusDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletViewSecurityStatusDto.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 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"; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; installFrontendConsoleBridge("main"); type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics"; type ActiveSessionState = "no_selection" | "locked" | "privileged_operation" | "view_open" | "owner_open"; type UnlockCapability = "view" | "owner"; const viewTitles: Record = { dashboard: "Dashboard", wallets: "Wallets", "create-import": "Create / Import", details: "Details", security: "Security", diagnostics: "Diagnostics", }; let activeWalletId: string | null = null; let activeSessionState: ActiveSessionState = "no_selection"; let activeViewEnabled = false; 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; function isViewId(value: string): value is ViewId { return value in viewTitles; } function activateView(viewId: ViewId, source: "startup" | "user"): void { if (source === "user") { frontendDebug("main", "Wallet Desk navigation activated", { viewId }); } const title = viewTitles[viewId]; const headerTitle = document.querySelector("#headerViewTitle"); const viewTitle = document.querySelector("#viewTitle"); if (headerTitle) { headerTitle.textContent = title; } if (viewTitle) { viewTitle.textContent = title; } document.title = `Wallet Desk — ${title}`; document.querySelectorAll("[data-view]").forEach(button => { const active = button.dataset.view === viewId; button.classList.toggle("active", active); button.setAttribute("aria-current", active ? "page" : "false"); }); document.querySelectorAll("[data-view-panel]").forEach(panel => { panel.hidden = panel.dataset.viewPanel !== viewId; }); frontendTrace("main", "Wallet Desk view DOM updated", { viewId, source }); } function bindFrontendInteractions(): void { document.addEventListener( "click", event => { const source = event.target; if (!(source instanceof Element)) { return; } const control = source.closest('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view], [data-shell-action]'); if (!control) { return; } frontendTrace("main", "Frontend control clicked", { controlId: control.id || null, controlType: control instanceof HTMLButtonElement ? control.type : null, disabled: control instanceof HTMLButtonElement ? control.disabled : control.getAttribute("aria-disabled") === "true", role: control.getAttribute("role"), shellAction: control.dataset.shellAction ?? null, tagName: control.tagName.toLowerCase(), viewId: control.dataset.view ?? null, }); }, true, ); frontendTrace("main", "Frontend control interaction logger installed"); } function bindNavigation(): void { document.querySelectorAll("[data-view]").forEach(button => { button.addEventListener("click", () => { const requestedView = button.dataset.view; frontendTrace("main", "Wallet Desk navigation clicked", { requestedView: requestedView ?? null }); if (requestedView && isViewId(requestedView)) { activateView(requestedView, "user"); } }); }); frontendTrace("main", "Wallet Desk navigation handlers installed"); } function appendIconText(cell: HTMLTableCellElement, iconClass: string, text: string): void { const icon = document.createElement("i"); icon.className = `fa-solid ${iconClass} me-2`; icon.setAttribute("aria-hidden", "true"); const label = document.createElement("span"); label.textContent = text; cell.append(icon, label); } function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowElement { const row = document.createElement("tr"); row.dataset.walletId = entry.walletId; row.dataset.walletSelectable = entry.inspectionStatus === "valid" ? "true" : "false"; row.dataset.inspectionStatus = entry.inspectionStatus; const state = document.createElement("td"); if (entry.state === "locked") { 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"); } } else { appendIconText(state, "fa-triangle-exclamation", "Erreur"); } const filename = document.createElement("td"); filename.textContent = entry.filename; const format = document.createElement("td"); format.textContent = entry.formatVersion === null ? "—" : entry.formatVersion.toString(); const view = document.createElement("td"); view.textContent = entry.viewEnabled === null ? "—" : entry.viewEnabled ? "enabled" : "disabled"; const inspection = document.createElement("td"); if (entry.inspectionStatus === "valid") { inspection.textContent = "Valide"; } else if (entry.diagnostic) { inspection.textContent = `Invalide — ${entry.diagnostic.domain}.${entry.diagnostic.code}`; inspection.title = entry.diagnostic.message; } else { inspection.textContent = "Invalide"; } row.append(state, filename, format, view, inspection); return row; } function initializeWalletTable(): void { if (DataTable.isDataTable("#walletInventoryTable")) { new DataTable("#walletInventoryTable").destroy(); } new DataTable("#walletInventoryTable", { order: [[1, "asc"]], pageLength: 10, select: { selector: "tbody tr[data-wallet-selectable='true'] td", style: "single", }, language: { emptyTable: "Aucun wallet .kspwallet disponible.", search: "Filtrer :", zeroRecords: "Aucun wallet correspondant.", }, }); frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.014-desktop-polish" }); } function renderWalletInventory(entries: WalletInventoryEntryDto[]): void { if (DataTable.isDataTable("#walletInventoryTable")) { new DataTable("#walletInventoryTable").destroy(); } const body = document.querySelector("#walletInventoryBody"); if (body) { body.replaceChildren(...entries.map(entry => renderWalletInventoryRow(entry))); } initializeWalletTable(); const invalidCount = entries.filter(entry => entry.inspectionStatus === "invalid").length; frontendDebug("main", "Wallet inventory rendered", { entryCount: entries.length, invalidCount }); } function setText(selector: string, value: string): void { const element = document.querySelector(selector); if (element) { element.textContent = value; } } function updateSessionActions(): void { const refreshBalance = document.querySelector("#refreshWalletBalance"); const lock = document.querySelector('[data-shell-action="lock"]'); const deselect = document.querySelector('[data-shell-action="deselect"]'); if (lock) { lock.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open"; } if (deselect) { deselect.disabled = activeSessionState === "no_selection" || activeSessionState === "privileged_operation"; } if (refreshBalance) { refreshBalance.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open"; } updateUnlockActions(); updateOwnerMetadataActions(); updateSecurityRotationActions(); updateStrongViewSecurityActions(); updateWalletExportActions(); } function updateOwnerMetadataActions(): void { const ownerOpen = activeSessionState === "owner_open"; document.querySelectorAll("[data-owner-metadata-control]").forEach(control => { control.disabled = !ownerOpen; }); } function updateSecurityRotationActions(): void { const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner"; const viewOpen = activeSessionState === "view_open" && authorizedWalletProjection?.capability === "view"; document.querySelectorAll("[data-owner-rotation-control]").forEach(control => { control.disabled = !ownerOpen; }); const viewControls = document.querySelectorAll("[data-view-rotation-control]"); viewControls.forEach(control => { control.disabled = (!ownerOpen && !viewOpen) || !activeViewEnabled; }); } function updateStrongViewSecurityActions(): void { const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner"; const disable = document.querySelector("#disableWalletView"); if (disable) { disable.disabled = !ownerOpen || !activeViewEnabled; } document.querySelectorAll("[data-view-security-recreate-control]").forEach(control => { control.disabled = !ownerOpen; }); } function updateWalletExportActions(): void { const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner"; document.querySelectorAll("[data-owner-export-control]").forEach(control => { control.disabled = !ownerOpen; }); } function updateUnlockActions(): void { const locked = activeSessionState === "locked"; const viewManual = document.querySelector("#unlockViewManual"); const ownerManual = document.querySelector("#unlockOwnerManual"); const viewConfigured = document.querySelector("#unlockViewConfigured"); const ownerConfigured = document.querySelector("#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 importFormatLabel(format: WalletTransferFormatDto): string { return format === "solana_cli_json" ? "Solana CLI JSON" : "Keypair Base58 complet"; } function updateImportActions(): void { const submit = document.querySelector("#importWalletSubmit"); if (submit) { submit.disabled = stagedImportSource === null; } } function clearImportSourceProjection(): void { stagedImportSource = null; setText("#importSourceName", "—"); setText("#importSourceFormat", "—"); setText("#importSourcePubkey", "—"); updateImportActions(); } function clearImportSensitiveInputs(): void { const ownerPassword = document.querySelector("#importWalletOwnerPassword"); const viewPassword = document.querySelector("#importWalletViewPassword"); const alias = document.querySelector("#importWalletAlias"); const note = document.querySelector("#importWalletInitialNote"); if (ownerPassword) { ownerPassword.value = ""; } if (viewPassword) { viewPassword.value = ""; } if (alias) { alias.value = ""; } if (note) { note.value = ""; } } function clearCreateFormSensitiveInputs(): void { const ownerPassword = document.querySelector("#createWalletOwnerPassword"); const viewPassword = document.querySelector("#createWalletViewPassword"); const alias = document.querySelector("#createWalletAlias"); const note = document.querySelector("#createWalletInitialNote"); if (ownerPassword) { ownerPassword.value = ""; } if (viewPassword) { viewPassword.value = ""; } if (alias) { alias.value = ""; } if (note) { note.value = ""; } } function clearUnlockSensitiveInputs(): void { const viewPassword = document.querySelector("#unlockWalletViewPassword"); const ownerPassword = document.querySelector("#unlockWalletOwnerPassword"); if (viewPassword) { viewPassword.value = ""; } if (ownerPassword) { ownerPassword.value = ""; } } function clearRotationSensitiveInputs(): void { for (const selector of ["#rotateOwnerPassword", "#rotateOwnerPasswordConfirm", "#rotateViewPassword", "#rotateViewPasswordConfirm"]) { const input = document.querySelector(selector); if (input) { input.value = ""; } } } function clearStrongViewSensitiveInputs(): void { for (const selector of ["#recreateViewPassword", "#recreateViewPasswordConfirm"]) { const input = document.querySelector(selector); if (input) { input.value = ""; } } } function clearBalanceProjection(): void { setText("#currentWalletBalanceSol", "—"); setText("#detailsBalanceLamports", "—"); setText("#detailsBalanceSol", "—"); setText("#detailsBalanceSlot", "—"); setText("#detailsBalanceApiVersion", "—"); setText("#detailsBalanceTransport", "—"); setText("#balanceStatus", "Unlock VIEW ou OWNER pour interroger getBalance."); } function clearAuthorizedProjection(): void { authorizedWalletProjection = null; clearOwnerMetadataProjection(); setText("#currentWalletPubkey", "—"); setText("#currentWalletAlias", "—"); setText("#currentWalletNotes", "—"); setText("#detailsWalletPubkey", "—"); setText("#detailsWalletAlias", "—"); setText("#detailsWalletNotes", "—"); clearBalanceProjection(); clearCreateFormSensitiveInputs(); clearRotationSensitiveInputs(); clearStrongViewSensitiveInputs(); 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 n’est disponible."); } function clearSelectedWallet(): void { activeWalletId = null; activeSessionState = "no_selection"; activeViewEnabled = false; configuredSecretCandidateCount = 0; const icon = document.querySelector("#currentWalletIcon"); if (icon) { icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary"; } setText("#currentWalletFilename", "Aucun wallet sélectionné."); setText("#currentWalletState", "—"); setText("#currentWalletFormat", "—"); setText("#currentWalletView", "—"); setText("#currentWalletSecretCandidates", "0"); setText("#unlockStatus", "Sélectionner un wallet verrouillé."); clearAuthorizedProjection(); updateSessionActions(); } function renderLockedWallet(wallet: LockedWalletDto): void { activeWalletId = wallet.walletId; activeSessionState = "locked"; activeViewEnabled = wallet.viewEnabled; configuredSecretCandidateCount = wallet.configuredSecretCandidateCount; clearAuthorizedProjection(); const icon = document.querySelector("#currentWalletIcon"); if (icon) { icon.className = "fa-solid fa-lock fa-2x mb-3"; } setText("#currentWalletFilename", wallet.filename); 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."); 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, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled, walletId: wallet.walletId, }); } function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void { activeWalletId = wallet.walletId; authorizedWalletProjection = wallet; activeSessionState = wallet.capability === "view" ? "view_open" : "owner_open"; activeViewEnabled = wallet.viewEnabled; configuredSecretCandidateCount = wallet.configuredSecretCandidateCount; clearUnlockSensitiveInputs(); const icon = document.querySelector("#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", `${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 ?? "—"); const notesText = wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "); setText("#currentWalletNotes", notesText); setText("#detailsWalletPubkey", wallet.pubkey); setText("#detailsWalletAlias", wallet.alias ?? "—"); setText("#detailsWalletNotes", notesText); renderOwnerMetadataProjection(wallet); clearBalanceProjection(); setText("#balanceStatus", "Session autorisée. Refresh balance appelle getBalance avec la Pubkey détenue par Rust."); setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`); setText( "#rotationStatus", wallet.capability === "owner" ? "OWNER ouvert. Rotation OWNER et administration VIEW disponibles ; les secrets Config restent inchangés." : "VIEW ouvert. Self-rotation VIEW disponible ; OWNER reste inaccessible.", ); setText( "#viewSecurityStatus", wallet.capability === "owner" ? wallet.viewEnabled ? "OWNER ouvert : VIEW enabled. Strong disable ou strong recreate disponibles." : "OWNER ouvert : VIEW disabled. Strong recreate disponible." : "VIEW ouvert : l’administration 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, configuredSecretCandidateCount, noteCount: wallet.notes.length, viewEnabled: wallet.viewEnabled, walletId: wallet.walletId, }); } function clearOwnerMetadataProjection(): void { pendingDeleteNoteId = null; const alias = document.querySelector("#ownerWalletAlias"); const newNote = document.querySelector("#ownerNewNote"); const list = document.querySelector("#ownerNotesList"); if (alias) { alias.value = ""; } if (newNote) { newNote.value = ""; } if (list) { const empty = document.createElement("span"); empty.className = "text-body-secondary"; empty.textContent = "Ouvrir OWNER pour administrer les notes."; list.replaceChildren(empty); } setText("#ownerMetadataStatus", "Session OWNER requise."); } function renderOwnerMetadataProjection(wallet: WalletAuthorizedDto): void { const alias = document.querySelector("#ownerWalletAlias"); const list = document.querySelector("#ownerNotesList"); if (alias) { alias.value = wallet.alias ?? ""; } if (list) { if (wallet.notes.length === 0) { const empty = document.createElement("span"); empty.className = "text-body-secondary"; empty.textContent = wallet.capability === "owner" ? "Aucune note protégée." : "Les notes sont visibles en VIEW mais modifiables uniquement en OWNER."; list.replaceChildren(empty); } else { const rows = wallet.notes.map(note => { const row = document.createElement("div"); row.className = "border rounded p-2"; const textarea = document.createElement("textarea"); textarea.className = "form-control mb-2"; textarea.rows = 2; textarea.value = note.text; textarea.disabled = wallet.capability !== "owner"; textarea.autocomplete = "off"; textarea.dataset.ownerMetadataControl = "true"; const actions = document.createElement("div"); actions.className = "d-flex gap-2 justify-content-end"; const save = document.createElement("button"); save.className = "btn btn-outline-primary btn-sm"; save.type = "button"; save.textContent = "Save note"; save.disabled = wallet.capability !== "owner"; save.dataset.ownerMetadataControl = "true"; save.addEventListener("click", () => { const request: WalletNoteUpdateRequestDto = { noteId: note.id, text: textarea.value }; void runOwnerMetadataMutation("update_wallet_note", { request }, "Mise à jour de la note…", "Note mise à jour."); }); const remove = document.createElement("button"); remove.className = "btn btn-outline-danger btn-sm"; remove.type = "button"; remove.textContent = "Delete"; remove.disabled = wallet.capability !== "owner"; remove.dataset.ownerMetadataControl = "true"; remove.addEventListener("click", () => { pendingDeleteNoteId = note.id; deleteNoteModal?.show(); frontendDebug("main", "OWNER note delete confirmation opened", { walletId: wallet.walletId }); }); actions.append(save, remove); row.append(textarea, actions); return row; }); list.replaceChildren(...rows); } } setText("#ownerMetadataStatus", wallet.capability === "owner" ? "OWNER ouvert : metadata modifiable." : "VIEW ouvert : metadata en lecture seule."); updateOwnerMetadataActions(); } function isWalletStateConflict(caughtError: unknown): boolean { if (typeof caughtError !== "object" || caughtError === null) { return false; } const error = caughtError as Partial; return error.domain === "wallet" && error.code === "state_conflict"; } async function recoverOwnerStateConflict(walletId: string): Promise { clearAuthorizedProjection(); clearBalanceProjection(); const request: WalletSelectionRequestDto = { walletId }; try { const wallet = await invokeKsp("main", "select_wallet", { request }); renderLockedWallet(wallet); setText("#ownerMetadataStatus", "Le fichier wallet a changé hors de cette session. Le handle OWNER stale a été purgé ; unlock OWNER requis avant toute nouvelle mutation."); setText("#rotationStatus", "Conflit d’état : rotation interrompue, wallet reverrouillé et réautorisation OWNER obligatoire."); setText("#viewSecurityStatus", "Conflit d’état : administration VIEW interrompue, handle OWNER purgé et réautorisation OWNER obligatoire."); setText("#unlockStatus", "Conflit d’état détecté : wallet réinspecté et reverrouillé. Réautoriser OWNER explicitement."); await loadWalletInventory("list_wallets", false); activateView("security", "user"); frontendWarn("main", "Wallet state conflict forced OWNER reauthorization", { walletId }); } catch { clearSelectedWallet(); setText("#ownerMetadataStatus", "Conflit d’état détecté et réinspection impossible. Resélectionner le wallet depuis l’inventaire."); setText("#rotationStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué."); setText("#viewSecurityStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué."); frontendWarn("main", "Wallet state conflict recovery failed", { walletId }); } } async function recoverRotationStateConflict(walletId: string, capability: UnlockCapability): Promise { clearAuthorizedProjection(); clearBalanceProjection(); const request: WalletSelectionRequestDto = { walletId }; try { const wallet = await invokeKsp("main", "select_wallet", { request }); renderLockedWallet(wallet); setText("#rotationStatus", `Conflit d’état : rotation ${capability.toUpperCase()} interrompue, wallet reverrouillé.`); setText("#unlockStatus", `Conflit d’état détecté : réautoriser ${capability.toUpperCase()} explicitement avant une nouvelle rotation.`); await loadWalletInventory("list_wallets", false); activateView("security", "user"); frontendWarn("main", "Wallet state conflict forced credential reauthorization", { capability, walletId }); } catch { clearSelectedWallet(); setText("#rotationStatus", "Conflit d’état détecté ; la réinspection du wallet a échoué."); frontendWarn("main", "Wallet credential rotation state conflict recovery failed", { capability, walletId }); } } async function runOwnerMetadataMutation( command: "add_wallet_note" | "delete_wallet_note" | "update_wallet_alias" | "update_wallet_note", args: Record, pendingMessage: string, successMessage: string, ): Promise { if (activeSessionState !== "owner_open" || !activeWalletId || authorizedWalletProjection?.capability !== "owner") { setText("#ownerMetadataStatus", "Session OWNER requise."); return false; } const walletId = activeWalletId; activeSessionState = "privileged_operation"; setText("#currentWalletState", "OWNER mutation…"); setText("#ownerMetadataStatus", pendingMessage); updateSessionActions(); frontendDebug("main", "OWNER metadata mutation requested", { command, walletId }); try { const wallet = await invokeKsp("main", command, args); if (activeWalletId !== walletId) { frontendWarn("main", "Stale OWNER metadata response ignored", { command, walletId }); return false; } renderAuthorizedWallet(wallet); setText("#ownerMetadataStatus", successMessage); await loadWalletInventory("list_wallets", false); return true; } catch (caughtError) { if (isWalletStateConflict(caughtError)) { await recoverOwnerStateConflict(walletId); return false; } if (activeWalletId === walletId) { activeSessionState = "owner_open"; setText("#currentWalletState", "OWNER open"); setText("#ownerMetadataStatus", "Mutation refusée ; la session OWNER reste ouverte."); updateSessionActions(); } frontendWarn("main", "OWNER metadata mutation failed", { command, walletId }); return false; } } function bindOwnerMetadataActions(): void { const modalElement = document.querySelector("#deleteWalletNoteModal"); if (modalElement) { deleteNoteModal = new Modal(modalElement); } document.querySelector("#saveOwnerAlias")?.addEventListener("click", () => { const alias = document.querySelector("#ownerWalletAlias"); if (!alias) { return; } const request: WalletAliasUpdateRequestDto = { alias: alias.value.length === 0 ? null : alias.value }; void runOwnerMetadataMutation("update_wallet_alias", { request }, "Mise à jour de l’alias…", "Alias mis à jour."); }); document.querySelector("#clearOwnerAlias")?.addEventListener("click", () => { const request: WalletAliasUpdateRequestDto = { alias: null }; void runOwnerMetadataMutation("update_wallet_alias", { request }, "Suppression de l’alias…", "Alias supprimé."); }); document.querySelector("#addOwnerNote")?.addEventListener("click", () => { const text = document.querySelector("#ownerNewNote"); if (!text || text.value.length === 0) { setText("#ownerMetadataStatus", "Texte de note requis."); return; } const request: WalletNoteAddRequestDto = { text: text.value }; void runOwnerMetadataMutation("add_wallet_note", { request }, "Ajout de la note…", "Note ajoutée.").then(succeeded => { if (succeeded && activeSessionState === "owner_open") { text.value = ""; } }); }); document.querySelector("#confirmDeleteWalletNote")?.addEventListener("click", () => { const noteId = pendingDeleteNoteId; pendingDeleteNoteId = null; deleteNoteModal?.hide(); if (!noteId) { return; } const request: WalletNoteDeleteRequestDto = { noteId }; void runOwnerMetadataMutation("delete_wallet_note", { request }, "Suppression de la note…", "Note supprimée."); }); frontendTrace("main", "OWNER metadata action handlers installed"); } async function selectWallet(walletId: string): Promise { clearSelectedWallet(); const request: WalletSelectionRequestDto = { walletId }; try { const wallet = await invokeKsp("main", "select_wallet", { request }); renderLockedWallet(wallet); } catch { clearSelectedWallet(); frontendWarn("main", "Locked Wallet selection failed", { walletId }); } } function bindWalletTableSelection(): void { const table = document.querySelector("#walletInventoryTable"); if (!table) { return; } table.addEventListener("click", event => { const source = event.target; if (!(source instanceof Element)) { return; } const row = source.closest("tbody tr[data-wallet-id]"); if (!row) { return; } const walletId = row.dataset.walletId; if (!walletId) { return; } if (row.dataset.walletSelectable !== "true") { frontendDebug("main", "Invalid Wallet inventory row selection ignored", { walletId }); return; } frontendTrace("main", "Wallet inventory row selected", { walletId }); void selectWallet(walletId); }); frontendTrace("main", "Wallet inventory row selection handler installed"); } async function loadWalletInventory(command: "list_wallets" | "refresh_wallets", clearSession: boolean): Promise { frontendDebug("main", "Wallet inventory load requested", { command }); const entries = await invokeKsp("main", command); if (clearSession) { clearSelectedWallet(); } renderWalletInventory(entries); } function createRequestFromForm(): WalletCreateRequestDto | null { const filename = document.querySelector("#createWalletFilename"); const ownerPassword = document.querySelector("#createWalletOwnerPassword"); const enableView = document.querySelector("#createWalletEnableView"); const viewPassword = document.querySelector("#createWalletViewPassword"); const alias = document.querySelector("#createWalletAlias"); const note = document.querySelector("#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("#createWalletForm"); const enableView = document.querySelector("#createWalletEnableView"); const viewPassword = document.querySelector("#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("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"); } function importRequestFromForm(): WalletImportRequestDto | null { const filename = document.querySelector("#importWalletFilename"); const ownerPassword = document.querySelector("#importWalletOwnerPassword"); const enableView = document.querySelector("#importWalletEnableView"); const viewPassword = document.querySelector("#importWalletViewPassword"); const alias = document.querySelector("#importWalletAlias"); const note = document.querySelector("#importWalletInitialNote"); if (!stagedImportSource || !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, }; } async function inspectImportSource(): Promise { const formatControl = document.querySelector("#importTransferFormat"); if (!formatControl) { return; } const format = formatControl.value as WalletTransferFormatDto; const request: WalletImportSourceRequestDto = { format }; clearImportSourceProjection(); setText("#importWalletStatus", "Sélection native de la source…"); frontendDebug("main", "Wallet import native picker requested", { format }); try { const inspection = await invokeKsp("main", "inspect_import_source", { request }); if (!inspection) { setText("#importWalletStatus", "Sélection annulée."); frontendDebug("main", "Wallet import native picker cancelled", { format }); return; } stagedImportSource = inspection; setText("#importSourceName", inspection.sourceName); setText("#importSourceFormat", importFormatLabel(inspection.format)); setText("#importSourcePubkey", inspection.pubkey); setText("#importWalletStatus", "Source validée. Saisir les nouveaux credentials KSP."); updateImportActions(); frontendInfo("main", "Wallet transfer source inspected", { format: inspection.format }); } catch { clearImportSourceProjection(); setText("#importWalletStatus", "Source invalide ou illisible. Consulter le diagnostic et les logs."); frontendWarn("main", "Wallet transfer source inspection failed", { format }); } } function bindImportWalletForm(): void { const form = document.querySelector("#importWalletForm"); const format = document.querySelector("#importTransferFormat"); const pick = document.querySelector("#inspectImportSource"); const enableView = document.querySelector("#importWalletEnableView"); const viewPassword = document.querySelector("#importWalletViewPassword"); if (format) { format.addEventListener("change", () => { clearImportSourceProjection(); setText("#importWalletStatus", "Format modifié ; choisir une nouvelle source."); void invokeKsp("main", "clear_import_source").catch(() => { frontendWarn("main", "Backend staged Wallet import source clear failed"); }); frontendTrace("main", "Wallet import format changed", { format: format.value }); }); } if (pick) { pick.addEventListener("click", () => { void inspectImportSource(); }); } if (enableView && viewPassword) { enableView.addEventListener("change", () => { viewPassword.disabled = !enableView.checked; if (!enableView.checked) { viewPassword.value = ""; } frontendTrace("main", "Import Wallet VIEW option changed", { enabled: enableView.checked }); }); } if (!form) { return; } form.addEventListener("submit", event => { event.preventDefault(); const request = importRequestFromForm(); if (!request || !stagedImportSource) { setText("#importWalletStatus", "Source/filename/passwords invalides ou incomplets."); frontendWarn("main", "Import Wallet form validation rejected request"); return; } const requestedWalletId = request.filename; const transferFormat = stagedImportSource.format; const requestedViewEnabled = request.viewPassword !== null; setText("#importWalletStatus", "Import en cours…"); frontendDebug("main", "Import Wallet request submitted", { transferFormat, viewEnabled: requestedViewEnabled, walletId: requestedWalletId }); clearSelectedWallet(); void invokeKsp("main", "import_wallet", { request }) .then(async wallet => { clearImportSensitiveInputs(); clearImportSourceProjection(); renderAuthorizedWallet(wallet); setText("#importWalletStatus", "Wallet importé ; session OWNER ouverte."); await loadWalletInventory("list_wallets", false); activateView("dashboard", "user"); }) .catch(() => { clearImportSensitiveInputs(); clearImportSourceProjection(); setText("#importWalletStatus", "Import impossible ; choisir à nouveau la source. Consulter le diagnostic et les logs."); frontendWarn("main", "Import Wallet operation failed", { transferFormat, walletId: requestedWalletId }); }); }); frontendTrace("main", "Import 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("#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 { const input = document.querySelector(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("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 { 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("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 }); } } async function rotateWalletPassword(capability: UnlockCapability): Promise { const sessionCapability = authorizedWalletProjection?.capability; const ownerAuthorized = activeSessionState === "owner_open" && sessionCapability === "owner"; const viewAuthorized = activeSessionState === "view_open" && sessionCapability === "view"; if (!activeWalletId || (capability === "owner" && !ownerAuthorized) || (capability === "view" && !ownerAuthorized && !viewAuthorized)) { clearRotationSensitiveInputs(); setText("#rotationStatus", capability === "owner" ? "Session OWNER requise." : "Session VIEW ou OWNER requise."); return; } if (capability === "view" && !activeViewEnabled) { clearRotationSensitiveInputs(); setText("#rotationStatus", "VIEW est désactivé ; aucune rotation VIEW n’est possible."); return; } const passwordSelector = capability === "owner" ? "#rotateOwnerPassword" : "#rotateViewPassword"; const confirmationSelector = capability === "owner" ? "#rotateOwnerPasswordConfirm" : "#rotateViewPasswordConfirm"; const password = document.querySelector(passwordSelector); const confirmation = document.querySelector(confirmationSelector); if (!password || !confirmation || password.value.length === 0) { clearRotationSensitiveInputs(); setText("#rotationStatus", "Nouveau password et confirmation requis."); return; } if (password.value !== confirmation.value) { clearRotationSensitiveInputs(); setText("#rotationStatus", "La confirmation ne correspond pas au nouveau password."); return; } const walletId = activeWalletId; const request: WalletPasswordRotationRequestDto = { password: password.value }; const command = capability === "owner" ? "rotate_owner_password" : "rotate_view_password"; activeSessionState = "privileged_operation"; setText("#currentWalletState", `Rotation ${capability.toUpperCase()}…`); setText("#rotationStatus", "Rotation Argon2 en cours…"); updateSessionActions(); frontendDebug("main", "Wallet credential rotation requested", { capability, walletId }); try { const wallet = await invokeKsp("main", command, { request }); clearRotationSensitiveInputs(); if (activeWalletId !== walletId) { frontendWarn("main", "Stale Wallet credential rotation response ignored", { capability, walletId }); return; } renderAuthorizedWallet(wallet); setText( "#rotationStatus", `${capability.toUpperCase()} password rotated. Les secrets Config (${configuredSecretCandidateCount} candidat(s)) ne sont pas modifiés automatiquement.`, ); await loadWalletInventory("list_wallets", false); frontendInfo("main", "Wallet credential rotation completed", { capability, walletId }); } catch (caughtError) { clearRotationSensitiveInputs(); if (isWalletStateConflict(caughtError)) { await recoverRotationStateConflict(walletId, sessionCapability === "view" ? "view" : "owner"); return; } if (activeWalletId === walletId && sessionCapability) { activeSessionState = sessionCapability === "view" ? "view_open" : "owner_open"; setText("#currentWalletState", `${sessionCapability.toUpperCase()} open`); setText("#rotationStatus", `Rotation refusée ; la session ${sessionCapability.toUpperCase()} reste ouverte.`); updateSessionActions(); } frontendWarn("main", "Wallet credential rotation failed", { capability, walletId }); } } function applyViewSecurityStatus(status: WalletViewSecurityStatusDto): boolean { const projection = authorizedWalletProjection; if ( !activeWalletId || status.walletId !== activeWalletId || status.capability !== "owner" || activeSessionState !== "privileged_operation" || projection?.capability !== "owner" ) { frontendWarn("main", "Stale strong VIEW administration response ignored", { walletId: status.walletId }); return false; } activeSessionState = "owner_open"; activeViewEnabled = status.viewEnabled; configuredSecretCandidateCount = status.configuredSecretCandidateCount; authorizedWalletProjection = { ...projection, configuredSecretCandidateCount: status.configuredSecretCandidateCount, viewEnabled: status.viewEnabled, }; setText("#currentWalletState", "OWNER open"); setText("#currentWalletView", status.viewEnabled ? "enabled" : "disabled"); setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString()); setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString()); updateSessionActions(); return true; } async function runStrongViewDisable(walletId: string): Promise { activeSessionState = "privileged_operation"; setText("#currentWalletState", "Strong disable VIEW…"); setText("#viewSecurityStatus", "Strong disable VIEW en cours…"); updateSessionActions(); frontendDebug("main", "Strong VIEW disable requested", { walletId }); try { const status = await invokeKsp("main", "disable_wallet_view"); if (!applyViewSecurityStatus(status)) { return; } setText("#viewSecurityStatus", "VIEW fortement désactivé. L’ancien credential VIEW ne peut plus ouvrir l’état courant."); await loadWalletInventory("list_wallets", false); frontendInfo("main", "Strong VIEW disable completed", { walletId }); } catch (caughtError) { if (isWalletStateConflict(caughtError)) { await recoverOwnerStateConflict(walletId); return; } if (activeWalletId === walletId) { activeSessionState = "owner_open"; setText("#currentWalletState", "OWNER open"); setText("#viewSecurityStatus", "Strong disable refusé ; la session OWNER reste ouverte."); updateSessionActions(); } frontendWarn("main", "Strong VIEW disable failed", { walletId }); } } async function runStrongViewRecreate(walletId: string, request: WalletPasswordRotationRequestDto): Promise { activeSessionState = "privileged_operation"; setText("#currentWalletState", "Strong recreate VIEW…"); setText("#viewSecurityStatus", "Strong recreate VIEW et Argon2 en cours…"); updateSessionActions(); frontendDebug("main", "Strong VIEW recreate requested", { walletId }); try { const status = await invokeKsp("main", "recreate_wallet_view", { request }); if (!applyViewSecurityStatus(status)) { return; } setText("#viewSecurityStatus", "VIEW fortement recréé avec une nouvelle autorité. Les secrets Config ne sont pas modifiés automatiquement."); await loadWalletInventory("list_wallets", false); frontendInfo("main", "Strong VIEW recreate completed", { walletId }); } catch (caughtError) { if (isWalletStateConflict(caughtError)) { await recoverOwnerStateConflict(walletId); return; } if (activeWalletId === walletId) { activeSessionState = "owner_open"; setText("#currentWalletState", "OWNER open"); setText("#viewSecurityStatus", "Strong recreate refusé ; la session OWNER reste ouverte."); updateSessionActions(); } frontendWarn("main", "Strong VIEW recreate failed", { walletId }); } } function bindStrongViewSecurityActions(): void { const disableModalElement = document.querySelector("#disableWalletViewModal"); if (disableModalElement) { disableViewModal = new Modal(disableModalElement); } const recreateModalElement = document.querySelector("#recreateWalletViewModal"); if (recreateModalElement) { recreateViewModal = new Modal(recreateModalElement); recreateModalElement.addEventListener("hidden.bs.modal", () => { clearStrongViewSensitiveInputs(); }); } document.querySelector("#disableWalletView")?.addEventListener("click", () => { if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) { setText("#viewSecurityStatus", "Session OWNER avec VIEW enabled requise."); return; } frontendDebug("main", "Strong VIEW disable confirmation opened", { walletId: activeWalletId }); disableViewModal?.show(); }); document.querySelector("#confirmDisableWalletView")?.addEventListener("click", () => { const walletId = activeWalletId; disableViewModal?.hide(); if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) { return; } void runStrongViewDisable(walletId); }); document.querySelector("#recreateWalletView")?.addEventListener("click", () => { if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") { clearStrongViewSensitiveInputs(); setText("#viewSecurityStatus", "Session OWNER requise."); return; } const password = document.querySelector("#recreateViewPassword"); const confirmation = document.querySelector("#recreateViewPasswordConfirm"); if (!password || !confirmation || password.value.length === 0) { clearStrongViewSensitiveInputs(); setText("#viewSecurityStatus", "Nouveau VIEW password et confirmation requis."); return; } if (password.value !== confirmation.value) { clearStrongViewSensitiveInputs(); setText("#viewSecurityStatus", "La confirmation ne correspond pas au nouveau VIEW password."); return; } frontendDebug("main", "Strong VIEW recreate confirmation opened", { walletId: activeWalletId }); recreateViewModal?.show(); }); document.querySelector("#confirmRecreateWalletView")?.addEventListener("click", () => { const walletId = activeWalletId; const password = document.querySelector("#recreateViewPassword"); const confirmation = document.querySelector("#recreateViewPasswordConfirm"); recreateViewModal?.hide(); if ( !walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !password || !confirmation || password.value.length === 0 || password.value !== confirmation.value ) { clearStrongViewSensitiveInputs(); return; } const request: WalletPasswordRotationRequestDto = { password: password.value }; clearStrongViewSensitiveInputs(); void runStrongViewRecreate(walletId, request); }); frontendTrace("main", "Strong VIEW security action handlers installed"); } async function runWalletExport(walletId: string, format: WalletTransferFormatDto): Promise { 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("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 n’ont 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 n’est autorisé et la session OWNER reste ouverte."); updateSessionActions(); } frontendWarn("main", "Wallet OWNER transfer export failed", { format, walletId }); } } function bindWalletExportActions(): void { const modalElement = document.querySelector("#exportWalletKeypairModal"); if (modalElement) { exportWalletModal = new Modal(modalElement); } document.querySelector("#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("#confirmExportWalletKeypair")?.addEventListener("click", () => { const walletId = activeWalletId; const formatControl = document.querySelector("#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("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => { void rotateWalletPassword("owner"); }); document.querySelector("#rotateViewPasswordSubmit")?.addEventListener("click", () => { void rotateWalletPassword("view"); }); frontendTrace("main", "Wallet credential rotation handlers installed"); } function bindUnlockActions(): void { document.querySelector("#unlockViewManual")?.addEventListener("click", () => { void unlockManual("view"); }); document.querySelector("#unlockOwnerManual")?.addEventListener("click", () => { void unlockManual("owner"); }); document.querySelector("#unlockViewConfigured")?.addEventListener("click", () => { void unlockConfigured("view"); }); document.querySelector("#unlockOwnerConfigured")?.addEventListener("click", () => { void unlockConfigured("owner"); }); frontendTrace("main", "Wallet unlock action handlers installed"); } async function lockCurrentWallet(): Promise { clearAuthorizedProjection(); try { const wallet = await invokeKsp("main", "lock_wallet"); renderLockedWallet(wallet); await loadWalletInventory("list_wallets", false); } catch { clearSelectedWallet(); frontendWarn("main", "Wallet lock operation failed"); } } async function deselectCurrentWallet(): Promise { try { const session = await invokeKsp("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 renderWalletBalance(balance: WalletBalanceDto): void { if (activeWalletId !== balance.walletId || (activeSessionState !== "view_open" && activeSessionState !== "owner_open")) { frontendWarn("main", "Stale Wallet balance response ignored", { walletId: balance.walletId }); return; } setText("#currentWalletBalanceSol", balance.sol); setText("#detailsBalanceLamports", balance.lamports); setText("#detailsBalanceSol", balance.sol); setText("#detailsBalanceSlot", balance.slot.toString()); setText("#detailsBalanceApiVersion", balance.apiVersion ?? "—"); setText("#detailsBalanceTransport", `${balance.transportProfile} / ${balance.transportRole}`); setText("#balanceStatus", "Balance réseau actualisée."); frontendInfo("main", "Wallet balance rendered", { lamports: balance.lamports, slot: balance.slot, walletId: balance.walletId }); } async function refreshWalletBalance(): Promise { if (!activeWalletId || (activeSessionState !== "view_open" && activeSessionState !== "owner_open")) { return; } const walletId = activeWalletId; setText("#balanceStatus", "Interrogation getBalance en cours…"); frontendDebug("main", "Wallet balance refresh requested", { walletId }); try { const balance = await invokeKsp("main", "refresh_wallet_balance"); renderWalletBalance(balance); } catch { if (activeWalletId === walletId) { clearBalanceProjection(); setText("#balanceStatus", "Échec getBalance. Consulter le diagnostic et les logs."); } frontendWarn("main", "Wallet balance refresh failed", { walletId }); } } function bindBalanceActions(): void { document.querySelector("#refreshWalletBalance")?.addEventListener("click", () => { void refreshWalletBalance(); }); frontendTrace("main", "Wallet balance action handlers installed"); } function renderRuntimeStatus(status: RuntimeStatusDto): void { setText("#runtimeVersion", status.applicationVersion); setText("#runtimeCompositeProfile", status.activeCompositeProfile); setText("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback transitoire"); setText("#runtimeTransportProfile", status.activeTransportProfile); setText("#runtimeTransportClusters", status.transportClusters.join(", ") || "—"); setText("#runtimeTransportProviders", status.transportProviders.join(", ") || "—"); setText("#runtimeTransportEndpoints", `${status.transportAvailableEndpointCount}/${status.transportEndpointCount} disponibles`); setText("#runtimeTransportRole", status.transportRole); setText("#diagnosticsTransportProfile", status.activeTransportProfile); setText("#diagnosticsTransportClusters", status.transportClusters.join(", ") || "—"); setText("#diagnosticsTransportProviders", status.transportProviders.join(", ") || "—"); setText("#diagnosticsTransportEndpoints", `${status.transportAvailableEndpointCount}/${status.transportEndpointCount}`); setText("#diagnosticsTransportRole", status.transportRole); 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); frontendTrace("main", "Wallet Desk runtime status rendered", { compositeProfile: status.activeCompositeProfile, fallbackLoggingActive: status.fallbackLoggingActive, shellPhase: status.shellPhase, transportEndpointCount: status.transportEndpointCount, transportProfile: status.activeTransportProfile, walletDirectoryCreatedOnStartup: status.effectiveWalletsDirectoryCreatedOnStartup, walletProfile: status.activeWalletProfile, }); } async function loadRuntimeStatus(): Promise { const status = await invokeKsp("main", "get_runtime_status"); renderRuntimeStatus(status); } function bindShellActions(): void { document.querySelectorAll("[data-shell-action]").forEach(button => { button.addEventListener("click", () => { const action = button.dataset.shellAction ?? "unknown"; frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled }); 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(); } }); }); frontendTrace("main", "Wallet Desk shell action handlers installed"); } async function initializeMain(): Promise { const windowLabel = getCurrentWindow().label; frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel }); bindFrontendInteractions(); bindNavigation(); bindWalletTableSelection(); bindCreateWalletForm(); bindImportWalletForm(); bindUnlockActions(); bindBalanceActions(); bindOwnerMetadataActions(); bindRotationActions(); bindStrongViewSecurityActions(); bindWalletExportActions(); bindShellActions(); clearSelectedWallet(); activateView("dashboard", "startup"); try { await loadRuntimeStatus(); await loadWalletInventory("list_wallets", false); } catch { renderWalletInventory([]); frontendWarn("main", "Wallet Desk startup data load failed"); } } document.addEventListener("DOMContentLoaded", () => { void initializeMain(); });