v0.2.6-pre.004

This commit is contained in:
2026-08-20 23:01:41 +02:00
parent 6eb4d71043
commit 76bfce17c1
22 changed files with 903 additions and 43 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 167 # version: 168
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.6-pre.3.fix.4" version = "0.2.6-pre.4"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 63 --> <!-- version: 64 -->
# Roadmap KSP # Roadmap KSP
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants. - [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication. - [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. - [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments didentifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom denvironnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. Les tranches suivantes ajoutent inventory, lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final. - [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments didentifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom denvironnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. `pre.004` branche ensuite linventory réel `.kspwallet` sur le répertoire effectif Config : enumeration non récursive, extension exacte, fichiers réguliers uniquement, rejet des symlinks, inspection locked via `ksp-wallet-lib`, diagnostics sûrs par ligne, DataTable réel, refresh et sélection root-scoped revalidée côté Rust sans Pubkey/alias/notes. Les tranches suivantes ajoutent création/lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final.
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe. - [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-wallet-desk/Cargo.toml # file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 1 # version: 2
[package] [package]
name = "ksp-app-wallet-desk" name = "ksp-app-wallet-desk"
@@ -27,10 +27,11 @@ fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" } ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
tauri.workspace = true tauri.workspace = true
tauri-plugin-tracing.workspace = true tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["time"] } tokio = { workspace = true, features = ["fs", "rt", "time"] }
ts-rs.workspace = true ts-rs.workspace = true
[dev-dependencies] [dev-dependencies]

View File

@@ -37,9 +37,9 @@
<div class="d-flex align-items-center justify-content-between mb-4"> <div class="d-flex align-items-center justify-content-between mb-4">
<div> <div>
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1> <h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Composition Config Wallet Desk active — inventory Wallet branché en pre.004.</p> <p class="text-body-secondary mb-0">Inventaire `.kspwallet` root-scoped et inspection locked actifs.</p>
</div> </div>
<span class="badge text-bg-secondary">0.2.6-pre.003</span> <span class="badge text-bg-secondary">0.2.6-pre.004</span>
</div> </div>
<section data-view-panel="dashboard"> <section data-view-panel="dashboard">
<div class="row g-3"> <div class="row g-3">
@@ -66,9 +66,14 @@
<div class="col-xl-4"> <div class="col-xl-4">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Wallet courant</div> <div class="card-header fw-semibold">Wallet courant</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center text-body-secondary"> <div class="card-body text-center">
<i class="fa-solid fa-lock fa-2x mb-3" aria-hidden="true"></i> <i id="currentWalletIcon" class="fa-solid fa-lock fa-2x mb-3 text-body-secondary" aria-hidden="true"></i>
<p class="mb-0">Aucun wallet sélectionné.</p> <p id="currentWalletFilename" class="fw-semibold mb-2">Aucun wallet sélectionné.</p>
<dl class="row small text-start mb-0">
<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>
</dl>
</div> </div>
</div> </div>
</div> </div>
@@ -81,14 +86,14 @@
<span class="fw-semibold">Wallets disponibles</span> <span class="fw-semibold">Wallets disponibles</span>
<small class="text-body-secondary"><i class="fa-solid fa-lock me-1" aria-hidden="true"></i>verrouillé · <i class="fa-solid fa-lock-open me-1" aria-hidden="true"></i>ouvert</small> <small class="text-body-secondary"><i class="fa-solid fa-lock me-1" aria-hidden="true"></i>verrouillé · <i class="fa-solid fa-lock-open me-1" aria-hidden="true"></i>ouvert</small>
</div> </div>
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets" disabled> <button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh <i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button> </button>
</div> </div>
<div class="card-body"> <div class="card-body">
<table id="walletInventoryTable" class="table table-striped table-hover align-middle w-100"> <table id="walletInventoryTable" class="table table-striped table-hover align-middle w-100">
<thead><tr><th>État</th><th>Filename</th><th>Format</th><th>VIEW</th><th>Inspection</th></tr></thead> <thead><tr><th>État</th><th>Filename</th><th>Format</th><th>VIEW</th><th>Inspection</th></tr></thead>
<tbody></tbody> <tbody id="walletInventoryBody"></tbody>
</table> </table>
</div> </div>
</div> </div>
@@ -102,7 +107,7 @@
</div> </div>
</main> </main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4"> <footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · Config composition pre.003</span> <span>KSP Wallet Desk · locked inventory pre.004</span>
</footer> </footer>
<script type="module" src="./ts/main.ts"></script> <script type="module" src="./ts/main.ts"></script>
</body> </body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 3 // version: 4
import "bootstrap"; import "bootstrap";
import DataTable from "datatables.net-bs5"; import DataTable from "datatables.net-bs5";
@@ -8,7 +8,10 @@ import ResizeObserver from "resize-observer-polyfill";
import "simplebar"; import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts"; import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log"; 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 { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
import "../sass/main.scss"; import "../sass/main.scss";
@@ -95,20 +98,169 @@ function bindNavigation(): void {
frontendTrace("main", "Wallet Desk navigation handlers installed"); 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") {
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 { function initializeWalletTable(): void {
if (DataTable.isDataTable("#walletInventoryTable")) {
new DataTable("#walletInventoryTable").destroy();
}
new DataTable("#walletInventoryTable", { new DataTable("#walletInventoryTable", {
order: [[1, "asc"]], order: [[1, "asc"]],
pageLength: 10, pageLength: 10,
select: { select: {
selector: "tbody tr[data-wallet-selectable='true'] td",
style: "single", style: "single",
}, },
language: { language: {
emptyTable: "L'inventaire Wallet sera branché en pre.004.", emptyTable: "Aucun wallet .kspwallet disponible.",
search: "Filtrer :", search: "Filtrer :",
zeroRecords: "Aucun wallet correspondant.", zeroRecords: "Aucun wallet correspondant.",
}, },
}); });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.003-config-wallet" }); frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.004-wallet-inventory" });
}
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 clearSelectedWallet(): void {
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
const state = document.querySelector<HTMLElement>("#currentWalletState");
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
const view = document.querySelector<HTMLElement>("#currentWalletView");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
}
if (filename) {
filename.textContent = "Aucun wallet sélectionné.";
}
if (state) {
state.textContent = "—";
}
if (format) {
format.textContent = "—";
}
if (view) {
view.textContent = "—";
}
}
function renderSelectedWallet(wallet: LockedWalletDto): void {
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
const state = document.querySelector<HTMLElement>("#currentWalletState");
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
const view = document.querySelector<HTMLElement>("#currentWalletView");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3";
}
if (filename) {
filename.textContent = wallet.filename;
}
if (state) {
state.textContent = "Locked";
}
if (format) {
format.textContent = wallet.formatVersion.toString();
}
if (view) {
view.textContent = wallet.viewEnabled ? "enabled" : "disabled";
}
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
}
async function selectWallet(walletId: string): Promise<void> {
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
renderSelectedWallet(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"): Promise<void> {
frontendDebug("main", "Wallet inventory load requested", { command });
const entries = await invokeKsp<WalletInventoryEntryDto[]>("main", command);
clearSelectedWallet();
renderWalletInventory(entries);
} }
function renderRuntimeStatus(status: RuntimeStatusDto): void { function renderRuntimeStatus(status: RuntimeStatusDto): void {
@@ -158,7 +310,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
phase.textContent = status.shellPhase; phase.textContent = status.shellPhase;
} }
if (shellStatus) { if (shellStatus) {
shellStatus.textContent = "Config Wallet Desk résolue ; répertoire Wallet prêt."; shellStatus.textContent = "Config résolue ; inventaire Wallet prêt.";
} }
frontendTrace("main", "Wallet Desk runtime status rendered", { frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile, compositeProfile: status.activeCompositeProfile,
@@ -177,7 +329,13 @@ async function loadRuntimeStatus(): Promise<void> {
function bindShellActions(): void { function bindShellActions(): void {
document.querySelectorAll<HTMLButtonElement>("[data-shell-action]").forEach(button => { document.querySelectorAll<HTMLButtonElement>("[data-shell-action]").forEach(button => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
frontendDebug("main", "Wallet Desk shell action clicked", { action: button.dataset.shellAction ?? "unknown", enabled: !button.disabled }); const action = button.dataset.shellAction ?? "unknown";
frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled });
if (action === "refresh-wallets" && !button.disabled) {
void loadWalletInventory("refresh_wallets").catch(() => {
frontendWarn("main", "Wallet inventory refresh failed");
});
}
}); });
}); });
frontendTrace("main", "Wallet Desk shell action handlers installed"); frontendTrace("main", "Wallet Desk shell action handlers installed");
@@ -188,17 +346,19 @@ async function initializeMain(): Promise<void> {
frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel }); frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel });
bindFrontendInteractions(); bindFrontendInteractions();
bindNavigation(); bindNavigation();
bindWalletTableSelection();
bindShellActions(); bindShellActions();
initializeWalletTable();
activateView("dashboard", "startup"); activateView("dashboard", "startup");
try { try {
await loadRuntimeStatus(); await loadRuntimeStatus();
await loadWalletInventory("list_wallets");
} catch { } catch {
renderWalletInventory([]);
const shellStatus = document.querySelector<HTMLElement>("#shellStatus"); const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (shellStatus) { if (shellStatus) {
shellStatus.textContent = "Le statut runtime n'a pas pu être chargé."; shellStatus.textContent = "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.";
} }
frontendTrace("main", "Wallet Desk shell status replaced", { status: "runtime_error" }); frontendWarn("main", "Wallet Desk startup data load failed");
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "ksp-app-wallet-desk", "name": "ksp-app-wallet-desk",
"private": true, "private": true,
"version": "0.2.6-pre.3.fix.4", "version": "0.2.6-pre.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs // file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 3 // version: 4
//! Shared backend state owned by the Wallet Desk Tauri application. //! Shared backend state owned by the Wallet Desk Tauri application.
@@ -61,7 +61,7 @@ impl AppState {
}); });
} }
/// Builds the safe Config-composition status DTO exposed during pre.003. /// Builds the safe runtime status DTO exposed by the Wallet Desk shell.
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> { pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count(); let document_count = self.config_management.engine().registry().descriptors().count();
let document_count = u32::try_from(document_count); let document_count = u32::try_from(document_count);
@@ -100,13 +100,19 @@ impl AppState {
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(), effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
shell_phase: "pre.003-config-wallet".to_owned(), shell_phase: "pre.004-wallet-inventory".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(), startup_diagnostic: runtime.startup_diagnostic.clone(),
wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(), wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
wallets_subdirectory, wallets_subdirectory,
}); });
} }
/// Returns the effective Config-managed Wallet directory used by inventory operations.
#[must_use]
pub(crate) fn wallet_inventory_root(&self) -> &std::path::Path {
return self.wallet_config_startup.resolved().effective_wallets_directory();
}
/// Returns the resolved common splash timings captured during bootstrap. /// Returns the resolved common splash timings captured during bootstrap.
#[must_use] #[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs // file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 2 // version: 3
//! Logging targets, domains and composite component identifiers owned by Wallet Desk. //! Logging targets, domains and composite component identifiers owned by Wallet Desk.
@@ -17,6 +17,8 @@ pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell"; pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
/// Structured domain used while preparing Wallet filesystem roots. /// Structured domain used while preparing Wallet filesystem roots.
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config"; pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
/// Structured domain used by Tauri window lifecycle operations. /// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window"; pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Wallet Desk. /// Owning target for backend events emitted by Wallet Desk.
@@ -27,3 +29,5 @@ pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main"; pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
/// Owning target for splash-window frontend events. /// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash"; pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
/// Native Wallet filename suffix accepted by the Wallet Desk inventory.
pub(crate) const WALLET_FILE_SUFFIX: &str = ".kspwallet";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/dto_common.rs // file: crates/ksp-app-wallet-desk/src/dto_common.rs
// version: 2 // version: 3
//! Common Tauri DTOs shared by Wallet Desk shell commands. //! Common Tauri DTOs shared by Wallet Desk shell commands.
@@ -53,7 +53,7 @@ pub(crate) struct RuntimeStatusDto {
pub(crate) fallback_logging_active: bool, pub(crate) fallback_logging_active: bool,
/// Whether bootstrap created the configured global Wallet root. /// Whether bootstrap created the configured global Wallet root.
pub(crate) root_wallets_directory_created_on_startup: bool, pub(crate) root_wallets_directory_created_on_startup: bool,
/// Current implementation phase exposed for the Config composition tranche. /// Current implementation phase exposed by the Wallet Desk shell.
pub(crate) shell_phase: String, pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable. /// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>, pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs // file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 2 // version: 3
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces. //! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
@@ -31,3 +31,7 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_INVALID: ksp_core_lib::ErrorCode =
/// Wallet Desk could not inspect or create the configured Wallet directory tree. /// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed"); ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed");
/// Wallet Desk cannot enumerate or inspect its effective Wallet inventory directory.
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
/// A requested Wallet inventory identifier is unsafe or does not resolve to an eligible regular `.kspwallet` file.
pub(crate) const ERROR_CODE_WALLET_SELECTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_selection_invalid");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs // file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 2 // version: 3
//! Tauri desktop application shell for KSP Wallet management and inspection. //! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -19,6 +19,7 @@ mod tauri;
mod tw_main; mod tw_main;
mod tw_splash; mod tw_splash;
mod wallet_config; mod wallet_config;
mod wallet_inventory;
/// Runs the KSP wallet desktop application. /// Runs the KSP wallet desktop application.
pub use self::tauri::run; pub use self::tauri::run;
@@ -49,6 +50,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
pub(crate) use self::constants::TRACING_DOMAIN_SHELL; pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used while preparing Wallet filesystem roots. /// Structured domain used while preparing Wallet filesystem roots.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG; pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
/// Structured domain used by Tauri window lifecycle operations. /// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Wallet Desk. /// Owning target for backend events emitted by Wallet Desk.
@@ -59,6 +62,8 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events. /// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Native Wallet filename suffix accepted by inventory operations.
pub(crate) use self::constants::WALLET_FILE_SUFFIX;
/// Safe command error projection exposed to Tauri commands. /// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto; pub(crate) use self::dto_common::CommandErrorDto;
/// Initial application/runtime snapshot exposed to the Wallet Desk shell. /// Initial application/runtime snapshot exposed to the Wallet Desk shell.
@@ -89,6 +94,10 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID; pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
/// Wallet Desk could not inspect or create the configured Wallet directory tree. /// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED; pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
/// Wallet inventory filesystem operation failed.
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// Wallet selection identifier is invalid or no longer eligible.
pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID;
/// Log payload sent by Wallet Desk frontend scripts. /// Log payload sent by Wallet Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade. /// Emits one validated frontend event through the KSP Logging facade.
@@ -111,3 +120,17 @@ pub(crate) use self::tw_splash::splash_frontend_ready_service;
pub(crate) use self::wallet_config::WalletConfigStartup; pub(crate) use self::wallet_config::WalletConfigStartup;
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree. /// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
pub(crate) use self::wallet_config::initialize_wallet_config; pub(crate) use self::wallet_config::initialize_wallet_config;
/// Safe locked Wallet projection returned after selection.
pub(crate) use self::wallet_inventory::LockedWalletDto;
/// Locked inspection outcome exposed by Wallet inventory rows.
pub(crate) use self::wallet_inventory::WalletInspectionStatusDto;
/// Safe Wallet inventory row projection.
pub(crate) use self::wallet_inventory::WalletInventoryEntryDto;
/// Visual lock/error state exposed by Wallet inventory rows.
pub(crate) use self::wallet_inventory::WalletInventoryStateDto;
/// Request DTO used for one root-scoped Wallet selection.
pub(crate) use self::wallet_inventory::WalletSelectionRequestDto;
/// Enumerates native Wallet files under the effective Config-managed root.
pub(crate) use self::wallet_inventory::list_wallet_inventory;
/// Re-inspects one root-scoped Wallet selection.
pub(crate) use self::wallet_inventory::select_locked_wallet;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/tauri.rs // file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 1 // version: 2
//! Tauri runtime assembly for the KSP wallet desktop application. //! Tauri runtime assembly for the KSP wallet desktop application.
@@ -37,7 +37,14 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch. #[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready]); return builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
get_runtime_status,
list_wallets,
refresh_wallets,
select_wallet,
splash_frontend_ready
]);
} }
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
@@ -73,6 +80,41 @@ fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::
}; };
} }
#[tauri::command]
async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::list_wallet_inventory(root.as_path()).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn refresh_wallets(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::list_wallet_inventory(root.as_path()).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn select_wallet(
request: crate::WalletSelectionRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::select_locked_wallet(root.as_path(), request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command] #[tauri::command]
async fn splash_frontend_ready( async fn splash_frontend_ready(
app: tauri::AppHandle, app: tauri::AppHandle,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_config.rs // file: crates/ksp-app-wallet-desk/src/wallet_config.rs
// version: 1 // version: 2
//! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation. //! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation.
@@ -105,8 +105,9 @@ fn prepare_wallet_directories(resolved: &ksp_config_lib::ResolvedWalletConfig) -
} }
fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result<bool> { fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
let metadata = std::fs::metadata(path); let metadata = std::fs::symlink_metadata(path);
return match metadata { return match metadata {
std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => directory_invalid(path, "configured Wallet root cannot be a symbolic link"),
std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false), std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false),
std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"), std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => { std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
// version: 1
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
use ts_rs::TS; // rust-rules: trait-import
/// Visual lock state projected for one Wallet inventory row.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryStateDto.ts")]
pub(crate) enum WalletInventoryStateDto {
/// Eligible Wallet remains locked.
Locked,
/// Candidate could not be inspected as a valid locked Wallet.
Error,
}
/// Locked inspection outcome projected for one Wallet inventory row.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInspectionStatusDto.ts")]
pub(crate) enum WalletInspectionStatusDto {
/// Native Wallet inspection succeeded.
Valid,
/// Candidate inspection failed with a safe diagnostic.
Invalid,
}
/// Safe inventory row exposed while Wallet contents remain locked.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts")]
pub(crate) struct WalletInventoryEntryDto {
/// Safe inspection diagnostic when the candidate is invalid.
pub(crate) diagnostic: std::option::Option<crate::CommandErrorDto>,
/// Native Wallet filename without its parent path.
pub(crate) filename: String,
/// Native Wallet format version when locked inspection succeeds.
pub(crate) format_version: std::option::Option<u32>,
/// Locked inspection outcome.
pub(crate) inspection_status: WalletInspectionStatusDto,
/// Current inventory lock/error state.
pub(crate) state: WalletInventoryStateDto,
/// Root-scoped identifier accepted by selection commands.
pub(crate) wallet_id: String,
/// Whether the locked Wallet advertises an enabled VIEW slot.
pub(crate) view_enabled: std::option::Option<bool>,
}
/// Safe locked Wallet projection returned after an explicit row selection.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts")]
pub(crate) struct LockedWalletDto {
/// Native Wallet filename without its parent path.
pub(crate) filename: String,
/// Native Wallet format version.
pub(crate) format_version: u32,
/// Root-scoped identifier accepted by later Wallet Desk operations.
pub(crate) wallet_id: String,
/// Whether the locked Wallet advertises an enabled VIEW slot.
pub(crate) view_enabled: bool,
}
/// Request DTO for selecting one root-scoped Wallet inventory entry.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts")]
pub(crate) struct WalletSelectionRequestDto {
/// Root-scoped Wallet identifier returned by the inventory.
pub(crate) wallet_id: String,
}
/// Enumerates eligible native Wallet files and returns deterministic locked projections.
pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<WalletInventoryEntryDto>> {
let root_text = root.to_string_lossy().into_owned();
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, root_path = root_text.as_str(), "Wallet inventory refresh started");
let reader = tokio::fs::read_dir(root).await;
let mut reader = match reader {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(inventory_io_error(root, "effective Wallet directory cannot be enumerated", error)),
};
let mut entries = std::vec::Vec::new();
loop {
let next = reader.next_entry().await;
let entry = match next {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => break,
std::result::Result::Err(error) => return std::result::Result::Err(inventory_io_error(root, "Wallet directory entry cannot be read", error)),
};
let filename = entry.file_name();
let filename = match filename.to_str() {
std::option::Option::Some(value) if is_wallet_filename(value) => value.to_owned(),
std::option::Option::Some(_) => continue,
std::option::Option::None => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, "Wallet inventory skipped a non-UTF-8 filename");
continue;
},
};
let path = entry.path();
let metadata = tokio::fs::symlink_metadata(path.as_path()).await;
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
entries.push(invalid_inventory_entry(filename, inventory_io_error(path.as_path(), "Wallet candidate metadata cannot be inspected", error)));
continue;
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), is_symlink = metadata.file_type().is_symlink(), "Wallet inventory skipped a non-regular candidate");
continue;
}
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
match inspected {
std::result::Result::Ok(locked) => entries.push(valid_inventory_entry(filename, locked)),
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet inventory candidate failed locked inspection");
entries.push(invalid_inventory_entry(filename, error));
},
}
}
entries.sort_by(|left, right| {
return left.filename.cmp(&right.filename);
});
let invalid_count = entries
.iter()
.filter(|entry| {
return entry.inspection_status == WalletInspectionStatusDto::Invalid;
})
.count();
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, root_path = root_text.as_str(), entry_count = entries.len(), invalid_count, "Wallet inventory refresh completed");
return std::result::Result::Ok(entries);
}
/// Re-resolves and re-inspects one selected inventory identifier without exposing its full path.
pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: WalletSelectionRequestDto) -> ksp_core_lib::Result<LockedWalletDto> {
let path = resolve_wallet_path(root, request.wallet_id.as_str()).await;
let path = match path {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let locked = match locked {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = request.wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
return std::result::Result::Ok(LockedWalletDto {
filename: request.wallet_id.clone(),
format_version: locked.format_version(),
wallet_id: request.wallet_id,
view_enabled: locked.view_enabled(),
});
}
fn valid_inventory_entry(filename: String, locked: ksp_wallet_lib::LockedWalletInfo) -> WalletInventoryEntryDto {
return WalletInventoryEntryDto {
diagnostic: std::option::Option::None,
filename: filename.clone(),
format_version: std::option::Option::Some(locked.format_version()),
inspection_status: WalletInspectionStatusDto::Valid,
state: WalletInventoryStateDto::Locked,
wallet_id: filename,
view_enabled: std::option::Option::Some(locked.view_enabled()),
};
}
fn invalid_inventory_entry(filename: String, error: ksp_core_lib::Error) -> WalletInventoryEntryDto {
return WalletInventoryEntryDto {
diagnostic: std::option::Option::Some(crate::CommandErrorDto::from_error(&error)),
filename: filename.clone(),
format_version: std::option::Option::None,
inspection_status: WalletInspectionStatusDto::Invalid,
state: WalletInventoryStateDto::Error,
wallet_id: filename,
view_enabled: std::option::Option::None,
};
}
fn is_wallet_filename(filename: &str) -> bool {
let path = std::path::Path::new(filename);
let components = path.components().count();
return components == 1
&& !filename.contains('/')
&& !filename.contains('\\')
&& filename.ends_with(crate::WALLET_FILE_SUFFIX)
&& filename.len() > crate::WALLET_FILE_SUFFIX.len();
}
async fn resolve_wallet_path(root: &std::path::Path, wallet_id: &str) -> ksp_core_lib::Result<std::path::PathBuf> {
if !is_wallet_filename(wallet_id) {
return selection_invalid(wallet_id, "Wallet identifier must be one UTF-8 filename ending in .kspwallet");
}
let path = root.join(wallet_id);
let metadata = tokio::fs::symlink_metadata(path.as_path()).await;
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return selection_invalid(wallet_id, "Wallet selection no longer exists in the effective inventory directory");
},
std::result::Result::Err(error) => {
return std::result::Result::Err(inventory_io_error(path.as_path(), "Wallet selection metadata cannot be inspected", error));
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return selection_invalid(wallet_id, "Wallet selection must resolve to a regular non-symlink file");
}
return std::result::Result::Ok(path);
}
fn selection_invalid<T>(wallet_id: &str, reason: &'static str) -> ksp_core_lib::Result<T> {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id, reason, "Wallet inventory selection rejected");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SELECTION_INVALID, "Wallet inventory selection is invalid")
.with_context("wallet_id", wallet_id)
.with_context("reason", reason),
);
}
fn inventory_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
let path_text = path.to_string_lossy().into_owned();
let source_kind = std::format!("{:?}", source.kind());
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, path = path_text.as_str(), reason, source_kind = source_kind.as_str(), "Wallet inventory filesystem operation failed");
return ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_INVENTORY_FAILED, "Wallet inventory filesystem operation failed")
.with_context("path", path_text)
.with_context("reason", reason)
.with_source(source);
}
#[cfg(test)]
#[path = "../unit_tests/wallet_inventory.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk", "productName": "KSP Wallet Desk",
"version": "0.2.6-pre.3.fix.4", "version": "0.2.6-pre.4",
"identifier": "com.sasedev.ksp-app-wallet-desk", "identifier": "com.sasedev.ksp-app-wallet-desk",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 3 // version: 4
//! Desktop build, shell and Config-status contract audits for Wallet Desk. //! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -86,6 +86,28 @@ fn shell_contains_wallet_navigation_datatable_and_lock_state_icons() {
assert!(main.contains("simplebar")); assert!(main.contains("simplebar"));
} }
#[test]
fn pre_004_inventory_uses_wallet_library_and_exposes_only_locked_safe_fields() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(manifest.contains("ksp-wallet-lib = { path = \"../ksp-wallet-lib\" }"));
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file_v1"));
assert!(inventory.contains("symlink_metadata"));
assert!(inventory.contains("WALLET_FILE_SUFFIX"));
assert!(tauri.contains("list_wallets"));
assert!(tauri.contains("refresh_wallets"));
assert!(tauri.contains("select_wallet"));
assert!(main.contains("WalletInventoryEntryDto"));
assert!(main.contains("LockedWalletDto"));
assert!(main.contains("refresh_wallets"));
assert!(!inventory.contains("pubkey"));
assert!(!inventory.contains("alias"));
assert!(!inventory.contains("notes"));
}
#[test] #[test]
fn frontend_control_interactions_are_trace_logged_without_control_values() { fn frontend_control_interactions_are_trace_logged_without_control_values() {
let root = app_root(); let root = app_root();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs // file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 1 // version: 2
//! Static desktop security contracts for the Wallet Desk pre.002 shell. //! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -36,3 +36,13 @@ fn frontend_shell_uses_no_browser_native_dialog_or_secret_storage() {
assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}"); assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}");
} }
} }
#[test]
fn locked_inventory_frontend_never_receives_protected_wallet_identity_fields() {
let root = app_root();
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
assert!(!inventory.contains("pubkey:"));
assert!(!inventory.contains("alias:"));
assert!(!inventory.contains("notes:"));
assert!(!inventory.contains("password:"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_config.rs // file: crates/ksp-app-wallet-desk/unit_tests/wallet_config.rs
// version: 1 // version: 2
#[test] #[test]
fn directory_preparation_creates_missing_root_and_nested_profile_path() { fn directory_preparation_creates_missing_root_and_nested_profile_path() {
@@ -37,6 +37,30 @@ fn directory_preparation_rejects_existing_non_directory_root() {
cleanup_fixture(root.as_path()); cleanup_fixture(root.as_path());
} }
#[cfg(unix)]
#[test]
fn global_wallet_root_rejects_symbolic_link() {
let root = fixture_path("root-symlink");
let outside = fixture_path("root-symlink-outside");
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
let outside_created = std::fs::create_dir_all(outside.as_path());
assert!(outside_created.is_ok());
if outside_created.is_ok() {
let linked = std::os::unix::fs::symlink(outside.as_path(), root.as_path());
assert!(linked.is_ok());
if linked.is_ok() {
let result = super::ensure_global_wallet_root(root.as_path());
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_DIRECTORY_INVALID);
}
}
}
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
}
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn profile_directory_preparation_rejects_symbolic_link_components() { fn profile_directory_preparation_rejects_symbolic_link_components() {

View File

@@ -0,0 +1,124 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_inventory.rs
// version: 1
const VALID_WALLET: &[u8] = include_bytes!("../../ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json");
#[test]
fn inventory_lists_regular_wallets_reports_invalid_candidates_and_ignores_other_entries() {
let root = fixture_path("inventory");
cleanup_fixture(root.as_path());
let created = std::fs::create_dir_all(root.as_path());
assert!(created.is_ok(), "inventory fixture root should be creatable: {created:?}");
if created.is_ok() {
assert!(std::fs::write(root.join("beta.kspwallet"), VALID_WALLET).is_ok());
assert!(std::fs::write(root.join("alpha.kspwallet"), b"not-json").is_ok());
assert!(std::fs::write(root.join("ignored.txt"), VALID_WALLET).is_ok());
assert!(std::fs::create_dir(root.join("directory.kspwallet")).is_ok());
let runtime = tokio::runtime::Builder::new_current_thread().build();
assert!(runtime.is_ok(), "test runtime should be constructible: {runtime:?}");
if let std::result::Result::Ok(runtime) = runtime {
let inventory = runtime.block_on(crate::list_wallet_inventory(root.as_path()));
assert!(inventory.is_ok(), "inventory should complete: {inventory:?}");
if let std::result::Result::Ok(entries) = inventory {
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].filename, "alpha.kspwallet");
assert_eq!(entries[0].inspection_status, crate::WalletInspectionStatusDto::Invalid);
assert_eq!(entries[0].state, crate::WalletInventoryStateDto::Error);
assert!(entries[0].diagnostic.is_some());
assert_eq!(entries[1].filename, "beta.kspwallet");
assert_eq!(entries[1].inspection_status, crate::WalletInspectionStatusDto::Valid);
assert_eq!(entries[1].state, crate::WalletInventoryStateDto::Locked);
assert_eq!(entries[1].format_version, std::option::Option::Some(1));
assert!(entries[1].view_enabled.is_some());
assert!(entries[1].diagnostic.is_none());
}
}
}
cleanup_fixture(root.as_path());
}
#[test]
fn selection_rejects_traversal_and_reinspects_one_valid_locked_wallet() {
let root = fixture_path("selection");
cleanup_fixture(root.as_path());
let created = std::fs::create_dir_all(root.as_path());
assert!(created.is_ok(), "selection fixture root should be creatable: {created:?}");
if created.is_ok() {
assert!(std::fs::write(root.join("selected.kspwallet"), VALID_WALLET).is_ok());
let runtime = tokio::runtime::Builder::new_current_thread().build();
assert!(runtime.is_ok(), "test runtime should be constructible: {runtime:?}");
if let std::result::Result::Ok(runtime) = runtime {
let rejected = runtime
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "../selected.kspwallet".to_owned() }));
assert!(rejected.is_err());
if let std::result::Result::Err(error) = rejected {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
}
let rejected_backslash = runtime
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "..\\selected.kspwallet".to_owned() }));
assert!(rejected_backslash.is_err());
if let std::result::Result::Err(error) = rejected_backslash {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
}
let selected =
runtime.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "selected.kspwallet".to_owned() }));
assert!(selected.is_ok(), "valid locked Wallet should be selectable: {selected:?}");
if let std::result::Result::Ok(selected) = selected {
assert_eq!(selected.wallet_id, "selected.kspwallet");
assert_eq!(selected.filename, "selected.kspwallet");
assert_eq!(selected.format_version, 1);
}
}
}
cleanup_fixture(root.as_path());
}
#[cfg(unix)]
#[test]
fn inventory_and_selection_exclude_symbolic_link_wallet_entries() {
let root = fixture_path("symlink-inventory");
let outside = fixture_path("symlink-wallet");
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
let root_created = std::fs::create_dir_all(root.as_path());
let outside_written = std::fs::write(outside.as_path(), VALID_WALLET);
assert!(root_created.is_ok());
assert!(outside_written.is_ok());
if root_created.is_ok() && outside_written.is_ok() {
let linked = std::os::unix::fs::symlink(outside.as_path(), root.join("linked.kspwallet"));
assert!(linked.is_ok());
if linked.is_ok() {
let runtime = tokio::runtime::Builder::new_current_thread().build();
assert!(runtime.is_ok());
if let std::result::Result::Ok(runtime) = runtime {
let inventory = runtime.block_on(crate::list_wallet_inventory(root.as_path()));
assert_eq!(inventory.as_ref().ok().map(std::vec::Vec::len), std::option::Option::Some(0));
let selected = runtime
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "linked.kspwallet".to_owned() }));
assert!(selected.is_err());
if let std::result::Result::Err(error) = selected {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
}
}
}
}
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
}
fn fixture_path(name: &str) -> std::path::PathBuf {
return std::env::temp_dir().join(std::format!("ksp-wallet-desk-{name}-{}", std::process::id()));
}
fn cleanup_fixture(path: &std::path::Path) {
let metadata = std::fs::symlink_metadata(path);
match metadata {
std::result::Result::Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {
let _ = std::fs::remove_dir_all(path);
},
std::result::Result::Ok(_) => {
let _ = std::fs::remove_file(path);
},
std::result::Result::Err(_) => {},
}
}

180
deltas/0.2.6/pre.004.md Normal file
View File

@@ -0,0 +1,180 @@
<!-- file: deltas/0.2.6/pre.004.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.004` — inventory `.kspwallet` et inspection locked
## Base requise
```text
0.2.6-pre.003-fix.004 appliquée
workspace.package.version = 0.2.6-pre.3.fix.4
```
Les preuves opérateur de `pre.003-fix.004` sont vertes : `cargo fmt --all`, audit structurel Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-app-config-desk`, `cargo test --workspace` et `cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json`. Le runtime utilise le profil composite Logging `supertrace` et démarre correctement.
## Type de livraison
```text
ksp-general-0.2.6-pre.004.zip
```
Cette tranche ajoute une dépendance interne Wallet et du runtime Rust/frontend ; le signal technique devient :
```text
workspace.package.version = 0.2.6-pre.4
commit = v0.2.6-pre.004
```
Aucun tag stable.
## Objet
Brancher l'inventory réel des fichiers natifs `.kspwallet` dans Wallet Desk sans introduire encore de password, de handle VIEW/OWNER ni de projection d'identité protégée.
## Frontière Wallet
`ksp-app-wallet-desk` dépend désormais directement de `ksp-wallet-lib` et délègue l'inspection locked à :
```text
ksp_wallet_lib::inspect_locked_wallet_file_v1
```
Aucune crypto, parser `.kspwallet`, keypair ou signature n'est réimplémenté dans l'application.
## Inventory filesystem
La racine unique est :
```text
ResolvedWalletConfig::effective_wallets_directory
```
Politique :
```text
read_dir asynchrone non récursif
filename UTF-8
suffixe exact .kspwallet
un seul composant filename
slash et backslash rejetés pour les sélections frontend
fichiers réguliers uniquement
symlinks d'entrée exclus
répertoires même nommés *.kspwallet exclus
tri déterministe par filename
```
Le bootstrap Config durcit aussi la racine globale : un `wallets_directory` existant sous forme de symlink est désormais refusé au lieu d'être suivi par `metadata()`.
Une erreur de lecture de la racine fait échouer la commande avec `wallet_desk.wallet_inventory_failed`. Une entrée `.kspwallet` présente mais invalide ne fait pas échouer tout l'inventory : elle reste visible avec état `error`, inspection `invalid` et `CommandErrorDto` sans context/source.
## DTOs locked
La tranche ajoute :
```text
WalletInventoryEntryDto
WalletInventoryStateDto
WalletInspectionStatusDto
LockedWalletDto
WalletSelectionRequestDto
```
Une ligne valide expose uniquement :
```text
wallet_id / filename
format_version
view_enabled
state = locked
inspection_status = valid
```
Une ligne invalide expose filename + diagnostic sûr. Aucun DTO ne porte Pubkey, alias, notes, password, path complet ou handle Wallet.
## Commandes Tauri
```text
list_wallets
refresh_wallets
select_wallet
```
`select_wallet` reçoit uniquement un `wallet_id`/filename, reconstruit le path sous la racine effective, rejette traversal/séparateurs/symlink/non-file puis relance l'inspection locked avant de répondre. La sélection n'installe pas encore un `WalletSession` durable ; celui-ci reste `pre.005`.
## Frontend
Le tableau Wallet devient un vrai DataTable alimenté par le backend :
```text
État -> fa-lock ou fa-triangle-exclamation
Filename -> texte DOM sûr
Format -> version ou —
VIEW -> enabled / disabled / —
Inspection -> Valide ou code diagnostic sûr
```
Le bouton Refresh appelle `refresh_wallets`. La sélection d'une ligne valide appelle `select_wallet` et affiche seulement filename/format/VIEW dans la carte « Wallet courant ». Les lignes invalides restent visibles mais ne sont pas sélectionnables. Un refresh purge la projection sélectionnée frontend afin de ne pas conserver une vue stale.
Le rendu utilise `textContent`/création DOM pour les valeurs issues du filesystem ; aucun filename n'est injecté par HTML.
## Logging
Les opérations suivantes sont instrumentées :
```text
inventory refresh start/completion
invalid locked inspection
non-regular/symlink skip
selection accepted/rejected
frontend inventory load/render/refresh/row selection
```
Le profil composite reste `supertrace`; aucun changement de `std.logging` n'est requis.
## Tests
Les canaris ajoutés couvrent :
```text
valid .kspwallet + invalid .kspwallet dans le même inventory
wrong extension ignorée
subdirectory *.kspwallet ignoré
tri déterministe
traversal slash/backslash rejeté
symlink Wallet ignoré et non sélectionnable sur Unix
root wallets_directory symlink rejeté sur Unix
DTO locked sans Pubkey/alias/notes/password
commands Tauri list/refresh/select présentes
frontend DataTable réellement branché sur les DTOs Wallet
```
## Dépendances
Cargo :
```text
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
tokio features += fs, rt
```
Aucune dépendance npm et aucun script npm ne changent. **Aucune commande npm directe n'est nécessaire pour cette tranche.**
## Validation opérateur requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk
cargo test --workspace
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre004 cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
```
Pour la vérification UI, placer si souhaité un ou plusieurs `.kspwallet` existants dans `var/wallet-desk-pre004`, ainsi qu'un fichier `invalid.kspwallet` non valide, puis tester recherche/tri, Refresh et sélection des lignes valides.
Toujours aucun `cargo tauri build` à ce stade.
## Suite
Après validation de `pre.004`, `pre.005` introduira la création de Wallet et le lifecycle/session Rust sans avancer encore la balance HTTP.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md --> <!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 62 --> <!-- version: 63 -->
# Séquence des releases fonctionnelles KSP # Séquence des releases fonctionnelles KSP
@@ -430,7 +430,7 @@ Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_
`0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` et chaque profil peut définir un `wallets_subdirectory` relatif pour isoler tests, temporaires ou futurs scénarios. Wallet Desk prépare/crée le répertoire effectif après résolution Config et journalise le succès en debug ou l'échec en error. Le gabarit Config Desk est réutilisé avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar et `resize-observer-polyfill`. L'inventory reste non récursive `.kspwallet`, sans symlink d'entrée et root-scoped; les handles VIEW/OWNER restent exclusivement côté Rust. L'ouverture accepte un password éphémère saisi ou une tentative explicite de secrets Config `KSP_SECRET_WALLET_PASS_*`, dont ni nom ni valeur ne traversent le frontend. `getBalance` utilise la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier délèguent à Wallet. La CSP reste `null` par défaut comme dans Config Desk et n'est réauditée qu'en présence d'un besoin WebView concret. Le forecast souple détaillé s'étend jusqu'à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. `0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` et chaque profil peut définir un `wallets_subdirectory` relatif pour isoler tests, temporaires ou futurs scénarios. Wallet Desk prépare/crée le répertoire effectif après résolution Config et journalise le succès en debug ou l'échec en error. Le gabarit Config Desk est réutilisé avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar et `resize-observer-polyfill`. L'inventory reste non récursive `.kspwallet`, sans symlink d'entrée et root-scoped; les handles VIEW/OWNER restent exclusivement côté Rust. L'ouverture accepte un password éphémère saisi ou une tentative explicite de secrets Config `KSP_SECRET_WALLET_PASS_*`, dont ni nom ni valeur ne traversent le frontend. `getBalance` utilise la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier délèguent à Wallet. La CSP reste `null` par défaut comme dans Config Desk et n'est réauditée qu'en présence d'un besoin WebView concret. Le forecast souple détaillé s'étend jusqu'à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par lapplication. Aucun inventory ou fichier `.kspwallet` nest encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner dinventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable denvironnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment didentifiant dun vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend nest modifiée. `0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par lapplication. Aucun inventory ou fichier `.kspwallet` nest encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner dinventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable denvironnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment didentifiant dun vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend nest modifiée. `0.2.6-pre.004` ajoute linventory `.kspwallet` réel sous `effective_wallets_directory` : lecture non récursive, extension exacte, exclusion des symlinks et non-fichiers, inspection par `ksp-wallet-lib::inspect_locked_wallet_file_v1`, lignes invalides conservées avec diagnostic sûr, DTOs locked sans identité protégée, DataTable/refresh actifs et sélection filename root-scoped revalidée côté Rust. Aucun handle VIEW/OWNER ni password nest introduit dans cette tranche.
## `0.2.7` — WebSocket Solana standard ## `0.2.7` — WebSocket Solana standard

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md --> <!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
<!-- version: 10 --> <!-- version: 11 -->
# Plan `0.2.6` — Wallet Desk # Plan `0.2.6` — Wallet Desk
@@ -1307,6 +1307,27 @@ refresh manuel + après opérations locales futures
À la fin de cette tranche, l'application sait lister et sélectionner un wallet sans révéler son identité protégée. À la fin de cette tranche, l'application sait lister et sélectionner un wallet sans révéler son identité protégée.
État matérialisé par `pre.004` :
```text
ksp-app-wallet-desk -> ksp-wallet-lib ajouté explicitement
effective_wallets_directory comme unique racine d'inventory
read_dir asynchrone non récursif
filename UTF-8 + suffixe exact .kspwallet
regular file uniquement ; symlink/non-file exclus
inspection via inspect_locked_wallet_file_v1
ligne invalide conservée avec CommandErrorDto sûr
WalletInventoryEntryDto / LockedWalletDto / WalletSelectionRequestDto
list_wallets / refresh_wallets / select_wallet
DataTable réel + refresh + sélection
fa-lock pour valid locked ; fa-triangle-exclamation pour invalid
selection revalidée côté Rust, filename single-component uniquement
root symlink désormais rejeté lors de la préparation Config
aucun Pubkey / alias / notes / password dans la projection locked
```
La sélection de `pre.004` reste une projection locked sans handle durable : le `WalletSession` Rust et les handles OWNER/VIEW arrivent en `pre.005`/`pre.006`. Un refresh invalide donc la sélection frontend pour éviter de conserver une projection potentiellement stale.
### `pre.005` — création Wallet et lifecycle de session Rust ### `pre.005` — création Wallet et lifecycle de session Rust
Objectifs : Objectifs :