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 — Dashboard + Initialisation du shell… +
+ +
+
+
+
+ +
+
+
+
+

Dashboard

+

Fondation UI de Wallet Desk — fonctionnalités Wallet branchées dans les tranches suivantes.

+
+ 0.2.6-pre.002 +
+
+
+
+
+
Runtime
+
+
+
Version
+
Phase
+
Documents Config enregistrés
+
Profil Logging
+
Fallback Logging
+
+
+
+
+
+
+
Wallet courant
+
+ +

Aucun wallet sélectionné.

+
+
+
+
+
+ + + + + +
+
+
+
+ + + + 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