v0.2.6-pre.006
This commit is contained in:
@@ -40,9 +40,9 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
|
||||
<p class="text-body-secondary mb-0">Inventaire `.kspwallet` root-scoped et inspection locked actifs.</p>
|
||||
<p class="text-body-secondary mb-0">Création native, sessions durables et unlock explicite VIEW/OWNER actifs.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.005</span>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.006</span>
|
||||
</div>
|
||||
<section data-view-panel="dashboard">
|
||||
<div class="row g-3">
|
||||
@@ -76,6 +76,7 @@
|
||||
<dt class="col-5">État</dt><dd id="currentWalletState" class="col-7">—</dd>
|
||||
<dt class="col-5">Format</dt><dd id="currentWalletFormat" class="col-7">—</dd>
|
||||
<dt class="col-5">VIEW</dt><dd id="currentWalletView" class="col-7">—</dd>
|
||||
<dt class="col-5">Secrets Config</dt><dd id="currentWalletSecretCandidates" class="col-7">0</dd>
|
||||
<dt class="col-5">Pubkey</dt><dd id="currentWalletPubkey" class="col-7 text-break">—</dd>
|
||||
<dt class="col-5">Alias</dt><dd id="currentWalletAlias" class="col-7 text-break">—</dd>
|
||||
<dt class="col-5">Notes</dt><dd id="currentWalletNotes" class="col-7 text-break">—</dd>
|
||||
@@ -159,14 +160,57 @@
|
||||
</div>
|
||||
</section>
|
||||
<section data-view-panel="details" hidden><div class="app-placeholder"><i class="fa-solid fa-id-card"></i><p>Les détails protégés seront affichés uniquement après VIEW/OWNER.</p></div></section>
|
||||
<section data-view-panel="security" hidden><div class="app-placeholder"><i class="fa-solid fa-shield-halved"></i><p>Les opérations OWNER seront ajoutées après le MVP identité + balance.</p></div></section>
|
||||
<section data-view-panel="security" hidden>
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">Unlock manuel du wallet sélectionné</div>
|
||||
<div class="card-body">
|
||||
<p class="text-body-secondary">Chaque tentative Argon2 est explicite et peut prendre plusieurs secondes. Le password est envoyé uniquement frontend → Rust puis purgé.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="unlockWalletViewPassword">VIEW password</label>
|
||||
<div class="input-group">
|
||||
<input id="unlockWalletViewPassword" class="form-control" type="password" autocomplete="current-password">
|
||||
<button id="unlockViewManual" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="unlockWalletOwnerPassword">OWNER password</label>
|
||||
<div class="input-group">
|
||||
<input id="unlockWalletOwnerPassword" class="form-control" type="password" autocomplete="current-password">
|
||||
<button id="unlockOwnerManual" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header fw-semibold">Secrets configurés</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2">Candidats effectifs : <strong id="unlockSecretCandidateCount">0</strong></p>
|
||||
<p class="small text-body-secondary">Les noms, suffixes et valeurs des secrets Wallet configurés restent exclusivement côté Rust/Config. Cette action tente les candidats uniquement sur demande.</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button id="unlockViewConfigured" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW avec secret configuré</button>
|
||||
<button id="unlockOwnerConfigured" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER avec secret configuré</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section data-view-panel="diagnostics" hidden><div class="app-placeholder"><i class="fa-solid fa-stethoscope"></i><p>Les diagnostics Config/Wallet/Transport seront enrichis progressivement.</p></div></section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||
<span>KSP Wallet Desk · create + session lifecycle pre.005</span>
|
||||
<span>KSP Wallet Desk · manual/configured unlock pre.006</span>
|
||||
</footer>
|
||||
<script type="module" src="./ts/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
import "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -14,6 +14,7 @@ import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/w
|
||||
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";
|
||||
@@ -22,7 +23,8 @@ import "../sass/main.scss";
|
||||
installFrontendConsoleBridge("main");
|
||||
|
||||
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
|
||||
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
|
||||
type ActiveSessionState = "no_selection" | "locked" | "privileged_operation" | "view_open" | "owner_open";
|
||||
type UnlockCapability = "view" | "owner";
|
||||
|
||||
const viewTitles: Record<ViewId, string> = {
|
||||
dashboard: "Dashboard",
|
||||
@@ -35,6 +37,8 @@ const viewTitles: Record<ViewId, string> = {
|
||||
|
||||
let activeWalletId: string | null = null;
|
||||
let activeSessionState: ActiveSessionState = "no_selection";
|
||||
let activeViewEnabled = false;
|
||||
let configuredSecretCandidateCount = 0;
|
||||
|
||||
function isViewId(value: string): value is ViewId {
|
||||
return value in viewTitles;
|
||||
@@ -121,8 +125,8 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
|
||||
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");
|
||||
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");
|
||||
}
|
||||
@@ -165,7 +169,7 @@ function initializeWalletTable(): void {
|
||||
zeroRecords: "Aucun wallet correspondant.",
|
||||
},
|
||||
});
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.006-wallet-unlock" });
|
||||
}
|
||||
|
||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||
@@ -192,11 +196,33 @@ function updateSessionActions(): void {
|
||||
const lock = document.querySelector<HTMLButtonElement>('[data-shell-action="lock"]');
|
||||
const deselect = document.querySelector<HTMLButtonElement>('[data-shell-action="deselect"]');
|
||||
if (lock) {
|
||||
lock.disabled = activeSessionState !== "owner_open";
|
||||
lock.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open";
|
||||
}
|
||||
if (deselect) {
|
||||
deselect.disabled = activeSessionState === "no_selection";
|
||||
deselect.disabled = activeSessionState === "no_selection" || activeSessionState === "privileged_operation";
|
||||
}
|
||||
updateUnlockActions();
|
||||
}
|
||||
|
||||
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 clearCreateFormSensitiveInputs(): void {
|
||||
@@ -218,16 +244,30 @@ function clearCreateFormSensitiveInputs(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function clearUnlockSensitiveInputs(): void {
|
||||
const viewPassword = document.querySelector<HTMLInputElement>("#unlockWalletViewPassword");
|
||||
const ownerPassword = document.querySelector<HTMLInputElement>("#unlockWalletOwnerPassword");
|
||||
if (viewPassword) {
|
||||
viewPassword.value = "";
|
||||
}
|
||||
if (ownerPassword) {
|
||||
ownerPassword.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuthorizedProjection(): void {
|
||||
setText("#currentWalletPubkey", "—");
|
||||
setText("#currentWalletAlias", "—");
|
||||
setText("#currentWalletNotes", "—");
|
||||
clearCreateFormSensitiveInputs();
|
||||
clearUnlockSensitiveInputs();
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -236,6 +276,8 @@ function clearSelectedWallet(): void {
|
||||
setText("#currentWalletState", "—");
|
||||
setText("#currentWalletFormat", "—");
|
||||
setText("#currentWalletView", "—");
|
||||
setText("#currentWalletSecretCandidates", "0");
|
||||
setText("#unlockStatus", "Sélectionner un wallet verrouillé.");
|
||||
clearAuthorizedProjection();
|
||||
updateSessionActions();
|
||||
}
|
||||
@@ -243,6 +285,8 @@ function clearSelectedWallet(): void {
|
||||
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) {
|
||||
@@ -252,26 +296,45 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
|
||||
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.");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
|
||||
frontendDebug("main", "Locked Wallet selection rendered", {
|
||||
configuredSecretCandidateCount,
|
||||
formatVersion: wallet.formatVersion,
|
||||
viewEnabled: wallet.viewEnabled,
|
||||
walletId: wallet.walletId,
|
||||
});
|
||||
}
|
||||
|
||||
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
activeWalletId = wallet.walletId;
|
||||
activeSessionState = "owner_open";
|
||||
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", "OWNER open");
|
||||
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 ?? "—");
|
||||
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
|
||||
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
capability: wallet.capability,
|
||||
configuredSecretCandidateCount,
|
||||
noteCount: wallet.notes.length,
|
||||
viewEnabled: wallet.viewEnabled,
|
||||
walletId: wallet.walletId,
|
||||
});
|
||||
}
|
||||
|
||||
async function selectWallet(walletId: string): Promise<void> {
|
||||
@@ -395,6 +458,94 @@ function bindCreateWalletForm(): void {
|
||||
frontendTrace("main", "Create 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 });
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -431,7 +582,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
|
||||
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
|
||||
setText("#runtimeShellPhase", status.shellPhase);
|
||||
setText("#shellStatus", "Config résolue ; création et session Wallet prêtes.");
|
||||
setText("#shellStatus", "Config résolue ; création et unlock VIEW/OWNER prêts.");
|
||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
@@ -477,6 +628,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindNavigation();
|
||||
bindWalletTableSelection();
|
||||
bindCreateWalletForm();
|
||||
bindUnlockActions();
|
||||
bindShellActions();
|
||||
clearSelectedWallet();
|
||||
activateView("dashboard", "startup");
|
||||
|
||||
Reference in New Issue
Block a user