Files
khadhroony-solana-project/crates/ksp-app-wallet-desk/frontend/ts/main.ts
2026-08-21 15:22:42 +02:00

1430 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 12
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 { 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 { 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";
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" | "privileged_operation" | "view_open" | "owner_open";
type UnlockCapability = "view" | "owner";
const viewTitles: Record<ViewId, string> = {
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 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<HTMLElement>("#headerViewTitle");
const viewTitle = document.querySelector<HTMLElement>("#viewTitle");
if (headerTitle) {
headerTitle.textContent = title;
}
if (viewTitle) {
viewTitle.textContent = title;
}
document.title = `Wallet Desk — ${title}`;
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
});
document.querySelectorAll<HTMLElement>("[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<HTMLElement>('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<HTMLButtonElement>("[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.011-strong-view-administration" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
if (DataTable.isDataTable("#walletInventoryTable")) {
new DataTable("#walletInventoryTable").destroy();
}
const body = document.querySelector<HTMLTableSectionElement>("#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<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function updateSessionActions(): void {
const refreshBalance = document.querySelector<HTMLButtonElement>("#refreshWalletBalance");
const lock = document.querySelector<HTMLButtonElement>('[data-shell-action="lock"]');
const deselect = document.querySelector<HTMLButtonElement>('[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();
}
function updateOwnerMetadataActions(): void {
const ownerOpen = activeSessionState === "owner_open";
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement>("[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<HTMLInputElement | HTMLButtonElement>("[data-owner-rotation-control]").forEach(control => {
control.disabled = !ownerOpen;
});
const viewControls = document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[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<HTMLButtonElement>("#disableWalletView");
if (disable) {
disable.disabled = !ownerOpen || !activeViewEnabled;
}
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-security-recreate-control]").forEach(control => {
control.disabled = !ownerOpen;
});
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
const ownerManual = document.querySelector<HTMLButtonElement>("#unlockOwnerManual");
const viewConfigured = document.querySelector<HTMLButtonElement>("#unlockViewConfigured");
const ownerConfigured = document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured");
if (viewManual) {
viewManual.disabled = !locked || !activeViewEnabled;
}
if (ownerManual) {
ownerManual.disabled = !locked;
}
if (viewConfigured) {
viewConfigured.disabled = !locked || !activeViewEnabled || configuredSecretCandidateCount === 0;
}
if (ownerConfigured) {
ownerConfigured.disabled = !locked || configuredSecretCandidateCount === 0;
}
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
}
function importFormatLabel(format: WalletTransferFormatDto): string {
return format === "solana_cli_json" ? "Solana CLI JSON" : "Keypair Base58 complet";
}
function updateImportActions(): void {
const submit = document.querySelector<HTMLButtonElement>("#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<HTMLInputElement>("#importWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#importWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#importWalletInitialNote");
if (ownerPassword) {
ownerPassword.value = "";
}
if (viewPassword) {
viewPassword.value = "";
}
if (alias) {
alias.value = "";
}
if (note) {
note.value = "";
}
}
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 clearUnlockSensitiveInputs(): void {
const viewPassword = document.querySelector<HTMLInputElement>("#unlockWalletViewPassword");
const ownerPassword = document.querySelector<HTMLInputElement>("#unlockWalletOwnerPassword");
if (viewPassword) {
viewPassword.value = "";
}
if (ownerPassword) {
ownerPassword.value = "";
}
}
function clearRotationSensitiveInputs(): void {
for (const selector of ["#rotateOwnerPassword", "#rotateOwnerPasswordConfirm", "#rotateViewPassword", "#rotateViewPasswordConfirm"]) {
const input = document.querySelector<HTMLInputElement>(selector);
if (input) {
input.value = "";
}
}
}
function clearStrongViewSensitiveInputs(): void {
for (const selector of ["#recreateViewPassword", "#recreateViewPasswordConfirm"]) {
const input = document.querySelector<HTMLInputElement>(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.");
}
function clearSelectedWallet(): void {
activeWalletId = null;
activeSessionState = "no_selection";
activeViewEnabled = false;
configuredSecretCandidateCount = 0;
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
}
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<HTMLElement>("#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.");
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<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
}
const capabilityLabel = wallet.capability === "view" ? "VIEW" : "OWNER";
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", `${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 : ladministration forte VIEW est réservée à OWNER.",
);
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<HTMLInputElement>("#ownerWalletAlias");
const newNote = document.querySelector<HTMLTextAreaElement>("#ownerNewNote");
const list = document.querySelector<HTMLElement>("#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<HTMLInputElement>("#ownerWalletAlias");
const list = document.querySelector<HTMLElement>("#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<CommandErrorDto>;
return error.domain === "wallet" && error.code === "state_conflict";
}
async function recoverOwnerStateConflict(walletId: string): Promise<void> {
clearAuthorizedProjection();
clearBalanceProjection();
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("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 linventaire.");
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<void> {
clearAuthorizedProjection();
clearBalanceProjection();
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("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<string, unknown>,
pendingMessage: string,
successMessage: string,
): Promise<boolean> {
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<WalletAuthorizedDto>("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<HTMLElement>("#deleteWalletNoteModal");
if (modalElement) {
deleteNoteModal = new Modal(modalElement);
}
document.querySelector<HTMLButtonElement>("#saveOwnerAlias")?.addEventListener("click", () => {
const alias = document.querySelector<HTMLInputElement>("#ownerWalletAlias");
if (!alias) {
return;
}
const request: WalletAliasUpdateRequestDto = { alias: alias.value.length === 0 ? null : alias.value };
void runOwnerMetadataMutation("update_wallet_alias", { request }, "Mise à jour de lalias…", "Alias mis à jour.");
});
document.querySelector<HTMLButtonElement>("#clearOwnerAlias")?.addEventListener("click", () => {
const request: WalletAliasUpdateRequestDto = { alias: null };
void runOwnerMetadataMutation("update_wallet_alias", { request }, "Suppression de lalias…", "Alias supprimé.");
});
document.querySelector<HTMLButtonElement>("#addOwnerNote")?.addEventListener("click", () => {
const text = document.querySelector<HTMLTextAreaElement>("#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<HTMLButtonElement>("#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<void> {
clearSelectedWallet();
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
renderLockedWallet(wallet);
} catch {
clearSelectedWallet();
frontendWarn("main", "Locked Wallet selection failed", { walletId });
}
}
function bindWalletTableSelection(): void {
const table = document.querySelector<HTMLTableElement>("#walletInventoryTable");
if (!table) {
return;
}
table.addEventListener("click", event => {
const source = event.target;
if (!(source instanceof Element)) {
return;
}
const row = source.closest<HTMLTableRowElement>("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<void> {
frontendDebug("main", "Wallet inventory load requested", { command });
const entries = await invokeKsp<WalletInventoryEntryDto[]>("main", command);
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");
}
function importRequestFromForm(): WalletImportRequestDto | null {
const filename = document.querySelector<HTMLInputElement>("#importWalletFilename");
const ownerPassword = document.querySelector<HTMLInputElement>("#importWalletOwnerPassword");
const enableView = document.querySelector<HTMLInputElement>("#importWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#importWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#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<void> {
const formatControl = document.querySelector<HTMLSelectElement>("#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<WalletTransferInspectionDto | null>("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<HTMLFormElement>("#importWalletForm");
const format = document.querySelector<HTMLSelectElement>("#importTransferFormat");
const pick = document.querySelector<HTMLButtonElement>("#inspectImportSource");
const enableView = document.querySelector<HTMLInputElement>("#importWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
if (format) {
format.addEventListener("change", () => {
clearImportSourceProjection();
setText("#importWalletStatus", "Format modifié ; choisir une nouvelle source.");
void invokeKsp<void>("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<WalletAuthorizedDto>("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<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3";
}
setText("#currentWalletState", "Locked");
setText("#unlockStatus", "Unlock refusé. Le wallet reste verrouillé.");
updateSessionActions();
}
async function unlockManual(capability: UnlockCapability): Promise<void> {
const input = document.querySelector<HTMLInputElement>(capability === "view" ? "#unlockWalletViewPassword" : "#unlockWalletOwnerPassword");
if (!input || input.value.length === 0) {
setText("#unlockStatus", "Password manuel requis.");
return;
}
const request: WalletUnlockRequestDto = { password: input.value };
const walletId = beginFrontendUnlock(capability, "manual");
if (!walletId) {
clearUnlockSensitiveInputs();
return;
}
const command = capability === "view" ? "unlock_wallet_view_manual" : "unlock_wallet_owner_manual";
frontendDebug("main", "Manual Wallet unlock requested", { capability, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command, { request });
clearUnlockSensitiveInputs();
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
clearUnlockSensitiveInputs();
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Manual Wallet unlock failed", { capability, walletId });
}
}
async function unlockConfigured(capability: UnlockCapability): Promise<void> {
const walletId = beginFrontendUnlock(capability, "configured");
if (!walletId) {
return;
}
const command = capability === "view" ? "unlock_wallet_view_configured" : "unlock_wallet_owner_configured";
frontendDebug("main", "Configured-secret Wallet unlock requested", { capability, configuredSecretCandidateCount, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command);
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Configured-secret Wallet unlock failed", { capability, configuredSecretCandidateCount, walletId });
}
}
async function rotateWalletPassword(capability: UnlockCapability): Promise<void> {
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 nest possible.");
return;
}
const passwordSelector = capability === "owner" ? "#rotateOwnerPassword" : "#rotateViewPassword";
const confirmationSelector = capability === "owner" ? "#rotateOwnerPasswordConfirm" : "#rotateViewPasswordConfirm";
const password = document.querySelector<HTMLInputElement>(passwordSelector);
const confirmation = document.querySelector<HTMLInputElement>(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<WalletAuthorizedDto>("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<void> {
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<WalletViewSecurityStatusDto>("main", "disable_wallet_view");
if (!applyViewSecurityStatus(status)) {
return;
}
setText("#viewSecurityStatus", "VIEW fortement désactivé. Lancien 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<void> {
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<WalletViewSecurityStatusDto>("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<HTMLElement>("#disableWalletViewModal");
if (disableModalElement) {
disableViewModal = new Modal(disableModalElement);
}
const recreateModalElement = document.querySelector<HTMLElement>("#recreateWalletViewModal");
if (recreateModalElement) {
recreateViewModal = new Modal(recreateModalElement);
recreateModalElement.addEventListener("hidden.bs.modal", () => {
clearStrongViewSensitiveInputs();
});
}
document.querySelector<HTMLButtonElement>("#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<HTMLButtonElement>("#confirmDisableWalletView")?.addEventListener("click", () => {
const walletId = activeWalletId;
disableViewModal?.hide();
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) {
return;
}
void runStrongViewDisable(walletId);
});
document.querySelector<HTMLButtonElement>("#recreateWalletView")?.addEventListener("click", () => {
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
clearStrongViewSensitiveInputs();
setText("#viewSecurityStatus", "Session OWNER requise.");
return;
}
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
const confirmation = document.querySelector<HTMLInputElement>("#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<HTMLButtonElement>("#confirmRecreateWalletView")?.addEventListener("click", () => {
const walletId = activeWalletId;
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
const confirmation = document.querySelector<HTMLInputElement>("#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");
}
function bindRotationActions(): void {
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
void rotateWalletPassword("owner");
});
document.querySelector<HTMLButtonElement>("#rotateViewPasswordSubmit")?.addEventListener("click", () => {
void rotateWalletPassword("view");
});
frontendTrace("main", "Wallet credential rotation handlers installed");
}
function bindUnlockActions(): void {
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
void unlockManual("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerManual")?.addEventListener("click", () => {
void unlockManual("owner");
});
document.querySelector<HTMLButtonElement>("#unlockViewConfigured")?.addEventListener("click", () => {
void unlockConfigured("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured")?.addEventListener("click", () => {
void unlockConfigured("owner");
});
frontendTrace("main", "Wallet unlock action handlers installed");
}
async function lockCurrentWallet(): Promise<void> {
clearAuthorizedProjection();
try {
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 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<void> {
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<WalletBalanceDto>("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<HTMLButtonElement>("#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);
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
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<void> {
const status = await invokeKsp<RuntimeStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
}
function bindShellActions(): void {
document.querySelectorAll<HTMLButtonElement>("[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<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel });
bindFrontendInteractions();
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindImportWalletForm();
bindUnlockActions();
bindBalanceActions();
bindOwnerMetadataActions();
bindRotationActions();
bindStrongViewSecurityActions();
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();
});