diff --git a/Cargo.toml b/Cargo.toml
index 8185cf7..081a269 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
# file: Cargo.toml
-# version: 164
+# version: 165
[workspace]
resolver = "3"
-members = ["crates/ksp-app-config-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]
-version = "0.2.6-pre.1"
+version = "0.2.6-pre.2"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
diff --git a/ROADMAP.md b/ROADMAP.md
index 44e6c15..e9c8a25 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,5 +1,5 @@
-
+
# 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.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 l’audit 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. Le gate `pre.001` retient `cfg.std.wallet` + composite Wallet Desk, inventory `.kspwallet` root-scoped/symlink-safe, handles VIEW/OWNER côté Rust, identité autorisée + `getBalance`, administration OWNER/import/export déjà possédés par Wallet, CSP/capabilities Tauri minimales et un forecast regranularisé jusqu’à `pre.013`.
+- [ ] `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. Les tranches suivantes ajoutent `cfg.std.wallet`, 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.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.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
diff --git a/crates/ksp-app-wallet-desk/Cargo.toml b/crates/ksp-app-wallet-desk/Cargo.toml
new file mode 100644
index 0000000..3cc1989
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/Cargo.toml
@@ -0,0 +1,40 @@
+# file: crates/ksp-app-wallet-desk/Cargo.toml
+# version: 1
+
+[package]
+name = "ksp-app-wallet-desk"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+publish.workspace = true
+
+[lib]
+name = "ksp_app_wallet_desk_lib"
+path = "src/lib.rs"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[[bin]]
+name = "ksp-app-wallet-desk"
+path = "src/main.rs"
+
+[build-dependencies]
+tauri-build.workspace = true
+
+[dependencies]
+chrono = { workspace = true, features = ["std", "now"] }
+fs2.workspace = true
+ksp-config-lib = { path = "../ksp-config-lib" }
+ksp-core-lib = { path = "../ksp-core-lib" }
+ksp-logging-lib = { path = "../ksp-logging-lib" }
+serde = { workspace = true, features = ["derive"] }
+tauri.workspace = true
+tauri-plugin-tracing.workspace = true
+tokio = { workspace = true, features = ["time"] }
+ts-rs.workspace = true
+
+[dev-dependencies]
+serde_json.workspace = true
+
+[lints]
+workspace = true
diff --git a/crates/ksp-app-wallet-desk/build.rs b/crates/ksp-app-wallet-desk/build.rs
new file mode 100644
index 0000000..6bf97a1
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/build.rs
@@ -0,0 +1,12 @@
+// file: crates/ksp-app-wallet-desk/build.rs
+// version: 1
+
+//! Build script for the KSP wallet desktop application.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+
+fn main() {
+ tauri_build::build()
+}
diff --git a/crates/ksp-app-wallet-desk/capabilities/default.json b/crates/ksp-app-wallet-desk/capabilities/default.json
new file mode 100644
index 0000000..e43d672
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/capabilities/default.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Default capability for KSP Wallet Desk",
+ "windows": [
+ "splash",
+ "main"
+ ],
+ "permissions": [
+ "core:default",
+ "tracing:default"
+ ]
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf b/crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf
new file mode 100644
index 0000000..e558a29
Binary files /dev/null and b/crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf differ
diff --git a/crates/ksp-app-wallet-desk/frontend/fonts/README.md b/crates/ksp-app-wallet-desk/frontend/fonts/README.md
new file mode 100644
index 0000000..e40fda6
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/fonts/README.md
@@ -0,0 +1,25 @@
+
+
+
+# Fonts du splash Wallet Desk
+
+Wallet Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et conserve la police locale :
+
+```text
+DOS_Amazigh.ttf
+font-family: Dos Amazigh
+```
+
+Destination :
+
+```text
+crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf
+```
+
+Empreinte SHA-256 de l'asset repris du gabarit :
+
+```text
+a01ff4b5a0d699db7cefcf5336bf0e379ff6fbf0d096087571a13c8782f9aa02
+```
+
+`splash.scss` déclare `Dos Amazigh` avec `@font-face` et l'applique au titre `#app-name`.
diff --git a/crates/ksp-app-wallet-desk/frontend/imgs/logo.png b/crates/ksp-app-wallet-desk/frontend/imgs/logo.png
new file mode 100644
index 0000000..94e89ca
Binary files /dev/null and b/crates/ksp-app-wallet-desk/frontend/imgs/logo.png differ
diff --git a/crates/ksp-app-wallet-desk/frontend/imgs/splash.png b/crates/ksp-app-wallet-desk/frontend/imgs/splash.png
new file mode 100644
index 0000000..a27d1eb
Binary files /dev/null and b/crates/ksp-app-wallet-desk/frontend/imgs/splash.png differ
diff --git a/crates/ksp-app-wallet-desk/frontend/main.html b/crates/ksp-app-wallet-desk/frontend/main.html
new file mode 100644
index 0000000..1fe3e66
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/main.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+ Wallet Desk — Dashboard
+
+
+
+
+

+
+ Wallet Desk —
+ Initialisation du shell…
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard
+
Fondation UI de Wallet Desk — fonctionnalités Wallet branchées dans les tranches suivantes.
+
+
0.2.6-pre.002
+
+
+
+
+
+
+
+
+ - Version
- —
+ - Phase
- —
+ - Documents Config enregistrés
- —
+ - Profil Logging
- —
+ - Fallback Logging
- —
+
+
+
+
+
+
+
+
+
+
Aucun wallet sélectionné.
+
+
+
+
+
+
+
+
+
+
+ | État | Filename | Format | VIEW | Inspection |
+
+
+
+
+
+
Create / Import sera branché après la fondation Config et inventory.
+
Les détails protégés seront affichés uniquement après VIEW/OWNER.
+
Les opérations OWNER seront ajoutées après le MVP identité + balance.
+
Les diagnostics Config/Wallet/Transport seront enrichis progressivement.
+
+
+
+
+
+
+
+
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/_app.scss b/crates/ksp-app-wallet-desk/frontend/sass/_app.scss
new file mode 100644
index 0000000..d12c44e
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/_app.scss
@@ -0,0 +1,94 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/_app.scss
+// version: 1
+
+$app-header-height: 72px;
+$app-footer-height: 42px;
+$app-sidebar-width: 250px;
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ overflow: hidden;
+ background: $gray-100;
+}
+
+.app-header {
+ position: fixed;
+ inset: 0 0 auto;
+ height: $app-header-height;
+ z-index: 1020;
+ border-bottom: 2px solid rgba($primary, 0.3);
+}
+
+.app-footer {
+ position: fixed;
+ inset: auto 0 0;
+ height: $app-footer-height;
+ z-index: 1020;
+ border-top: 2px solid rgba($primary, 0.3);
+}
+
+.app-main {
+ position: relative;
+ height: calc(100vh - $app-header-height - $app-footer-height);
+ margin-top: $app-header-height;
+ margin-bottom: $app-footer-height;
+ overflow: hidden;
+}
+
+.app-logo {
+ display: block;
+ width: auto;
+ height: 42px;
+}
+
+.app-shell-status,
+.app-runtime-list dd,
+#walletInventoryTable td:nth-child(2),
+#walletInventoryTable td:nth-child(3) {
+ font-family: var(--bs-font-monospace);
+}
+
+.app-sidebar {
+ width: $app-sidebar-width;
+ min-width: $app-sidebar-width;
+ max-width: $app-sidebar-width;
+}
+
+.app-sidebar-scroll,
+.app-content {
+ height: 100%;
+ max-height: 100%;
+}
+
+.app-sidebar .nav-link {
+ white-space: nowrap;
+}
+
+.app-placeholder {
+ min-height: 320px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ padding: 2rem;
+ border: 1px dashed var(--bs-border-color);
+ border-radius: var(--bs-border-radius-lg);
+ color: var(--bs-secondary-color);
+ background: var(--bs-body-bg);
+ text-align: center;
+}
+
+.app-placeholder i {
+ font-size: 2.5rem;
+}
+
+#walletInventoryTable tbody tr {
+ cursor: pointer;
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss b/crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
new file mode 100644
index 0000000..60c5e93
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
@@ -0,0 +1,160 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
+// version: 1
+
+// Pulse 5.3.8
+// Bootswatch
+
+
+// Variables
+
+// Buttons
+
+.btn {
+
+ &:focus,
+ &:active,
+ &:active:focus,
+ &.active:focus {
+ outline: none;
+ }
+
+ &-secondary {
+ color: $gray-900;
+ background-color: $white;
+ border-color: #ccc;
+
+ &:hover {
+ color: $gray-900;
+ background-color: $gray-300;
+ border-color: $gray-500;
+ }
+
+ &.disabled {
+ color: tint-color($gray-900, 5%);
+ background-color: $white;
+ border-color: tint-color(#ccc, 5%);
+ }
+ }
+
+ &-warning {
+ color: $white;
+ }
+
+ &-primary:focus {
+ box-shadow: 0 0 5px tint-color($primary, 10%);
+ }
+
+ &-secondary:focus {
+ box-shadow: 0 0 5px $gray-400;
+ }
+
+ &-success:focus {
+ box-shadow: 0 0 5px tint-color($success, 10%);
+ }
+
+ &-info:focus {
+ box-shadow: 0 0 5px tint-color($info, 10%);
+ }
+
+ &-warning:focus {
+ box-shadow: 0 0 5px tint-color($warning, 10%);
+ }
+
+ &-danger:focus {
+ box-shadow: 0 0 5px tint-color($danger, 10%);
+ }
+
+ &.disabled:focus {
+ box-shadow: none;
+ }
+}
+
+// Tables
+
+.table .thead-dark th {
+ background-color: $secondary;
+ border-color: $table-border-color;
+}
+
+.table-primary,
+.table-secondary,
+.table-success,
+.table-warning,
+.table-danger,
+.table-info,
+.table-light {
+ --#{$prefix}table-color: #{$body-color};
+}
+
+// Forms
+
+.form-control:focus {
+ box-shadow: 0 0 5px rgba(100, 65, 164, .4);
+}
+
+// Navs
+
+.nav-tabs {
+
+ .nav-link,
+ .nav-link.active {
+ border-width: 0 0 1px;
+ }
+
+ .nav-link:hover,
+ .nav-link.active,
+ .nav-link.active:hover,
+ .nav-link.active:focus {
+ border-bottom: 1px solid $primary;
+ }
+
+ .nav-item+.nav-item {
+ margin-left: 0;
+ }
+}
+
+.breadcrumb {
+ &-item.active {
+ color: $gray-700;
+ }
+}
+
+// Indicators
+
+.badge {
+ &.bg-light {
+ color: $dark;
+ }
+}
+
+// Progress bars
+
+.progress {
+ height: 8px;
+}
+
+// Containers
+
+.list-group {
+ &-item {
+ color: rgba(255, 255, 255, .8);
+
+ &.active,
+ &:hover,
+ &:focus {
+ color: $white;
+ }
+
+ &.active {
+ font-weight: 700;
+
+ &:hover {
+ background-color: $list-group-hover-bg;
+ }
+ }
+
+ &.disabled:hover {
+ color: $list-group-disabled-color;
+ }
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss b/crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss
new file mode 100644
index 0000000..681b874
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss
@@ -0,0 +1,19 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss
+// version: 1
+
+//@use '@fortawesome/fontawesome-free/scss/variables' with (
+// // customizing $font-path - make sure it points to where your webfonts are stored in your project
+// $font-path: '../webfonts',
+//);
+@use '@fortawesome/fontawesome-free/scss/variables' with (
+ // use fonts from @fortawesome/fontawesome-free
+ $font-path: '@fortawesome/fontawesome-free/webfonts',
+);
+// load Font Awesome core
+@use '@fortawesome/fontawesome-free/scss/fontawesome';
+
+// load and make available Font Awesome helpers (mixins, functions, and variables)
+@use '@fortawesome/fontawesome-free/scss/fa' as fa;
+@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
+@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
+@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss b/crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss
new file mode 100644
index 0000000..c6ef9f5
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss
@@ -0,0 +1,248 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss
+// version: 1
+
+/* Rtl support */
+[data-simplebar] {
+ position: relative;
+ flex-direction: column;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ align-content: flex-start;
+ align-items: flex-start;
+}
+
+.simplebar-wrapper {
+ overflow: hidden;
+ width: inherit;
+ height: inherit;
+ max-width: inherit;
+ max-height: inherit;
+}
+
+.simplebar-mask {
+ direction: inherit;
+ position: absolute;
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ width: auto !important;
+ height: auto !important;
+// z-index: 0;
+ inset: 0;
+}
+
+.simplebar-offset {
+ direction: inherit !important;
+ box-sizing: inherit !important;
+ resize: none !important;
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ padding: 0;
+ margin: 0;
+ -webkit-overflow-scrolling: touch;
+ inset: 0;
+}
+
+.simplebar-content-wrapper {
+ direction: inherit;
+ box-sizing: border-box !important;
+ position: relative;
+ display: block;
+ height: 100%;
+ width: auto;
+ max-width: 100%;
+ max-height: 100%;
+ overflow: auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+ }
+}
+
+.simplebar-hide-scrollbar {
+ &::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+ }
+
+ position: fixed;
+ left: 0;
+ visibility: hidden;
+ overflow-y: scroll;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.simplebar-content {
+ &:before {
+ content: ' ';
+ display: table;
+ }
+
+ &:after {
+ content: ' ';
+ display: table;
+ }
+}
+
+.simplebar-placeholder {
+ max-height: 100%;
+ max-width: 100%;
+ width: 100%;
+ pointer-events: none;
+}
+
+.simplebar-height-auto-observer-wrapper {
+ box-sizing: inherit !important;
+ height: 100%;
+ width: 100%;
+ max-width: 1px;
+ position: relative;
+ float: left;
+ max-height: 1px;
+ overflow: hidden;
+// z-index: -1;
+ padding: 0;
+ margin: 0;
+ pointer-events: none;
+ flex-grow: inherit;
+ flex-shrink: 0;
+ flex-basis: 0;
+}
+
+.simplebar-height-auto-observer {
+ box-sizing: inherit;
+ display: block;
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 1000%;
+ width: 1000%;
+ min-height: 1px;
+ min-width: 1px;
+ overflow: hidden;
+ pointer-events: none;
+// z-index: -1;
+}
+
+.simplebar-track {
+// z-index: 1;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+[data-simplebar].simplebar-dragging {
+ pointer-events: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ .simplebar-content {
+ pointer-events: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ .simplebar-track {
+ pointer-events: all;
+ }
+}
+
+.simplebar-scrollbar {
+ position: absolute;
+ left: 0;
+ right: 0;
+ min-height: 10px;
+
+
+ &:before {
+ position: absolute;
+ content: '';
+ background: black;
+ border-radius: 7px;
+ left: 2px;
+ right: 2px;
+ opacity: 0;
+ transition: opacity 0.2s 0.5s linear;
+ top: 2px;
+ bottom: 2px;
+ }
+}
+
+.simplebar-scrollbar.simplebar-visible {
+ &:before {
+ opacity: 0.5;
+ transition-delay: 0s;
+ transition-duration: 0s;
+ }
+}
+
+.simplebar-track.simplebar-vertical {
+ top: 0;
+ width: 11px;
+}
+
+.simplebar-track.simplebar-horizontal {
+ left: 0;
+ height: 11px;
+
+ .simplebar-scrollbar {
+ right: auto;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ min-height: 0;
+ min-width: 10px;
+ width: auto;
+ }
+}
+
+[data-simplebar-direction='rtl'] {
+ .simplebar-track.simplebar-vertical {
+ right: auto;
+ left: 0;
+ }
+}
+
+.simplebar-dummy-scrollbar-size {
+ direction: rtl;
+ position: fixed;
+ opacity: 0;
+ visibility: hidden;
+ height: 500px;
+ width: 500px;
+ overflow-y: hidden;
+ overflow-x: scroll;
+ -ms-overflow-style: scrollbar !important;
+
+ >div {
+ width: 200%;
+ height: 200%;
+ margin: 10px 0;
+ }
+}
+
+.simplebar-hover {
+ cursor: pointer;
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/_variables.scss b/crates/ksp-app-wallet-desk/frontend/sass/_variables.scss
new file mode 100644
index 0000000..ddfaf1d
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/_variables.scss
@@ -0,0 +1,95 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/_variables.scss
+// version: 1
+
+// Pulse 5.3.8
+// Bootswatch
+
+$theme: "pulse" !default;
+
+//
+// Color system
+//
+
+$white: #fff !default;
+$gray-100: #fafafa !default;
+$gray-200: #f9f8fc !default;
+$gray-300: #ededed !default;
+$gray-400: #cbc8d0 !default;
+$gray-500: #adb5bd !default;
+$gray-600: #868e96 !default;
+$gray-700: #444 !default;
+$gray-800: #343a40 !default;
+$gray-900: #17141f !default;
+$black: #000 !default;
+
+$blue: #007bff !default;
+$indigo: #6610f2 !default;
+$purple: #593196 !default;
+$pink: #e83e8c !default;
+$red: #fc3939 !default;
+$orange: #fd7e14 !default;
+$yellow: #efa31d !default;
+$green: #13b955 !default;
+$teal: #20c997 !default;
+$cyan: #009cdc !default;
+
+$primary: $purple !default;
+$secondary: #a991d4 !default;
+$success: $green !default;
+$info: $cyan !default;
+$warning: $yellow !default;
+$danger: $red !default;
+$light: $gray-200 !default;
+$dark: $gray-900 !default;
+
+$min-contrast-ratio: 2.1 !default;
+
+// Options
+
+$enable-rounded: false !default;
+
+// Body
+
+$body-color: $gray-700 !default;
+
+// Links
+
+$link-hover-color: $primary !default;
+
+// Tables
+
+$table-color: initial !default;
+
+$table-border-color: rgba(0, 0, 0, .05) !default;
+
+// Forms
+
+$input-focus-border-color: $primary !default;
+
+// Dropdowns
+
+$dropdown-link-hover-color: $white !default;
+$dropdown-link-hover-bg: $primary !default;
+
+// Navs
+
+$nav-tabs-border-color: $gray-300 !default;
+$nav-tabs-link-hover-border-color: $primary !default;
+
+// Navbar
+
+$navbar-padding-y: 1.2rem !default;
+
+// Progress bars
+
+$progress-bg: $gray-300 !default;
+$progress-bar-bg: $primary !default;
+
+// List group
+
+$list-group-bg: $gray-900 !default;
+$list-group-border-color: transparent !default;
+$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
+$list-group-active-color: $white !default;
+$list-group-active-bg: $list-group-bg !default;
+$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/main.scss b/crates/ksp-app-wallet-desk/frontend/sass/main.scss
new file mode 100644
index 0000000..811a2ce
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/main.scss
@@ -0,0 +1,10 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/main.scss
+// version: 1
+
+@import "bootstrap/scss/functions";
+@import "variables";
+@import "fontawesome";
+@import "simplebar";
+@import "bootstrap/scss/bootstrap";
+@import "bootswatch";
+@import "app";
diff --git a/crates/ksp-app-wallet-desk/frontend/sass/splash.scss b/crates/ksp-app-wallet-desk/frontend/sass/splash.scss
new file mode 100644
index 0000000..31d75e5
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/sass/splash.scss
@@ -0,0 +1,65 @@
+// file: crates/ksp-app-wallet-desk/frontend/sass/splash.scss
+// version: 1
+
+@font-face {
+ font-family: "Dos Amazigh";
+ src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
+ font-weight: normal;
+ font-style: normal;
+ font-display: swap;
+}
+
+body {
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ font-family: Arial, sans-serif;
+}
+
+#splash-container {
+ position: relative;
+ width: 960px;
+ height: 637px;
+ opacity: 0;
+}
+
+#splash-image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+#app-name {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 2;
+ transform: translate(-50%, -50%);
+ color: #fff;
+ font-family: "Dos Amazigh", sans-serif;
+ font-size: 76px;
+ font-weight: bold;
+ text-align: center;
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+}
+
+#splash-status {
+ position: absolute;
+ right: 1rem;
+ bottom: 1rem;
+ left: 1rem;
+ z-index: 3;
+ padding: 0.55rem 0.75rem;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.35);
+ font-family: monospace;
+ font-size: 0.82rem;
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/splash.html b/crates/ksp-app-wallet-desk/frontend/splash.html
new file mode 100644
index 0000000..afae8a8
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/splash.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Chargement — KSP Wallet Desk
+
+
+
+

+
Wallet Desk
+
Initialisation…
+
+
+
+
diff --git a/crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts b/crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
new file mode 100644
index 0000000..caf8a37
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
@@ -0,0 +1,110 @@
+// file: crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
+// version: 1
+
+import { invoke } from "@tauri-apps/api/core";
+import type { FrontendLogPayloadDto } from "./bindings/ksp_app_wallet_desk/frontend_logging/FrontendLogPayloadDto.ts";
+
+export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
+export type FrontendLogTargetId = "frontend" | "main" | "splash";
+
+type ConsoleMethod = (...items: unknown[]) => void;
+
+const originalConsole = {
+ trace: console.trace.bind(console),
+ debug: console.debug.bind(console),
+ log: console.log.bind(console),
+ info: console.info.bind(console),
+ warn: console.warn.bind(console),
+ error: console.error.bind(console),
+};
+
+function stringifyItem(item: unknown): string {
+ if (item instanceof Error) {
+ return item.stack ?? item.message;
+ }
+ if (typeof item === "string") {
+ return item;
+ }
+ try {
+ const serialized = JSON.stringify(item);
+ return serialized ?? String(item);
+ } catch {
+ return String(item);
+ }
+}
+
+function formatMessage(items: unknown[]): string {
+ return items.map(item => stringifyItem(item)).join(" ");
+}
+
+async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise {
+ const payload: FrontendLogPayloadDto = {
+ level,
+ targetId,
+ message,
+ };
+ await invoke("emit_frontend_log", { payload });
+}
+
+function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
+ return level === "trace"
+ ? originalConsole.trace(message)
+ : level === "debug"
+ ? originalConsole.debug(message)
+ : level === "info"
+ ? originalConsole.info(message)
+ : level === "warn"
+ ? originalConsole.warn(message)
+ : originalConsole.error(message);
+}
+
+export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise {
+ writeOriginalConsole(level, message);
+ await sendFrontendLog(level, targetId, message);
+}
+
+export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
+ const message = formatMessage(items);
+ originalConsole.trace(message);
+ void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+}
+
+export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
+ const message = formatMessage(items);
+ originalConsole.debug(message);
+ void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+}
+
+export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
+ const message = formatMessage(items);
+ originalConsole.info(message);
+ void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+}
+
+export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
+ const message = formatMessage(items);
+ originalConsole.warn(message);
+ void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+}
+
+export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
+ const message = formatMessage(items);
+ originalConsole.error(message);
+ void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+}
+
+function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
+ return (...items: unknown[]) => {
+ original(...items);
+ void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
+ };
+}
+
+export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
+ console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
+ console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
+ console.log = buildConsoleBridge("info", targetId, originalConsole.log);
+ console.info = buildConsoleBridge("info", targetId, originalConsole.info);
+ console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
+ console.error = buildConsoleBridge("error", targetId, originalConsole.error);
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/ts/invoke.ts b/crates/ksp-app-wallet-desk/frontend/ts/invoke.ts
new file mode 100644
index 0000000..4589231
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/ts/invoke.ts
@@ -0,0 +1,17 @@
+// file: crates/ksp-app-wallet-desk/frontend/ts/invoke.ts
+// version: 1
+
+import { invoke } from "@tauri-apps/api/core";
+import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
+
+export async function invokeKsp(targetId: FrontendLogTargetId, command: string, args?: Record): Promise {
+ frontendDebug(targetId, "Frontend IPC command requested", { command });
+ try {
+ const result = await invoke(command, args);
+ frontendTrace(targetId, "Frontend IPC command completed", { command });
+ return result;
+ } catch (caughtError) {
+ frontendError(targetId, "Frontend IPC command failed", { command });
+ throw caughtError;
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/frontend/ts/main.ts b/crates/ksp-app-wallet-desk/frontend/ts/main.ts
new file mode 100644
index 0000000..5d72717
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/ts/main.ts
@@ -0,0 +1,152 @@
+// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
+// version: 1
+
+import "bootstrap";
+import DataTable from "datatables.net-bs5";
+import "datatables.net-select-bs5";
+import ResizeObserver from "resize-observer-polyfill";
+import "simplebar";
+import { getCurrentWindow } from "@tauri-apps/api/window";
+import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
+import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
+import { invokeKsp } from "./invoke";
+import "../sass/main.scss";
+
+(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
+installFrontendConsoleBridge("main");
+
+type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
+
+const viewTitles: Record = {
+ dashboard: "Dashboard",
+ wallets: "Wallets",
+ "create-import": "Create / Import",
+ details: "Details",
+ security: "Security",
+ diagnostics: "Diagnostics",
+};
+
+function isViewId(value: string): value is ViewId {
+ return value in viewTitles;
+}
+
+function activateView(viewId: ViewId, source: "startup" | "user"): void {
+ if (source === "user") {
+ frontendDebug("main", "Wallet Desk navigation activated", { viewId });
+ }
+ const title = viewTitles[viewId];
+ const headerTitle = document.querySelector("#headerViewTitle");
+ const viewTitle = document.querySelector("#viewTitle");
+ if (headerTitle) {
+ headerTitle.textContent = title;
+ }
+ if (viewTitle) {
+ viewTitle.textContent = title;
+ }
+ document.title = `Wallet Desk — ${title}`;
+ document.querySelectorAll("[data-view]").forEach(button => {
+ const active = button.dataset.view === viewId;
+ button.classList.toggle("active", active);
+ button.setAttribute("aria-current", active ? "page" : "false");
+ });
+ document.querySelectorAll("[data-view-panel]").forEach(panel => {
+ panel.hidden = panel.dataset.viewPanel !== viewId;
+ });
+ frontendTrace("main", "Wallet Desk view DOM updated", { viewId, source });
+}
+
+function bindNavigation(): void {
+ document.querySelectorAll("[data-view]").forEach(button => {
+ button.addEventListener("click", () => {
+ const requestedView = button.dataset.view;
+ frontendTrace("main", "Wallet Desk navigation clicked", { requestedView: requestedView ?? null });
+ if (requestedView && isViewId(requestedView)) {
+ activateView(requestedView, "user");
+ }
+ });
+ });
+ frontendTrace("main", "Wallet Desk navigation handlers installed");
+}
+
+function initializeWalletTable(): void {
+ new DataTable("#walletInventoryTable", {
+ order: [[1, "asc"]],
+ pageLength: 10,
+ select: {
+ style: "single",
+ },
+ language: {
+ emptyTable: "L'inventaire Wallet sera branché en pre.004.",
+ search: "Filtrer :",
+ zeroRecords: "Aucun wallet correspondant.",
+ },
+ });
+ frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.002-shell" });
+}
+
+function renderRuntimeStatus(status: RuntimeStatusDto): void {
+ const version = document.querySelector("#runtimeVersion");
+ const profile = document.querySelector("#runtimeLoggingProfile");
+ const fallback = document.querySelector("#runtimeLoggingFallback");
+ const documents = document.querySelector("#runtimeConfigDocuments");
+ const phase = document.querySelector("#runtimeShellPhase");
+ const shellStatus = document.querySelector("#shellStatus");
+ if (version) {
+ version.textContent = status.applicationVersion;
+ }
+ if (profile) {
+ profile.textContent = status.activeLoggingProfile ?? "fallback transitoire";
+ }
+ if (fallback) {
+ fallback.textContent = status.fallbackLoggingActive ? "oui" : "non";
+ }
+ if (documents) {
+ documents.textContent = status.configDocumentCount.toString();
+ }
+ if (phase) {
+ phase.textContent = status.shellPhase;
+ }
+ if (shellStatus) {
+ shellStatus.textContent = "Shell Wallet Desk prêt.";
+ }
+ frontendTrace("main", "Wallet Desk runtime status rendered", {
+ fallbackLoggingActive: status.fallbackLoggingActive,
+ shellPhase: status.shellPhase,
+ });
+}
+
+async function loadRuntimeStatus(): Promise {
+ const status = await invokeKsp("main", "get_runtime_status");
+ renderRuntimeStatus(status);
+}
+
+function bindShellActions(): void {
+ document.querySelectorAll("[data-shell-action]").forEach(button => {
+ button.addEventListener("click", () => {
+ frontendDebug("main", "Wallet Desk shell action clicked", { action: button.dataset.shellAction ?? "unknown", enabled: !button.disabled });
+ });
+ });
+ frontendTrace("main", "Wallet Desk shell action handlers installed");
+}
+
+async function initializeMain(): Promise {
+ const windowLabel = getCurrentWindow().label;
+ frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel });
+ bindNavigation();
+ bindShellActions();
+ initializeWalletTable();
+ activateView("dashboard", "startup");
+ try {
+ await loadRuntimeStatus();
+ } catch {
+ const shellStatus = document.querySelector("#shellStatus");
+ if (shellStatus) {
+ shellStatus.textContent = "Le statut runtime n'a pas pu être chargé.";
+ }
+ frontendTrace("main", "Wallet Desk shell status replaced", { status: "runtime_error" });
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ void initializeMain();
+});
diff --git a/crates/ksp-app-wallet-desk/frontend/ts/splash.ts b/crates/ksp-app-wallet-desk/frontend/ts/splash.ts
new file mode 100644
index 0000000..72800cf
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/frontend/ts/splash.ts
@@ -0,0 +1,107 @@
+// file: crates/ksp-app-wallet-desk/frontend/ts/splash.ts
+// version: 1
+
+import { listen } from "@tauri-apps/api/event";
+import { getCurrentWindow } from "@tauri-apps/api/window";
+import type { SplashOrderDto } from "./bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts";
+import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
+import { invokeKsp } from "./invoke";
+import "../sass/splash.scss";
+
+installFrontendConsoleBridge("splash");
+
+let activeOpacityFrame: number | null = null;
+
+function easeInOut(value: number): number {
+ return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
+}
+
+function normalizeDurationMs(value: number | null): number {
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
+}
+
+async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise {
+ frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
+ if (activeOpacityFrame !== null) {
+ cancelAnimationFrame(activeOpacityFrame);
+ activeOpacityFrame = null;
+ }
+ element.style.opacity = fromOpacity.toString();
+ element.style.willChange = "opacity";
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ await new Promise(resolve => {
+ const startedAt = performance.now();
+ const opacityDelta = toOpacity - fromOpacity;
+ const updateOpacity = (currentTime: number): void => {
+ const elapsedMs = currentTime - startedAt;
+ const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1);
+ element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
+ if (rawProgress >= 1) {
+ element.style.opacity = toOpacity.toString();
+ element.style.willChange = "auto";
+ activeOpacityFrame = null;
+ resolve();
+ return;
+ }
+ activeOpacityFrame = requestAnimationFrame(updateOpacity);
+ };
+ activeOpacityFrame = requestAnimationFrame(updateOpacity);
+ });
+ frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
+}
+
+function replaceStatus(message: string | null): void {
+ if (!message) {
+ return;
+ }
+ const status = document.querySelector("#splash-status");
+ if (status) {
+ status.textContent = message;
+ frontendTrace("splash", "Splash status replaced", { message });
+ }
+}
+
+async function handleSplashOrder(order: SplashOrderDto): Promise {
+ frontendTrace("splash", "Splash order received", { action: order.action });
+ const container = document.querySelector("#splash-container");
+ replaceStatus(order.message);
+ if (!container) {
+ return;
+ }
+ if (order.action === "fade_in") {
+ await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
+ return;
+ }
+ if (order.action === "fade_out") {
+ await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
+ }
+}
+
+async function initializeSplash(): Promise {
+ const windowLabel = getCurrentWindow().label;
+ frontendInfo("splash", "Wallet Desk splash frontend loaded", { windowLabel });
+ const container = document.querySelector("#splash-container");
+ if (container) {
+ container.style.opacity = "0";
+ container.style.willChange = "opacity";
+ frontendTrace("splash", "Splash container prepared for managed fade-in");
+ }
+ await listen("ksp-splash-order", event => {
+ void handleSplashOrder(event.payload);
+ });
+ frontendDebug("splash", "Splash lifecycle listener installed");
+ try {
+ await invokeKsp("splash", "splash_frontend_ready");
+ } catch {
+ replaceStatus("Le lifecycle du splash n'a pas pu démarrer.");
+ if (container) {
+ container.style.opacity = "1";
+ container.style.willChange = "auto";
+ }
+ frontendError("splash", "Splash readiness command failed");
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ void initializeSplash();
+});
diff --git a/crates/ksp-app-wallet-desk/icons/favicon.ico b/crates/ksp-app-wallet-desk/icons/favicon.ico
new file mode 100644
index 0000000..9dfa172
Binary files /dev/null and b/crates/ksp-app-wallet-desk/icons/favicon.ico differ
diff --git a/crates/ksp-app-wallet-desk/icons/favicon.png b/crates/ksp-app-wallet-desk/icons/favicon.png
new file mode 100644
index 0000000..94e89ca
Binary files /dev/null and b/crates/ksp-app-wallet-desk/icons/favicon.png differ
diff --git a/crates/ksp-app-wallet-desk/package.json b/crates/ksp-app-wallet-desk/package.json
new file mode 100644
index 0000000..0d15179
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "ksp-app-wallet-desk",
+ "private": true,
+ "version": "0.2.6-pre.2",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build"
+ },
+ "dependencies": {
+ "@fltsci/tauri-plugin-tracing": "^0.3",
+ "@fortawesome/fontawesome-free": "^7.3",
+ "@tauri-apps/api": "^2.11",
+ "bootstrap": "^5.3",
+ "datatables.net-bs5": "^3.0",
+ "datatables.net-select-bs5": "^4.0",
+ "resize-observer-polyfill": "^1.5",
+ "simplebar": "^6.3"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.11",
+ "@types/bootstrap": "^5.2",
+ "@types/node": "^26.1",
+ "sass-embedded": "^1.102",
+ "typescript": "^7.0",
+ "vite": "^8.2"
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/src/app_state.rs b/crates/ksp-app-wallet-desk/src/app_state.rs
new file mode 100644
index 0000000..c98b6e6
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/app_state.rs
@@ -0,0 +1,111 @@
+// file: crates/ksp-app-wallet-desk/src/app_state.rs
+// version: 2
+
+//! Shared backend state owned by the Wallet Desk Tauri application.
+
+/// Shared Wallet Desk application state managed by Tauri.
+pub(crate) struct AppState {
+ config_management: ksp_config_lib::ConfigManagement,
+ logging_runtime: std::sync::Mutex,
+ splash_settings: crate::SplashSettings,
+ splash_sequence_started: std::sync::atomic::AtomicBool,
+}
+
+impl AppState {
+ /// Initializes Config ownership, Logging and the common desktop splash state.
+ pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result {
+ let config_management = crate::config_management(arguments);
+ let config_management = match config_management {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let runtime_identity = crate::launch_identity();
+ let runtime_identity = match runtime_identity {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
+ let logging_startup = match logging_startup {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let splash_settings = crate::SplashSettings::load();
+ let splash_settings = match splash_settings {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => {
+ ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, error_domain = error.code().domain(), error_code = error.code().code(), "managed splash timings are invalid; using transient in-memory defaults");
+ crate::SplashSettings::fallback()
+ },
+ };
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, minimum_ms = splash_settings.minimum_ms(), minimum_source = splash_settings.minimum_source(), fade_ms = splash_settings.fade_ms(), fade_source = splash_settings.fade_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Wallet Desk splash timings");
+ return std::result::Result::Ok(Self {
+ config_management,
+ logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
+ guard: logging_startup.guard,
+ active_profile_id: logging_startup.active_profile_id,
+ fallback_active: logging_startup.fallback_active,
+ startup_diagnostic: logging_startup.startup_diagnostic,
+ }),
+ splash_settings,
+ splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
+ });
+ }
+
+ /// Builds the safe shell status DTO exposed during pre.002.
+ pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result {
+ let document_count = self.config_management.engine().registry().descriptors().count();
+ let document_count = u32::try_from(document_count);
+ let document_count = match document_count {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(
+ crate::ERROR_CODE_APP_STATE_INVALID,
+ "Config registry contains too many descriptors for the Wallet Desk shell DTO",
+ )
+ .with_source(error),
+ );
+ },
+ };
+ let runtime = self.logging_runtime.lock();
+ let runtime = match runtime {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => {
+ return std::result::Result::Err(ksp_core_lib::Error::new(
+ crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
+ "Wallet Desk Logging runtime state lock is poisoned",
+ ));
+ },
+ };
+ let _keep_guard_alive = &runtime.guard;
+ return std::result::Result::Ok(crate::RuntimeStatusDto {
+ application_version: env!("CARGO_PKG_VERSION").to_owned(),
+ config_document_count: document_count,
+ active_logging_profile: runtime.active_profile_id.clone(),
+ fallback_logging_active: runtime.fallback_active,
+ startup_diagnostic: runtime.startup_diagnostic.clone(),
+ shell_phase: "pre.002-shell".to_owned(),
+ });
+ }
+
+ /// Returns the resolved common splash timings captured during bootstrap.
+ #[must_use]
+ pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
+ return self.splash_settings;
+ }
+
+ /// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
+ pub(crate) fn begin_splash_sequence(&self) -> bool {
+ return self
+ .splash_sequence_started
+ .compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
+ .is_ok();
+ }
+}
+
+struct LoggingRuntimeState {
+ guard: ksp_logging_lib::LoggingGuard,
+ active_profile_id: std::option::Option,
+ fallback_active: bool,
+ startup_diagnostic: std::option::Option,
+}
diff --git a/crates/ksp-app-wallet-desk/src/bootstrap.rs b/crates/ksp-app-wallet-desk/src/bootstrap.rs
new file mode 100644
index 0000000..60d30d2
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/bootstrap.rs
@@ -0,0 +1,135 @@
+// file: crates/ksp-app-wallet-desk/src/bootstrap.rs
+// version: 1
+
+//! Config and Logging bootstrap for Wallet Desk.
+
+/// Crate-internal Logging startup state shared by the application state.
+pub(crate) struct LoggingStartup {
+ /// Guard that keeps the installed Logging runtime alive.
+ pub(crate) guard: ksp_logging_lib::LoggingGuard,
+ /// Active managed profile when Config resolved one successfully.
+ pub(crate) active_profile_id: std::option::Option,
+ /// Whether a transient in-memory fallback was installed.
+ pub(crate) fallback_active: bool,
+ /// Safe startup diagnostic retained for the shell.
+ pub(crate) startup_diagnostic: std::option::Option,
+}
+
+enum LoggingStartupPlan {
+ Managed {
+ active_profile_id: String,
+ settings: ksp_logging_lib::LoggingSettings,
+ },
+ Fallback {
+ initial_error: ksp_core_lib::Error,
+ diagnostic: crate::CommandErrorDto,
+ settings: ksp_logging_lib::LoggingSettings,
+ },
+}
+
+/// Builds the Config management facade from the common KSP CLI bootstrap contract.
+pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result {
+ let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
+ let bootstrap = match bootstrap {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
+ let registry = match registry {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
+ return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
+}
+
+/// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
+pub(crate) fn initialize_logging(
+ management: &ksp_config_lib::ConfigManagement,
+ runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
+) -> ksp_core_lib::Result {
+ let plan = resolve_logging_startup(management);
+ return match plan {
+ LoggingStartupPlan::Managed { active_profile_id, settings } => {
+ let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
+ match guard {
+ std::result::Result::Ok(guard) => {
+ ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, active_profile = active_profile_id.as_str(), "initialized Wallet Desk logging from managed configuration");
+ std::result::Result::Ok(LoggingStartup {
+ guard,
+ active_profile_id: std::option::Option::Some(active_profile_id),
+ fallback_active: false,
+ startup_diagnostic: std::option::Option::None,
+ })
+ },
+ std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
+ }
+ },
+ LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
+ initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
+ },
+ };
+}
+
+fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
+ return ksp_logging_lib::LoggingSettings::new(
+ ksp_logging_lib::LogFilterLevel::Info,
+ ksp_logging_lib::SpanEvents::Off,
+ std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
+ std::vec::Vec::new(),
+ );
+}
+
+fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
+ let environment = ksp_config_lib::ConfigEnvironment::load();
+ let environment = match environment {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return fallback_startup_plan(error),
+ };
+ let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
+ let resolved = match resolved {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return fallback_startup_plan(error),
+ };
+ return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
+}
+
+fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
+ let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
+ return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
+}
+
+fn initialize_fallback_logging(
+ initial_error: ksp_core_lib::Error,
+ runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
+) -> ksp_core_lib::Result {
+ let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
+ return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
+}
+
+fn initialize_planned_fallback_logging(
+ initial_error: ksp_core_lib::Error,
+ diagnostic: crate::CommandErrorDto,
+ settings: ksp_logging_lib::LoggingSettings,
+ runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
+) -> ksp_core_lib::Result {
+ let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
+ let guard = match guard {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(fallback_error) => {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Wallet Desk fallback Logging runtime")
+ .with_context("initial_error_domain", initial_error.code().domain())
+ .with_context("initial_error_code", initial_error.code().code())
+ .with_source(fallback_error),
+ );
+ },
+ };
+ ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, error_domain = diagnostic.domain.as_str(), error_code = diagnostic.code.as_str(), "managed Logging configuration is unavailable; using transient in-memory fallback");
+ return std::result::Result::Ok(LoggingStartup {
+ guard,
+ active_profile_id: std::option::Option::None,
+ fallback_active: true,
+ startup_diagnostic: std::option::Option::Some(diagnostic),
+ });
+}
diff --git a/crates/ksp-app-wallet-desk/src/constants.rs b/crates/ksp-app-wallet-desk/src/constants.rs
new file mode 100644
index 0000000..0f0ee89
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/constants.rs
@@ -0,0 +1,21 @@
+// file: crates/ksp-app-wallet-desk/src/constants.rs
+// version: 1
+
+//! Logging targets and domains owned by Wallet Desk.
+
+/// Structured domain used while bootstrapping Config and Logging.
+pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
+/// Structured domain used by technical frontend events.
+pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
+/// Structured domain used by the Wallet Desk shell.
+pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
+/// Structured domain used by Tauri window lifecycle operations.
+pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
+/// Owning target for backend events emitted by Wallet Desk.
+pub(crate) const TRACING_TARGET: &str = "ksp-app-wallet-desk";
+/// Owning target for generic frontend events emitted through the KSP bridge.
+pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
+/// Owning target for main-window frontend events.
+pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
+/// Owning target for splash-window frontend events.
+pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
diff --git a/crates/ksp-app-wallet-desk/src/dto_common.rs b/crates/ksp-app-wallet-desk/src/dto_common.rs
new file mode 100644
index 0000000..a2bb9cc
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/dto_common.rs
@@ -0,0 +1,54 @@
+// file: crates/ksp-app-wallet-desk/src/dto_common.rs
+// version: 1
+
+//! Common Tauri DTOs shared by Wallet Desk shell commands.
+
+use ts_rs::TS; // rust-rules: trait-import
+
+/// Safe command error projection that never serializes arbitrary KSP error context or source values.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts")]
+pub(crate) struct CommandErrorDto {
+ /// Stable KSP error domain.
+ pub(crate) domain: String,
+ /// Stable KSP error code within the domain.
+ pub(crate) code: String,
+ /// Human-readable error message without arbitrary context fields.
+ pub(crate) message: String,
+}
+
+impl CommandErrorDto {
+ /// Builds a bounded safe projection from a KSP error.
+ #[must_use]
+ pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
+ return Self {
+ domain: error.code().domain().to_owned(),
+ code: error.code().code().to_owned(),
+ message: error.message().to_owned(),
+ };
+ }
+}
+
+/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")]
+pub(crate) struct RuntimeStatusDto {
+ /// Cargo application version.
+ pub(crate) application_version: String,
+ /// Number of documents registered by Config before Wallet-specific documents are introduced.
+ pub(crate) config_document_count: u32,
+ /// Active configured Logging profile, or `None` while the transient fallback runtime is active.
+ pub(crate) active_logging_profile: std::option::Option,
+ /// Whether Wallet Desk had to install its transient in-memory Logging fallback.
+ pub(crate) fallback_logging_active: bool,
+ /// Safe startup diagnostic that caused fallback Logging, when applicable.
+ pub(crate) startup_diagnostic: std::option::Option,
+ /// Current implementation phase exposed for the pre.002 shell.
+ pub(crate) shell_phase: String,
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/dto_common.rs"]
+mod tests;
diff --git a/crates/ksp-app-wallet-desk/src/errors.rs b/crates/ksp-app-wallet-desk/src/errors.rs
new file mode 100644
index 0000000..9ae3d81
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/errors.rs
@@ -0,0 +1,26 @@
+// file: crates/ksp-app-wallet-desk/src/errors.rs
+// version: 1
+
+//! Application-local error codes for the Wallet Desk shell.
+
+/// Shared Wallet Desk application state is internally inconsistent.
+pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid");
+/// Shared Wallet Desk runtime state cannot be locked safely.
+pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_lock_failed");
+/// Frontend logging requested an unsupported level.
+pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_level_invalid");
+/// Frontend logging requested a target outside the application whitelist.
+pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_target_invalid");
+/// Wallet Desk could not install the managed Logging runtime or its safe fallback.
+pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "logging_bootstrap_failed");
+/// Splash readiness was invoked from a window other than the splash window.
+pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "splash_origin_invalid");
+/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
+pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "splash_setting_invalid");
+/// Tauri runtime assembly or execution failed.
+pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_runtime_failed");
+/// A required Tauri window is missing from the configured application runtime.
+pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_missing");
+/// A Tauri window show/focus/destroy/event operation failed.
+pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
+ ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_operation_failed");
diff --git a/crates/ksp-app-wallet-desk/src/frontend_logging.rs b/crates/ksp-app-wallet-desk/src/frontend_logging.rs
new file mode 100644
index 0000000..d290c3d
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/frontend_logging.rs
@@ -0,0 +1,145 @@
+// file: crates/ksp-app-wallet-desk/src/frontend_logging.rs
+// version: 1
+
+//! KSP-owned bridge for technical log events emitted by Wallet Desk frontend scripts.
+
+use ts_rs::TS; // rust-rules: trait-import
+
+/// Log payload sent by Wallet Desk frontend scripts.
+#[derive(Clone, Debug, serde::Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/frontend_logging/FrontendLogPayloadDto.ts")]
+pub(crate) struct FrontendLogPayloadDto {
+ /// Lowercase KSP log level.
+ pub(crate) level: String,
+ /// Whitelisted logical frontend target identifier.
+ pub(crate) target_id: String,
+ /// Rendered technical frontend message.
+ pub(crate) message: String,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum FrontendLogLevel {
+ Debug,
+ Error,
+ Info,
+ Trace,
+ Warn,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum FrontendLogTarget {
+ Frontend,
+ Main,
+ Splash,
+}
+
+/// Emits one validated frontend event through the KSP Logging facade.
+pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
+ let level = parse_level(payload.level.as_str());
+ let level = match level {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let target = parse_target(payload.target_id.as_str());
+ let target = match target {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ emit_validated_frontend_log(level, target, payload.message.as_str());
+ return std::result::Result::Ok(());
+}
+
+fn parse_level(level: &str) -> ksp_core_lib::Result {
+ return match level.trim().to_ascii_lowercase().as_str() {
+ "debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
+ "error" => std::result::Result::Ok(FrontendLogLevel::Error),
+ "info" => std::result::Result::Ok(FrontendLogLevel::Info),
+ "trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
+ "warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
+ _ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
+ };
+}
+
+fn parse_target(target_id: &str) -> ksp_core_lib::Result {
+ return match target_id.trim().to_ascii_lowercase().as_str() {
+ "frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
+ "main" => std::result::Result::Ok(FrontendLogTarget::Main),
+ "splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
+ _ => {
+ std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
+ },
+ };
+}
+
+fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
+ return match target {
+ FrontendLogTarget::Frontend => emit_frontend_target(level, message),
+ FrontendLogTarget::Main => emit_main_target(level, message),
+ FrontendLogTarget::Splash => emit_splash_target(level, message),
+ };
+}
+
+fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
+ return match level {
+ FrontendLogLevel::Debug => {
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
+ },
+ FrontendLogLevel::Error => {
+ ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
+ },
+ FrontendLogLevel::Info => {
+ ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
+ },
+ FrontendLogLevel::Trace => {
+ ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
+ },
+ FrontendLogLevel::Warn => {
+ ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
+ },
+ };
+}
+
+fn emit_main_target(level: FrontendLogLevel, message: &str) {
+ return match level {
+ FrontendLogLevel::Debug => {
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
+ },
+ FrontendLogLevel::Error => {
+ ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
+ },
+ FrontendLogLevel::Info => {
+ ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
+ },
+ FrontendLogLevel::Trace => {
+ ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
+ },
+ FrontendLogLevel::Warn => {
+ ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
+ },
+ };
+}
+
+fn emit_splash_target(level: FrontendLogLevel, message: &str) {
+ return match level {
+ FrontendLogLevel::Debug => {
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
+ },
+ FrontendLogLevel::Error => {
+ ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
+ },
+ FrontendLogLevel::Info => {
+ ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
+ },
+ FrontendLogLevel::Trace => {
+ ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
+ },
+ FrontendLogLevel::Warn => {
+ ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
+ },
+ };
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/frontend_logging.rs"]
+mod tests;
diff --git a/crates/ksp-app-wallet-desk/src/lib.rs b/crates/ksp-app-wallet-desk/src/lib.rs
new file mode 100644
index 0000000..f0219d4
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/lib.rs
@@ -0,0 +1,90 @@
+// file: crates/ksp-app-wallet-desk/src/lib.rs
+// version: 1
+
+//! Tauri desktop application shell for KSP Wallet management and inspection.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+
+mod app_state;
+mod bootstrap;
+mod constants;
+mod dto_common;
+mod errors;
+mod frontend_logging;
+mod logging_runtime;
+mod splash;
+mod tauri;
+mod tw_main;
+mod tw_splash;
+
+/// Runs the KSP wallet desktop application.
+pub use self::tauri::run;
+
+/// Shared Wallet Desk application state managed by Tauri.
+pub(crate) use self::app_state::AppState;
+/// Crate-internal Logging startup state shared by the application state.
+pub(crate) use self::bootstrap::LoggingStartup;
+/// Builds the Config management facade from the common KSP CLI bootstrap contract.
+pub(crate) use self::bootstrap::config_management;
+/// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
+pub(crate) use self::bootstrap::initialize_logging;
+/// Structured domain used while bootstrapping Config and Logging.
+pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
+/// Structured domain used by technical frontend events.
+pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
+/// Structured domain used by the Wallet Desk shell.
+pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
+/// Structured domain used by Tauri window lifecycle operations.
+pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
+/// Owning target for backend events emitted by Wallet Desk.
+pub(crate) use self::constants::TRACING_TARGET;
+/// Owning target for generic frontend events emitted through the KSP bridge.
+pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
+/// Owning target for main-window frontend events.
+pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
+/// Owning target for splash-window frontend events.
+pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
+/// Safe command error projection exposed to Tauri commands.
+pub(crate) use self::dto_common::CommandErrorDto;
+/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
+pub(crate) use self::dto_common::RuntimeStatusDto;
+/// Shared Wallet Desk application state is internally inconsistent.
+pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
+/// Shared Wallet Desk runtime state cannot be locked safely.
+pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
+/// Frontend logging requested an unsupported level.
+pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
+/// Frontend logging requested a target outside the application whitelist.
+pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
+/// Wallet Desk could not install the managed Logging runtime or its safe fallback.
+pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
+/// Splash readiness was invoked from a window other than the splash window.
+pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
+/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
+pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
+/// Tauri runtime assembly or execution failed.
+pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
+/// A required Tauri window is missing from the configured application runtime.
+pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
+/// A Tauri window show/focus/destroy/event operation failed.
+pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
+/// Log payload sent by Wallet Desk frontend scripts.
+pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
+/// Emits one validated frontend event through the KSP Logging facade.
+pub(crate) use self::frontend_logging::emit_frontend_log_event;
+/// Creates the stable runtime identity for this Wallet Desk process launch.
+pub(crate) use self::logging_runtime::launch_identity;
+/// Command emitted by Rust to the splash frontend.
+pub(crate) use self::splash::SplashOrderDto;
+/// Runtime timings used by the common desk splash lifecycle.
+pub(crate) use self::splash::SplashSettings;
+/// Resolves the required main window or returns a typed error.
+pub(crate) use self::tw_main::require_main_window;
+/// Shows and focuses the main Wallet Desk window.
+pub(crate) use self::tw_main::show_main_window;
+/// Resolves the required splash window or returns a typed error.
+pub(crate) use self::tw_splash::require_splash_window;
+/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
+pub(crate) use self::tw_splash::splash_frontend_ready_service;
diff --git a/crates/ksp-app-wallet-desk/src/logging_runtime.rs b/crates/ksp-app-wallet-desk/src/logging_runtime.rs
new file mode 100644
index 0000000..b9bc34e
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/logging_runtime.rs
@@ -0,0 +1,10 @@
+// file: crates/ksp-app-wallet-desk/src/logging_runtime.rs
+// version: 1
+
+//! Wallet Desk Logging runtime identity helpers.
+
+/// Creates the stable runtime identity for this Wallet Desk process launch.
+pub(crate) fn launch_identity() -> ksp_core_lib::Result {
+ let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
+ return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
+}
diff --git a/crates/ksp-app-wallet-desk/src/main.rs b/crates/ksp-app-wallet-desk/src/main.rs
new file mode 100644
index 0000000..4e1b723
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/main.rs
@@ -0,0 +1,58 @@
+// file: crates/ksp-app-wallet-desk/src/main.rs
+// version: 1
+
+//! Binary entry point for the KSP wallet desktop application.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use fs2::FileExt; // rust-rules: trait-import
+
+fn main() -> std::process::ExitCode {
+ let working_directory = configure_runtime_working_directory();
+ if let std::result::Result::Err(error) = working_directory {
+ eprintln!("cannot configure Wallet Desk runtime working directory: {error}");
+ return std::process::ExitCode::FAILURE;
+ }
+ let mut lock_path = std::env::temp_dir();
+ lock_path.push("com_sasedev_ksp_app_wallet_desk.lock");
+ let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
+ std::result::Result::Ok(file) => file,
+ std::result::Result::Err(error) => {
+ eprintln!("cannot create application lock '{}': {error}", lock_path.display());
+ return std::process::ExitCode::FAILURE;
+ },
+ };
+ if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
+ if error.kind() == std::io::ErrorKind::WouldBlock {
+ eprintln!("another ksp-app-wallet-desk instance is already running");
+ return std::process::ExitCode::FAILURE;
+ }
+ eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
+ return std::process::ExitCode::FAILURE;
+ }
+ let _lock_file = lock_file;
+ let arguments = std::env::args_os().collect::>();
+ let run_result = ksp_app_wallet_desk_lib::run(arguments.as_slice());
+ return match run_result {
+ std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
+ std::result::Result::Err(error) => {
+ eprintln!("application error: {error}");
+ std::process::ExitCode::FAILURE
+ },
+ };
+}
+
+fn configure_runtime_working_directory() -> std::io::Result<()> {
+ #[cfg(debug_assertions)]
+ {
+ let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
+ return std::env::set_current_dir(workspace_root);
+ }
+ #[cfg(not(debug_assertions))]
+ {
+ return std::result::Result::Ok(());
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/src/splash.rs b/crates/ksp-app-wallet-desk/src/splash.rs
new file mode 100644
index 0000000..3b9352a
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/splash.rs
@@ -0,0 +1,172 @@
+// file: crates/ksp-app-wallet-desk/src/splash.rs
+// version: 1
+
+//! Common splash settings and frontend event contracts for Wallet Desk.
+
+use ts_rs::TS; // rust-rules: trait-import
+
+const DEFAULT_SPLASH_FADE_MS: u32 = 300;
+const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
+const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
+const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
+const MAX_SPLASH_FADE_MS: u32 = 10_000;
+const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
+
+/// Runtime timings used by the common desk splash lifecycle.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) struct SplashSettings {
+ minimum_ms: u64,
+ fade_ms: u32,
+ minimum_source: ksp_config_lib::ConfigEnvironmentSource,
+ fade_source: ksp_config_lib::ConfigEnvironmentSource,
+}
+
+impl SplashSettings {
+ /// Resolves splash timings through the Config-owned environment snapshot.
+ pub(crate) fn load() -> ksp_core_lib::Result {
+ let environment = ksp_config_lib::ConfigEnvironment::load();
+ let environment = match environment {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
+ let minimum = match minimum {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let fade = environment.resolve_variable(ENV_SPLASH_FADE_MS, std::option::Option::Some("300"));
+ let fade = match fade {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
+ let minimum_ms = match minimum_ms {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let fade_ms = parse_u32_setting(ENV_SPLASH_FADE_MS, fade.value(), MAX_SPLASH_FADE_MS);
+ let fade_ms = match fade_ms {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ return std::result::Result::Ok(Self { minimum_ms, fade_ms, minimum_source: minimum.source(), fade_source: fade.source() });
+ }
+
+ /// Returns safe in-memory timings used when the managed environment cannot be resolved.
+ #[must_use]
+ pub(crate) const fn fallback() -> Self {
+ return Self {
+ minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
+ fade_ms: DEFAULT_SPLASH_FADE_MS,
+ minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
+ fade_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
+ };
+ }
+
+ /// Returns the minimum visible duration after splash frontend readiness.
+ #[must_use]
+ pub(crate) const fn minimum_ms(self) -> u64 {
+ return self.minimum_ms;
+ }
+
+ /// Returns the fade duration used for both fade-in and fade-out.
+ #[must_use]
+ pub(crate) const fn fade_ms(self) -> u32 {
+ return self.fade_ms;
+ }
+
+ /// Returns the safe provenance code for the minimum duration.
+ #[must_use]
+ pub(crate) const fn minimum_source(self) -> &'static str {
+ return environment_source_code(self.minimum_source);
+ }
+
+ /// Returns the safe provenance code for the fade duration.
+ #[must_use]
+ pub(crate) const fn fade_source(self) -> &'static str {
+ return environment_source_code(self.fade_source);
+ }
+
+ /// Returns the minimum backend lifecycle duration from readiness until main activation.
+ #[must_use]
+ pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
+ return self.minimum_ms + u64::from(self.fade_ms);
+ }
+}
+
+/// Command emitted by Rust to the splash frontend.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts")]
+pub(crate) struct SplashOrderDto {
+ /// Splash action name.
+ pub(crate) action: String,
+ /// Optional status text.
+ pub(crate) message: std::option::Option,
+ /// Optional animation duration in milliseconds.
+ pub(crate) duration_ms: std::option::Option,
+}
+
+impl SplashOrderDto {
+ /// Creates a new `SplashOrderDto` value.
+ #[must_use]
+ pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option) -> Self {
+ return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms };
+ }
+}
+
+const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
+ return match source {
+ ksp_config_lib::ConfigEnvironmentSource::Process => "process",
+ ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
+ ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
+ };
+}
+
+fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result {
+ let parsed = value.parse::();
+ let parsed = match parsed {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
+ .with_context("variable_name", variable_name)
+ .with_source(error),
+ );
+ },
+ };
+ if parsed > maximum {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
+ .with_context("variable_name", variable_name)
+ .with_context("maximum_ms", maximum.to_string()),
+ );
+ }
+ return std::result::Result::Ok(parsed);
+}
+
+fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result {
+ let parsed = value.parse::();
+ let parsed = match parsed {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
+ .with_context("variable_name", variable_name)
+ .with_source(error),
+ );
+ },
+ };
+ if parsed > maximum {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
+ .with_context("variable_name", variable_name)
+ .with_context("maximum_ms", maximum.to_string()),
+ );
+ }
+ return std::result::Result::Ok(parsed);
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/splash.rs"]
+mod tests;
diff --git a/crates/ksp-app-wallet-desk/src/tauri.rs b/crates/ksp-app-wallet-desk/src/tauri.rs
new file mode 100644
index 0000000..aeba8db
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/tauri.rs
@@ -0,0 +1,87 @@
+// file: crates/ksp-app-wallet-desk/src/tauri.rs
+// version: 1
+
+//! Tauri runtime assembly for the KSP wallet desktop application.
+
+/// Runs the Wallet Desk application.
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
+ let app_state = crate::AppState::initialize(arguments);
+ let app_state = match app_state {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let mut builder = tauri::Builder::default();
+ builder = configure_state(builder, app_state);
+ builder = configure_plugins(builder);
+ builder = configure_commands(builder);
+ builder = configure_setup(builder);
+ let run_result = builder.run(tauri::generate_context!());
+ return match run_result {
+ std::result::Result::Ok(()) => std::result::Result::Ok(()),
+ std::result::Result::Err(error) => std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Wallet Desk Tauri runtime")
+ .with_context("tauri_error", error.to_string()),
+ ),
+ };
+}
+
+fn configure_state(builder: tauri::Builder, app_state: crate::AppState) -> tauri::Builder {
+ return builder.manage(app_state);
+}
+
+fn configure_plugins(builder: tauri::Builder) -> tauri::Builder {
+ let tracing_plugin = tauri_plugin_tracing::Builder::new().build::();
+ return builder.plugin(tracing_plugin);
+}
+
+#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
+fn configure_commands(builder: tauri::Builder) -> tauri::Builder {
+ return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready]);
+}
+
+fn configure_setup(builder: tauri::Builder) -> tauri::Builder {
+ return builder.setup(|app| {
+ let splash = crate::require_splash_window(app);
+ if let std::result::Result::Err(error) = splash {
+ return std::result::Result::Err(std::boxed::Box::new(error));
+ }
+ let main = crate::require_main_window(app);
+ if let std::result::Result::Err(error) = main {
+ return std::result::Result::Err(std::boxed::Box::new(error));
+ }
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Wallet Desk Tauri shell setup completed");
+ return std::result::Result::Ok(());
+ });
+}
+
+#[tauri::command]
+fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
+ let result = crate::emit_frontend_log_event(payload);
+ return match result {
+ std::result::Result::Ok(()) => std::result::Result::Ok(()),
+ std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
+ };
+}
+
+#[tauri::command]
+fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result {
+ let result = state.runtime_status();
+ 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 splash_frontend_ready(
+ app: tauri::AppHandle,
+ window: tauri::WebviewWindow,
+ state: tauri::State<'_, crate::AppState>,
+) -> std::result::Result<(), crate::CommandErrorDto> {
+ let result = crate::splash_frontend_ready_service(app, window, &state).await;
+ return match result {
+ std::result::Result::Ok(()) => std::result::Result::Ok(()),
+ std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
+ };
+}
diff --git a/crates/ksp-app-wallet-desk/src/tw_main.rs b/crates/ksp-app-wallet-desk/src/tw_main.rs
new file mode 100644
index 0000000..5faae65
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/tw_main.rs
@@ -0,0 +1,48 @@
+// file: crates/ksp-app-wallet-desk/src/tw_main.rs
+// version: 1
+
+//! Tauri-window helpers for the Wallet Desk main window.
+
+use tauri::Manager; // rust-rules: trait-import
+
+/// Crate-internal main window label.
+pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
+
+/// Resolves the required main window or returns a typed error.
+pub(crate) fn require_main_window(manager: &impl Manager) -> ksp_core_lib::Result {
+ let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
+ return match window {
+ std::option::Option::Some(value) => std::result::Result::Ok(value),
+ std::option::Option::None => std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Wallet Desk main window is missing")
+ .with_context("window_label", WINDOW_LABEL_MAIN),
+ ),
+ };
+}
+
+/// Shows and focuses the main Wallet Desk window.
+pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
+ let window = crate::require_main_window(app);
+ let window = match window {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let shown = window.show();
+ if let std::result::Result::Err(error) = shown {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Wallet Desk main window")
+ .with_context("window_label", WINDOW_LABEL_MAIN)
+ .with_source(error),
+ );
+ }
+ let focused = window.set_focus();
+ if let std::result::Result::Err(error) = focused {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Wallet Desk main window")
+ .with_context("window_label", WINDOW_LABEL_MAIN)
+ .with_source(error),
+ );
+ }
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_MAIN, "main window shown and focused");
+ return std::result::Result::Ok(());
+}
diff --git a/crates/ksp-app-wallet-desk/src/tw_splash.rs b/crates/ksp-app-wallet-desk/src/tw_splash.rs
new file mode 100644
index 0000000..8f59866
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/src/tw_splash.rs
@@ -0,0 +1,96 @@
+// file: crates/ksp-app-wallet-desk/src/tw_splash.rs
+// version: 1
+
+//! Tauri-window lifecycle for the Wallet Desk splash window.
+
+use tauri::Emitter; // rust-rules: trait-import
+use tauri::Manager; // rust-rules: trait-import
+
+/// Crate-internal splash window label.
+pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
+
+const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
+
+/// Resolves the required splash window or returns a typed error.
+pub(crate) fn require_splash_window(manager: &impl Manager) -> ksp_core_lib::Result {
+ let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
+ return match window {
+ std::option::Option::Some(value) => std::result::Result::Ok(value),
+ std::option::Option::None => std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Wallet Desk splash window is missing")
+ .with_context("window_label", WINDOW_LABEL_SPLASH),
+ ),
+ };
+}
+
+/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
+pub(crate) async fn splash_frontend_ready_service(
+ app: tauri::AppHandle,
+ invoking_window: tauri::WebviewWindow,
+ state: &crate::AppState,
+) -> ksp_core_lib::Result<()> {
+ if invoking_window.label() != WINDOW_LABEL_SPLASH {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
+ .with_context("window_label", invoking_window.label()),
+ );
+ }
+ if !state.begin_splash_sequence() {
+ ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
+ return std::result::Result::Ok(());
+ }
+ let settings = state.splash_settings();
+ let lifecycle_started = std::time::Instant::now();
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, minimum_ms = settings.minimum_ms(), minimum_source = settings.minimum_source(), fade_ms = settings.fade_ms(), fade_source = settings.fade_source(), expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), "splash frontend readiness accepted; starting splash to main lifecycle");
+ let fade_in = emit_order(
+ &invoking_window,
+ crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Wallet Desk..."), std::option::Option::Some(settings.fade_ms())),
+ );
+ if let std::result::Result::Err(error) = fade_in {
+ return std::result::Result::Err(error);
+ }
+ let minimum_wait_started = std::time::Instant::now();
+ tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await;
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.minimum_ms(), actual_wait_ms = minimum_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash minimum wait completed");
+ let fade_out = emit_order(
+ &invoking_window,
+ crate::SplashOrderDto::new("fade_out", std::option::Option::Some("Initialisation terminée."), std::option::Option::Some(settings.fade_ms())),
+ );
+ if let std::result::Result::Err(error) = fade_out {
+ return std::result::Result::Err(error);
+ }
+ let fade_wait_started = std::time::Instant::now();
+ tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await;
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.fade_ms(), actual_wait_ms = fade_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash fade-out wait completed");
+ let show_main = crate::show_main_window(&app);
+ if let std::result::Result::Err(error) = show_main {
+ return std::result::Result::Err(error);
+ }
+ let destroyed = invoking_window.destroy();
+ if let std::result::Result::Err(error) = destroyed {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Wallet Desk splash window")
+ .with_context("window_label", WINDOW_LABEL_SPLASH)
+ .with_source(error),
+ );
+ }
+ ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash window destroyed after main activation");
+ return std::result::Result::Ok(());
+}
+
+fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
+ let action = order.action.clone();
+ let emitted = window.emit(SPLASH_EVENT_NAME, order);
+ return match emitted {
+ std::result::Result::Ok(()) => {
+ ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
+ std::result::Result::Ok(())
+ },
+ std::result::Result::Err(error) => std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Wallet Desk splash order")
+ .with_context("window_label", WINDOW_LABEL_SPLASH)
+ .with_context("action", action)
+ .with_source(error),
+ ),
+ };
+}
diff --git a/crates/ksp-app-wallet-desk/tauri.conf.json b/crates/ksp-app-wallet-desk/tauri.conf.json
new file mode 100644
index 0000000..297e832
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/tauri.conf.json
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "KSP Wallet Desk",
+ "version": "0.2.6-pre.2",
+ "identifier": "com.sasedev.ksp-app-wallet-desk",
+ "build": {
+ "beforeDevCommand": "npm run dev",
+ "devUrl": "http://localhost:1432",
+ "beforeBuildCommand": "npm run build",
+ "frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "label": "splash",
+ "url": "splash.html",
+ "title": "Chargement — KSP Wallet Desk",
+ "width": 960,
+ "height": 637,
+ "resizable": false,
+ "decorations": false,
+ "transparent": true,
+ "center": true,
+ "alwaysOnTop": true
+ },
+ {
+ "label": "main",
+ "url": "main.html",
+ "title": "KSP Wallet Desk",
+ "width": 1500,
+ "height": 960,
+ "minWidth": 1024,
+ "minHeight": 768,
+ "center": true,
+ "visible": false
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": [
+ "icons/favicon.png",
+ "icons/favicon.ico"
+ ]
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs
new file mode 100644
index 0000000..c8342c4
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs
@@ -0,0 +1,85 @@
+// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
+// version: 1
+
+//! Desktop build and shell contract audits for Wallet Desk pre.002.
+
+fn app_root() -> std::path::PathBuf {
+ return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+}
+
+fn read_text(path: &std::path::Path) -> String {
+ let source = std::fs::read_to_string(path);
+ assert!(source.is_ok(), "unable to read {}", path.display());
+ return match source {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => String::new(),
+ };
+}
+
+fn read_json(path: &std::path::Path) -> serde_json::Value {
+ let source = read_text(path);
+ let parsed = serde_json::from_str::(source.as_str());
+ assert!(parsed.is_ok(), "unable to parse {}", path.display());
+ return match parsed {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => serde_json::Value::Null,
+ };
+}
+
+#[test]
+fn tauri_shell_uses_reserved_wallet_desk_ports_and_template_windows() {
+ let root = app_root();
+ let tauri = read_json(root.join("tauri.conf.json").as_path());
+ assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1432"));
+ assert_eq!(tauri.pointer("/build/beforeDevCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
+ assert_eq!(tauri.pointer("/build/beforeBuildCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
+ assert_eq!(tauri.pointer("/app/security/csp"), std::option::Option::Some(&serde_json::Value::Null));
+ let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
+ assert!(windows.is_some());
+ let windows = match windows {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ let labels = windows.iter().filter_map(|window| return window.get("label").and_then(serde_json::Value::as_str)).collect::>();
+ assert_eq!(labels, ["splash", "main"]);
+}
+
+#[test]
+fn package_reuses_config_desk_frontend_baseline_without_standalone_check_script() {
+ let root = app_root();
+ let package = read_json(root.join("package.json").as_path());
+ assert!(package.pointer("/scripts/check").is_none());
+ let dependencies = package.get("dependencies").and_then(serde_json::Value::as_object);
+ assert!(dependencies.is_some());
+ let dependencies = match dependencies {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ for dependency in [
+ "@fltsci/tauri-plugin-tracing",
+ "@fortawesome/fontawesome-free",
+ "@tauri-apps/api",
+ "bootstrap",
+ "datatables.net-bs5",
+ "datatables.net-select-bs5",
+ "resize-observer-polyfill",
+ "simplebar",
+ ] {
+ assert!(dependencies.contains_key(dependency), "missing dependency {dependency}");
+ }
+}
+
+#[test]
+fn shell_contains_wallet_navigation_datatable_and_lock_state_icons() {
+ let root = app_root();
+ let html = read_text(root.join("frontend/main.html").as_path());
+ let main = read_text(root.join("frontend/ts/main.ts").as_path());
+ assert!(html.contains("id=\"walletInventoryTable\""));
+ assert!(html.contains("fa-lock"));
+ assert!(html.contains("fa-lock-open"));
+ assert!(html.contains("data-view=\"wallets\""));
+ assert!(main.contains("new DataTable"));
+ assert!(main.contains("datatables.net-select-bs5"));
+ assert!(main.contains("ResizeObserver"));
+ assert!(main.contains("simplebar"));
+}
diff --git a/crates/ksp-app-wallet-desk/tests/desktop_security.rs b/crates/ksp-app-wallet-desk/tests/desktop_security.rs
new file mode 100644
index 0000000..bece6df
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/tests/desktop_security.rs
@@ -0,0 +1,38 @@
+// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
+// version: 1
+
+//! Static desktop security contracts for the Wallet Desk pre.002 shell.
+
+fn app_root() -> std::path::PathBuf {
+ return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+}
+
+fn read_text(path: &std::path::Path) -> String {
+ let source = std::fs::read_to_string(path);
+ assert!(source.is_ok(), "unable to read {}", path.display());
+ return match source {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => String::new(),
+ };
+}
+
+#[test]
+fn initial_capability_surface_is_core_plus_tracing_only() {
+ let root = app_root();
+ let capability = read_text(root.join("capabilities/default.json").as_path());
+ assert!(capability.contains("\"core:default\""));
+ assert!(capability.contains("\"tracing:default\""));
+ assert!(!capability.contains("dialog:"));
+ assert!(!capability.contains("fs:"));
+}
+
+#[test]
+fn frontend_shell_uses_no_browser_native_dialog_or_secret_storage() {
+ let root = app_root();
+ let main = read_text(root.join("frontend/ts/main.ts").as_path());
+ let html = read_text(root.join("frontend/main.html").as_path());
+ for forbidden in ["window.alert", "window.confirm", "window.prompt", "localStorage", "sessionStorage"] {
+ assert!(!main.contains(forbidden), "forbidden frontend primitive {forbidden}");
+ assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}");
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/tests/public_api.rs b/crates/ksp-app-wallet-desk/tests/public_api.rs
new file mode 100644
index 0000000..0e1647a
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/tests/public_api.rs
@@ -0,0 +1,14 @@
+// file: crates/ksp-app-wallet-desk/tests/public_api.rs
+// version: 1
+
+//! Public API checks for the KSP wallet desktop application library.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+
+#[test]
+fn run_entry_point_is_available_from_crate_root() {
+ let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_wallet_desk_lib::run;
+ assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of:: ksp_core_lib::Result<()>>());
+}
diff --git a/crates/ksp-app-wallet-desk/tsconfig.json b/crates/ksp-app-wallet-desk/tsconfig.json
new file mode 100644
index 0000000..b5e3e21
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "allowSyntheticDefaultImports": true,
+ "types": [
+ "vite/client",
+ "node"
+ ]
+ },
+ "include": [
+ "frontend",
+ "vite.config.ts"
+ ]
+}
diff --git a/crates/ksp-app-wallet-desk/unit_tests/dto_common.rs b/crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
new file mode 100644
index 0000000..b1886f3
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
@@ -0,0 +1,11 @@
+// file: crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
+// version: 1
+
+#[test]
+fn command_error_projection_excludes_context_values() {
+ let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
+ let dto = crate::CommandErrorDto::from_error(&error);
+ assert_eq!(dto.domain, "test");
+ assert_eq!(dto.code, "failure");
+ assert_eq!(dto.message, "safe message");
+}
diff --git a/crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs b/crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs
new file mode 100644
index 0000000..6091632
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs
@@ -0,0 +1,40 @@
+// file: crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs
+// version: 1
+
+#[test]
+fn frontend_log_levels_are_explicit_and_case_insensitive() {
+ assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
+ assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
+ assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
+ assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
+ assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
+}
+
+#[test]
+fn frontend_log_targets_are_whitelisted() {
+ assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
+ assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
+ assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
+}
+
+#[test]
+fn unsupported_frontend_log_level_is_rejected() {
+ let result = super::parse_level("notice");
+ assert!(result.is_err());
+ let error = match result {
+ std::result::Result::Ok(_) => return,
+ std::result::Result::Err(error) => error,
+ };
+ assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
+}
+
+#[test]
+fn arbitrary_frontend_log_target_is_rejected() {
+ let result = super::parse_target("ksp-app-wallet-desk.test.logging");
+ assert!(result.is_err());
+ let error = match result {
+ std::result::Result::Ok(_) => return,
+ std::result::Result::Err(error) => error,
+ };
+ assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
+}
diff --git a/crates/ksp-app-wallet-desk/unit_tests/splash.rs b/crates/ksp-app-wallet-desk/unit_tests/splash.rs
new file mode 100644
index 0000000..a92764a
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/unit_tests/splash.rs
@@ -0,0 +1,37 @@
+// file: crates/ksp-app-wallet-desk/unit_tests/splash.rs
+// version: 1
+
+#[test]
+fn fallback_splash_settings_are_short_and_ordered() {
+ let settings = crate::SplashSettings::fallback();
+ assert_eq!(settings.minimum_ms(), 1200);
+ assert_eq!(settings.fade_ms(), 300);
+ assert_eq!(settings.minimum_source(), "fallback");
+ assert_eq!(settings.fade_source(), "fallback");
+ assert_eq!(settings.expected_backend_lifecycle_ms(), 1500);
+ assert!(settings.minimum_ms() >= u64::from(settings.fade_ms()));
+}
+
+#[test]
+fn splash_order_contract_keeps_action_message_and_duration() {
+ let order = crate::SplashOrderDto::new("fade_out", std::option::Option::Some("done"), std::option::Option::Some(300));
+ assert_eq!(order.action, "fade_out");
+ assert_eq!(order.message.as_deref(), std::option::Option::Some("done"));
+ assert_eq!(order.duration_ms, std::option::Option::Some(300));
+}
+
+#[test]
+fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
+ let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
+ let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
+ let oversized_fade = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_MS", "10001", 10_000);
+ assert!(malformed.is_err());
+ assert!(oversized_minimum.is_err());
+ assert!(oversized_fade.is_err());
+ if let std::result::Result::Err(error) = oversized_minimum {
+ assert!(error.context().iter().any(|field| return field.key() == "maximum_ms" && field.value() == "60000"));
+ }
+ if let std::result::Result::Err(error) = oversized_fade {
+ assert!(error.context().iter().any(|field| return field.key() == "maximum_ms" && field.value() == "10000"));
+ }
+}
diff --git a/crates/ksp-app-wallet-desk/vite.config.ts b/crates/ksp-app-wallet-desk/vite.config.ts
new file mode 100644
index 0000000..36a5d00
--- /dev/null
+++ b/crates/ksp-app-wallet-desk/vite.config.ts
@@ -0,0 +1,72 @@
+// file: crates/ksp-app-wallet-desk/vite.config.ts
+// version: 1
+
+import { NodePackageImporter } from "sass-embedded";
+import { fileURLToPath } from "node:url";
+import { resolve } from "node:path";
+import { defineConfig, normalizePath } from "vite";
+
+const appRoot = fileURLToPath(new URL(".", import.meta.url));
+const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
+const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist"));
+const devHost = process.env.TAURI_DEV_HOST;
+
+export default defineConfig({
+ clearScreen: false,
+ root: frontendRoot,
+ publicDir: false,
+ input: {
+ main: normalizePath(resolve(frontendRoot, "main.html")),
+ splash: normalizePath(resolve(frontendRoot, "splash.html")),
+ },
+ build: {
+ outDir: frontendDist,
+ emptyOutDir: true,
+ minify: true,
+ sourcemap: false,
+ cssCodeSplit: true,
+ rolldownOptions: {
+ output: {
+ entryFileNames: "js/[name]-[hash].js",
+ chunkFileNames: "js/chunks/[name]-[hash].js",
+ assetFileNames: assetInfo => {
+ const originalName = assetInfo.names[0] ?? "";
+ const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
+ if (extension === "css") {
+ return "css/[name]-[hash][extname]";
+ }
+ if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
+ return "fonts/[name]-[hash][extname]";
+ }
+ if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
+ return "imgs/[name][extname]";
+ }
+ return "otherassets/[name][extname]";
+ },
+ },
+ },
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ quietDeps: true,
+ silenceDeprecations: ["import", "color-functions", "global-builtin"],
+ verbose: false,
+ importers: [new NodePackageImporter()],
+ },
+ },
+ },
+ server: {
+ port: 1432,
+ strictPort: true,
+ host: devHost || false,
+ ws: {
+ protocol: "ws",
+ host: devHost || "localhost",
+ port: 1433,
+ },
+ watch: {
+ ignored: ["**/src/**"],
+ },
+ },
+});
diff --git a/deltas/0.2.6/pre.002.md b/deltas/0.2.6/pre.002.md
new file mode 100644
index 0000000..116f087
--- /dev/null
+++ b/deltas/0.2.6/pre.002.md
@@ -0,0 +1,202 @@
+
+
+
+# Delta `0.2.6-pre.002` — crate Tauri et shell Wallet Desk
+
+## Base requise
+
+```text
+0.2.6-pre.001-fix.002 appliquée
+workspace.package.version = 0.2.6-pre.1
+```
+
+Les validations opérateur de `pre.001` sont vertes : `cargo fmt --all`, audit structurel Python, `cargo check --workspace` et `cargo clippy --workspace --all-targets`.
+
+## Type de livraison
+
+```text
+ksp-general-0.2.6-pre.002.zip
+```
+
+Cette tranche est technique : création de crate, Rust/Tauri, frontend et configuration de build. Elle incrémente donc `workspace.package.version` vers `0.2.6-pre.2`.
+
+## Objet
+
+Matérialiser la première application `ksp-app-wallet-desk` sans commencer les fonctionnalités Wallet métier prévues à partir de `pre.003`/`pre.004`.
+
+Le delta fournit :
+
+```text
+crate Rust mixte lib + bin
+single-instance launcher
+normalisation CWD debug workspace
+bootstrap Config + Logging commun
+fallback Logging borné
+splash + main window
+bridge frontend -> Rust -> ksp-logging-lib
+DTO TS-RS foundation
+ports Vite/HMR 1432/1433
+Bootstrap
+Font Awesome
+DataTables Bootstrap 5 + Select
+SimpleBar
+resize-observer-polyfill
+shell Dashboard/Wallets/Create-Import/Details/Security/Diagnostics
+DataTable Wallet réelle mais volontairement vide en pre.002
+capabilities core:default + tracing:default
+CSP null comme Config Desk
+```
+
+## Frontières conservées
+
+`pre.002` ne dépend pas encore de `ksp-wallet-lib` ni de `ksp-onchain-transport-lib`. La seule composition KSP active dans la nouvelle app est celle nécessaire au gabarit desktop :
+
+```text
+ksp-app-wallet-desk
+ -> ksp-config-lib
+ -> ksp-core-lib
+ -> ksp-logging-lib
+ -> Tauri
+```
+
+Aucune logique `.kspwallet`, aucun inventory filesystem, aucun secret Wallet et aucun RPC ne sont simulés dans le frontend.
+
+## Frontend et npm
+
+Le `package.json` reprend la baseline Config Desk :
+
+```text
+@fltsci/tauri-plugin-tracing ^0.3
+@fortawesome/fontawesome-free ^7.3
+@tauri-apps/api ^2.11
+bootstrap ^5.3
+datatables.net-bs5 ^3.0
+datatables.net-select-bs5 ^4.0
+resize-observer-polyfill ^1.5
+simplebar ^6.3
+```
+
+Les outils restent dans `devDependencies`. Aucun script `check` standalone n'est créé. Les scripts `dev` et `build` existent uniquement parce que Tauri les appelle via `beforeDevCommand` et `beforeBuildCommand`.
+
+Lors de l'application de ce delta, l'installation initiale des packages se fait depuis la crate avec :
+
+```bash
+cd crates/ksp-app-wallet-desk
+npm i -D
+cd ../..
+```
+
+Le lockfile npm et `node_modules` restent non versionnés.
+
+## Dépendances auditées
+
+Au `2026-08-20`, les versions actuelles vérifiées incluent :
+
+```text
+@tauri-apps/api 2.11.1
+@tauri-apps/cli 2.11.4
+@fortawesome/fontawesome-free 7.3.1
+datatables.net-bs5 3.0.1
+datatables.net-select-bs5 4.0.0
+resize-observer-polyfill 1.5.1
+simplebar 6.3.3
+```
+
+Les ranges du gabarit couvrent ces versions sans changement de génération.
+
+## Shell et logging
+
+Le splash reprend les paramètres Config-owned existants :
+
+```text
+KSP_DESK_SPLASH_MINIMUM_MS
+KSP_DESK_SPLASH_FADE_MS
+```
+
+Le frontend instrumente :
+
+```text
+chargement main/splash
+navigation
+mise à jour DOM des vues
+initialisation DataTables
+clics d'actions shell
+IPC runtime status
+ordres/animations splash
+```
+
+Les targets frontend sont whitelisted (`frontend`, `main`, `splash`) avant réémission via `ksp-logging-lib`.
+
+## Tests ajoutés
+
+```text
+public API run()
+contrat Tauri ports/windows/beforeDev/beforeBuild/CSP
+baseline dependencies et absence de script npm check
+présence DataTables/Select/SimpleBar/ResizeObserver/Font Awesome
+capabilities initiales core + tracing seulement
+absence window.alert/confirm/prompt/localStorage/sessionStorage
+DTO CommandError sans context arbitraire
+whitelist frontend logging
+contrats splash
+```
+
+## Fichiers principaux ajoutés
+
+```text
+crates/ksp-app-wallet-desk/Cargo.toml
+crates/ksp-app-wallet-desk/build.rs
+crates/ksp-app-wallet-desk/capabilities/default.json
+crates/ksp-app-wallet-desk/package.json
+crates/ksp-app-wallet-desk/tauri.conf.json
+crates/ksp-app-wallet-desk/tsconfig.json
+crates/ksp-app-wallet-desk/vite.config.ts
+crates/ksp-app-wallet-desk/src/**
+crates/ksp-app-wallet-desk/frontend/**
+crates/ksp-app-wallet-desk/tests/**
+crates/ksp-app-wallet-desk/unit_tests/**
+```
+
+Les assets fonts/logo/splash/favicon sont repris du gabarit Config Desk.
+
+## Fichiers modifiés hors crate
+
+```text
+Cargo.toml
+ROADMAP.md
+docs/000-README.md
+docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
+```
+
+## Version technique
+
+```text
+workspace.package.version = 0.2.6-pre.2
+commit = v0.2.6-pre.002
+```
+
+Aucun tag stable.
+
+## Validation opérateur requise
+
+Après application :
+
+```bash
+cd crates/ksp-app-wallet-desk
+npm i -D
+cd ../..
+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 tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
+```
+
+Le parcours `cargo tauri dev` doit vérifier visuellement et dans les logs : splash, ouverture de la fenêtre principale, navigation des six vues, DataTable vide fonctionnelle, icônes Font Awesome, scrolling SimpleBar et bridge frontend Logging.
+
+`cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json` **n'est pas exécuté en `pre.002`** : il reste réservé à la toute dernière validation de la release conformément à `KSP-APP-034`.
+
+## Suite
+
+`pre.003` ajoute `cfg.std.wallet`, le composite Wallet Desk, `wallets_directory` global, `wallets_subdirectory` par profil et la préparation/création du répertoire effectif.
diff --git a/docs/000-README.md b/docs/000-README.md
index 3c8c213..49cf2d0 100644
--- a/docs/000-README.md
+++ b/docs/000-README.md
@@ -1,5 +1,5 @@
-
+
# Documentation KSP
@@ -74,7 +74,7 @@ D'autres sous-répertoires seront ajoutés uniquement lorsque leur rôle aura é
## Documents de planification
-Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) a ouvert la release stable `0.2.2 — HTTP Accounts + Tokens + Cluster`. Son plan clôturé [`plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) conserve l'audit et l'implémentation des 22 wrappers typés, tandis que [`validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) enregistre les validations déterministes, les graphes Cargo et les deux smokes Devnet passés avant publication. Le prompt [`../prompts/008-V0_2_3_START_PROMPT.md`](../prompts/008-V0_2_3_START_PROMPT.md) a ouvert la release stable `0.2.3 — HTTP Transactions`. Son plan clôturé [`plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) conserve l'audit et l'implémentation des 11 wrappers ; le réaudit [`validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`](validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md) confirme la complétude des 37 wrappers HTTP typés et [`validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](validation/006-V0_2_3_HTTP_TRANSACTIONS.md) enregistre les validations finales, graphes Cargo et deux smokes Devnet passés avant publication. Le prompt [`../prompts/009-V0_2_4_START_PROMPT.md`](../prompts/009-V0_2_4_START_PROMPT.md) a ouvert la release stable `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`. Son plan clôturé [`plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) conserve l’implémentation des 15 wrappers et la compliance `52/52 + 14/14`; la matrice finale [`validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) enregistre le réaudit SIMD/inventaire, les canaries globales et les preuves opérateur avant publication. Le prompt [`../prompts/010-V0_2_5_START_PROMPT.md`](../prompts/010-V0_2_5_START_PROMPT.md), finalisé par `0.2.4-pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`. Son plan historique clôturé [`plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) part du gate `pre.001` (héritage, threat model offline, VIEW/OWNER indépendants et niveau B read-only), puis matérialise la crate en `pre.002`, le wire/transcript en `pre.003`, les primitives Argon2id/XChaCha20-Poly1305 en `pre.004`, les payloads/create/open en `pre.005`, la persistence en `pre.006`, l'administration/signature en `pre.007` et les adapters transfer en `pre.008`. `pre.009` ferme l'audit adversarial/interoperability/compliance dans [`validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) avant la documentation finale `pre.010` ; `pre.010` finalise [`../crates/ksp-wallet-lib/README.md`](../crates/ksp-wallet-lib/README.md), [`../crates/ksp-wallet-lib/USAGE.md`](../crates/ksp-wallet-lib/USAGE.md), la spec, les graphes et la matrice ; `pre.010-fix.001`–`fix.003` ferment ensuite la mise à niveau Dalek et la normalisation Rust/audit structurel. `0.2.5-rel.001` publie la release stable et [`../prompts/011-V0_2_6_START_PROMPT.md`](../prompts/011-V0_2_6_START_PROMPT.md) ouvre `0.2.6 — Wallet Desk`. Le gate `0.2.6-pre.001` est matérialisé dans [`plans/013-V0_2_6_WALLET_DESK_PLAN.md`](plans/013-V0_2_6_WALLET_DESK_PLAN.md) : il réaudite Config Desk et les APIs finales, fixe `std.wallet`/composite, inventory/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, threat model desktop, balance HTTP et regranularise la release jusqu’à `pre.013` avant implémentation Tauri lourde.
+Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) a ouvert la release stable `0.2.2 — HTTP Accounts + Tokens + Cluster`. Son plan clôturé [`plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) conserve l'audit et l'implémentation des 22 wrappers typés, tandis que [`validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) enregistre les validations déterministes, les graphes Cargo et les deux smokes Devnet passés avant publication. Le prompt [`../prompts/008-V0_2_3_START_PROMPT.md`](../prompts/008-V0_2_3_START_PROMPT.md) a ouvert la release stable `0.2.3 — HTTP Transactions`. Son plan clôturé [`plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) conserve l'audit et l'implémentation des 11 wrappers ; le réaudit [`validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md`](validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md) confirme la complétude des 37 wrappers HTTP typés et [`validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](validation/006-V0_2_3_HTTP_TRANSACTIONS.md) enregistre les validations finales, graphes Cargo et deux smokes Devnet passés avant publication. Le prompt [`../prompts/009-V0_2_4_START_PROMPT.md`](../prompts/009-V0_2_4_START_PROMPT.md) a ouvert la release stable `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`. Son plan clôturé [`plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) conserve l’implémentation des 15 wrappers et la compliance `52/52 + 14/14`; la matrice finale [`validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) enregistre le réaudit SIMD/inventaire, les canaries globales et les preuves opérateur avant publication. Le prompt [`../prompts/010-V0_2_5_START_PROMPT.md`](../prompts/010-V0_2_5_START_PROMPT.md), finalisé par `0.2.4-pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`. Son plan historique clôturé [`plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) part du gate `pre.001` (héritage, threat model offline, VIEW/OWNER indépendants et niveau B read-only), puis matérialise la crate en `pre.002`, le wire/transcript en `pre.003`, les primitives Argon2id/XChaCha20-Poly1305 en `pre.004`, les payloads/create/open en `pre.005`, la persistence en `pre.006`, l'administration/signature en `pre.007` et les adapters transfer en `pre.008`. `pre.009` ferme l'audit adversarial/interoperability/compliance dans [`validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) avant la documentation finale `pre.010` ; `pre.010` finalise [`../crates/ksp-wallet-lib/README.md`](../crates/ksp-wallet-lib/README.md), [`../crates/ksp-wallet-lib/USAGE.md`](../crates/ksp-wallet-lib/USAGE.md), la spec, les graphes et la matrice ; `pre.010-fix.001`–`fix.003` ferment ensuite la mise à niveau Dalek et la normalisation Rust/audit structurel. `0.2.5-rel.001` publie la release stable et [`../prompts/011-V0_2_6_START_PROMPT.md`](../prompts/011-V0_2_6_START_PROMPT.md) ouvre `0.2.6 — Wallet Desk`. Le gate `0.2.6-pre.001` est matérialisé dans [`plans/013-V0_2_6_WALLET_DESK_PLAN.md`](plans/013-V0_2_6_WALLET_DESK_PLAN.md) : il réaudite Config Desk et les APIs finales, retient le gabarit Bootstrap/Font Awesome/DataTables/Select/SimpleBar/resize-observer-polyfill, fixe `std.wallet` avec root global + sous-répertoire de profil, création automatique des répertoires, secrets `KSP_SECRET_WALLET_PASS_*` via Config, inventory/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps et balance HTTP. Le forecast souple est détaillé jusqu’à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final. `0.2.6-pre.002` matérialise la crate `ksp-app-wallet-desk` et son shell Tauri splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et bridge frontend Logging ; les contrats Wallet/Transport restent volontairement réservés aux tranches suivantes.
## Spécifications de formats
diff --git a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
index a39c2a1..76c96e7 100644
--- a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
+++ b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
@@ -1,5 +1,5 @@
-
+
# Plan `0.2.6` — Wallet Desk
@@ -21,6 +21,8 @@ La base auditée est la release stable fournie `v0.2.5`, avec `workspace.package
Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.001` : `cargo fmt --all`, `python3 scripts/audit_rust_workspace_rules.py`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont verts, sans warning signalé. Le présent plan intègre les corrections documentaires de `pre.001-fix.001` puis `pre.001-fix.002` sans modifier la version Cargo, car aucun fichier code/build/runtime/configuration exécutable n'est touché. `pre.001-fix.002` aligne en particulier le workflow frontend sur la règle KSP : npm direct sert uniquement à installer ou mettre à jour les dépendances ; les cycles dev/build passent exclusivement par Cargo/Tauri.
+`0.2.6-pre.002` matérialise ensuite la première tranche technique : la crate `ksp-app-wallet-desk` rejoint le workspace avec le shell splash/main du gabarit Config Desk, le bootstrap Config + Logging commun, les ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et le bridge frontend -> Rust -> `ksp-logging-lib`. Cette tranche ne branche encore ni `std.wallet`, ni inventory filesystem réel, ni `ksp-wallet-lib`, ni Transport : ces responsabilités restent dans les prereleases déjà dimensionnées.
+
## 2. Sources relues et hiérarchie appliquée
L'audit suit l'ordre d'autorité demandé :
@@ -1218,7 +1220,9 @@ Cette tranche se termine par le présent plan/delta et les validations opérateu
### `pre.002` — crate Tauri et shell complet du gabarit
-Objectifs :
+État : **matérialisé dans le delta `0.2.6-pre.002`**, sous réserve des validations opérateur Cargo/Tauri.
+
+Objectifs/livrables :
```text
crates/ksp-app-wallet-desk