// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // version: 5 import "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 { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.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 { 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"; (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" | "owner_open"; 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"; 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 === "owner_open") { appendIconText(state, "fa-lock-open", "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.005-wallet-session-create" }); } 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 lock = document.querySelector('[data-shell-action="lock"]'); const deselect = document.querySelector('[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("#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 clearAuthorizedProjection(): void { setText("#currentWalletPubkey", "—"); setText("#currentWalletAlias", "—"); setText("#currentWalletNotes", "—"); clearCreateFormSensitiveInputs(); } function clearSelectedWallet(): void { activeWalletId = null; activeSessionState = "no_selection"; 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", "—"); clearAuthorizedProjection(); updateSessionActions(); } function renderLockedWallet(wallet: LockedWalletDto): void { activeWalletId = wallet.walletId; activeSessionState = "locked"; 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"); 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("#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 { 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"); } 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 renderRuntimeStatus(status: RuntimeStatusDto): void { 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, shellPhase: status.shellPhase, 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(); bindShellActions(); clearSelectedWallet(); activateView("dashboard", "startup"); try { await loadRuntimeStatus(); await loadWalletInventory("list_wallets", false); } catch { renderWalletInventory([]); setText("#shellStatus", "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé."); frontendWarn("main", "Wallet Desk startup data load failed"); } } document.addEventListener("DOMContentLoaded", () => { void initializeMain(); });