v0.2.6-pre.014

This commit is contained in:
2026-08-22 00:21:14 +02:00
parent c07db925b1
commit 6907e4bbb8
39 changed files with 1544 additions and 370 deletions

View File

@@ -1,5 +1,5 @@
# file: .env.example # file: .env.example
# version: 3 # version: 4
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
@@ -11,7 +11,8 @@ KSP_WALLETS_DIRECTORY=wallets
# Optional Wallet Desk password candidates are introduced by the later unlock tranche. # Optional Wallet Desk password candidates are introduced by the later unlock tranche.
# Additional KSP_SECRET_WALLET_PASS_<LABEL> variables may be defined locally; never commit real password values. # Additional KSP_SECRET_WALLET_PASS_<LABEL> variables may be defined locally; never commit real password values.
# KSP_SECRET_WALLET_PASS_01= # KSP_SECRET_WALLET_PASS_WALLET_01='test'
# KSP_SECRET_WALLET_PASS_01='test'
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json. # Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent. # The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
@@ -25,8 +26,11 @@ KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. # Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me # KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready. # Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
KSP_DESK_SPLASH_FADE_IN_MS=300
# Minimum fully-visible time in milliseconds used by the common KSP desk splash lifecycle.
KSP_DESK_SPLASH_MINIMUM_MS=1200 KSP_DESK_SPLASH_MINIMUM_MS=1200
# Fade-in/fade-out duration in milliseconds used by the common KSP desk splash lifecycle. # Fade-out duration in milliseconds used by the common KSP desk splash lifecycle.
KSP_DESK_SPLASH_FADE_MS=300 KSP_DESK_SPLASH_FADE_OUT_MS=300

View File

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

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 75 --> <!-- version: 76 -->
# Roadmap KSP # Roadmap KSP
@@ -50,14 +50,15 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants. - [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication. - [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. - [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [/] `0.2.6``ksp-app-wallet-desk` : composition Config + Wallet + HTTP, inventory/création/unlock/balance/import acquis ; administration OWNER, rotations, lifecycle VIEW, export, intégration/sécurité, polish desktop puis documentation finale restent à clôturer. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. - [/] `0.2.6``ksp-app-wallet-desk` : surface fonctionnelle Wallet Desk acquise ; `pre.014` harmonise le gabarit/splash/Tauri multi-app puis `pre.015` clôture documentation et build final. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
- [ ] `0.2.7`Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe. - [ ] `0.2.7`Durcir la persistence `.kspwallet` dans `ksp-wallet-lib` avec un conteneur binaire versionné rétrocompatible ; conserver les semantics cryptographiques V1 et rejouer Wallet Desk comme consommateur sans changement fonctionnel attendu.
- [ ] `0.2.8`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [ ] `0.2.8`É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.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `0.2.9` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.10`Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [ ] `0.2.10`Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
- [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. - [ ] `0.2.11` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. - [ ] `0.2.12` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `0.2.13` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
- [ ] `0.2.14` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
### Règles Transport pour toute la série ### Règles Transport pour toute la série

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md --> <!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 24 --> <!-- version: 25 -->
# `ksp-app-config-desk` # `ksp-app-config-desk`
@@ -82,7 +82,7 @@ Les DTO Rust applicatifs restent la source de vérité et les bindings généré
Le frontend possède désormais `shell_registry.ts`. Le registre décrit les vues du shell et les adapters d'éditeurs spécialisés par `file_id`. Le panneau Documents reste générique ; lorsqu'un document possède un adapter enregistré, **Ouvrir l'éditeur spécialisé** déclenche la navigation par événement de registre. `cfg.std.logging -> Logging` est le premier adapter. Ajouter un futur éditeur ne demande donc pas de réécrire le moteur Documents ni la logique générale d'activation des panneaux. Le frontend possède désormais `shell_registry.ts`. Le registre décrit les vues du shell et les adapters d'éditeurs spécialisés par `file_id`. Le panneau Documents reste générique ; lorsqu'un document possède un adapter enregistré, **Ouvrir l'éditeur spécialisé** déclenche la navigation par événement de registre. `cfg.std.logging -> Logging` est le premier adapter. Ajouter un futur éditeur ne demande donc pas de réécrire le moteur Documents ni la logique générale d'activation des panneaux.
Deux audits d'intégration applicatifs complètent les audits Config/Logging existants : contrat Tauri/Vite/package scripts, centralisation des commandes Tauri, absence de `tauri-plugin-log`, interdiction des dialogues navigateur natifs et absence de stockage persistant pour le reveal Secret. Le script frontend `npm run check` exécute uniquement `tsc --noEmit`. Le build Vite de production nest pas lancé séparément : il appartient au `beforeBuildCommand` de `cargo tauri build`, qui reste la dernière validation après tous les contrôles Rust, le type-check frontend et le parcours fonctionnel `tauri dev`. Deux audits d'intégration applicatifs complètent les audits Config/Logging existants : contrat Tauri/Vite/package scripts, centralisation des commandes Tauri, absence de `tauri-plugin-log`, interdiction des dialogues navigateur natifs et absence de stockage persistant pour le reveal Secret. Aucun script frontend standalone nest lancé par lopérateur : Tauri possède le cycle dev/build et déclenche les hooks npm de la crate. Le build Vite/TypeScript de production appartient au `beforeBuildCommand` du `cargo tauri build` crate-local, qui reste la dernière validation après tous les contrôles Rust et le parcours fonctionnel `cargo tauri dev`.
## Artefacts frontend ## Artefacts frontend
@@ -134,7 +134,7 @@ Rust valide le niveau et le `targetId`, choisit un callsite statique puis émet
## Bootstrap Config et Logging ## Bootstrap Config et Logging
Au démarrage, l'application construit `ConfigBootstrapOptions`, `ConfigFileRegistry`, `ConfigDocumentEngine` et `ConfigManagement` à partir des arguments complets du processus. En développement, le launcher normalise d'abord le current working directory Rust vers la racine du workspace : `cargo tauri dev -c ...` peut sinon lancer le binaire depuis la crate Tauri, ce qui ferait manquer les chemins Config relatifs `config/`, `config/schemas/` et le `.env` racine. Cette normalisation ne lit ni ne parse aucun fichier Config elle-même ; l'ownership reste intégralement dans `ksp-config-lib`. Elle tente ensuite de résoudre le profil Logging par défaut avec un snapshot `ConfigEnvironment` frais. Au démarrage, l'application construit `ConfigBootstrapOptions`, `ConfigFileRegistry`, `ConfigDocumentEngine` et `ConfigManagement` à partir des arguments complets du processus. En développement, le launcher normalise d'abord le current working directory Rust vers la racine du workspace : `cargo tauri dev` est volontairement lancé depuis la crate Tauri, ce qui ferait manquer les chemins Config relatifs `config/`, `config/schemas/` et le `.env` racine. Cette normalisation ne lit ni ne parse aucun fichier Config elle-même ; l'ownership reste intégralement dans `ksp-config-lib`. Elle tente ensuite de résoudre le profil Logging par défaut avec un snapshot `ConfigEnvironment` frais.
Si le document Logging, sa résolution environnementale ou l'initialisation du runtime configuré échoue avant installation du subscriber, Config Desk installe un fallback **transitoire** en mémoire : console stderr, niveau `Info`, aucun fichier, aucun span lifecycle. Ce fallback n'est jamais persisté et son diagnostic frontend est limité à `domain`, `code` et `message`. Si le document Logging, sa résolution environnementale ou l'initialisation du runtime configuré échoue avant installation du subscriber, Config Desk installe un fallback **transitoire** en mémoire : console stderr, niveau `Info`, aucun fichier, aucun span lifecycle. Ce fallback n'est jamais persisté et son diagnostic frontend est limité à `domain`, `code` et `message`.
@@ -146,7 +146,8 @@ Le `LoggingGuard` est conservé dans `AppState` et sert désormais au hot reload
```text ```text
KSP_DESK_SPLASH_MINIMUM_MS=1200 KSP_DESK_SPLASH_MINIMUM_MS=1200
KSP_DESK_SPLASH_FADE_MS=300 KSP_DESK_SPLASH_FADE_IN_MS=300
KSP_DESK_SPLASH_FADE_OUT_MS=300
``` ```
Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale, émet `fade_out`, attend la durée de fade puis `tw_main.rs` affiche/focalise `main` avant destruction du splash. Le backend journalise en `debug` les valeurs résolues, leur provenance (`process`, `dotenv` ou `fallback`), chaque attente réellement observée et la durée totale. Avec `12000/3000`, la durée backend attendue entre readiness et activation de `main` est donc d'environ `15000 ms`. Une valeur splash invalide ne bloque pas le manager : des timings de fallback sûrs restent en mémoire afin que la future surface `.env` puisse permettre la réparation. Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale, émet `fade_out`, attend la durée de fade puis `tw_main.rs` affiche/focalise `main` avant destruction du splash. Le backend journalise en `debug` les valeurs résolues, leur provenance (`process`, `dotenv` ou `fallback`), chaque attente réellement observée et la durée totale. Avec `12000/3000`, la durée backend attendue entre readiness et activation de `main` est donc d'environ `15000 ms`. Une valeur splash invalide ne bloque pas le manager : des timings de fallback sûrs restent en mémoire afin que la future surface `.env` puisse permettre la réparation.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md --> <!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 24 --> <!-- version: 25 -->
# Utilisation de `ksp-app-config-desk` # Utilisation de `ksp-app-config-desk`
@@ -31,7 +31,7 @@ cd ../../
Le frontend n'est pas lancé directement avec npm. KSP est un workspace Rust multi-app : depuis la racine du workspace, la configuration Tauri de l'application doit être sélectionnée explicitement : Le frontend n'est pas lancé directement avec npm. KSP est un workspace Rust multi-app : depuis la racine du workspace, la configuration Tauri de l'application doit être sélectionnée explicitement :
```bash ```bash
cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json (cd crates/ksp-app-config-desk && cargo tauri dev)
``` ```
Tauri exécute alors le hook : Tauri exécute alors le hook :
@@ -54,7 +54,7 @@ Si le port HTTP est déjà occupé, le démarrage doit échouer au lieu de séle
Le build de l'application passe également par Tauri. Depuis la racine du workspace : Le build de l'application passe également par Tauri. Depuis la racine du workspace :
```bash ```bash
cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json (cd crates/ksp-app-config-desk && cargo tauri build)
``` ```
`beforeBuildCommand` déclenche : `beforeBuildCommand` déclenche :
@@ -112,12 +112,14 @@ Les variables communes sont :
```text ```text
KSP_DESK_SPLASH_MINIMUM_MS=1200 KSP_DESK_SPLASH_MINIMUM_MS=1200
KSP_DESK_SPLASH_FADE_MS=300 KSP_DESK_SPLASH_FADE_IN_MS=300
KSP_DESK_SPLASH_FADE_OUT_MS=300
``` ```
La priorité est celle de Config : process > `.env` > fallback. Les valeurs sont lues uniquement via `ksp-config-lib`. Une valeur non numérique ou hors borne déclenche un fallback runtime en mémoire plutôt qu'un refus de démarrer Config Desk. La priorité est celle de Config : process > `.env` > fallback. Les valeurs sont lues uniquement via `ksp-config-lib`. Une valeur non numérique ou hors borne déclenche un fallback runtime en mémoire plutôt qu'un refus de démarrer Config Desk.
Le frontend `splash.ts` installe son listener puis appelle `splash_frontend_ready`. Le backend vérifie que l'appel provient réellement de la WebView `splash`; une readiness dupliquée (par exemple après reload Vite) est ignorée. La durée minimale commence à cette readiness : Rust émet le fade-in, attend `minimum`, émet le fade-out, attend `fade`, puis active `main`. Ainsi `KSP_DESK_SPLASH_MINIMUM_MS=12000` et `KSP_DESK_SPLASH_FADE_MS=3000` donnent environ `15000 ms` de lifecycle backend. Des logs `debug` indiquent source des deux valeurs, attentes configurées/réelles et durée totale afin de vérifier ce contrat. Le frontend `splash.ts` installe son listener puis appelle `splash_frontend_ready`. Le backend vérifie que l'appel provient réellement de la WebView `splash`; une readiness dupliquée (par exemple après reload Vite) est ignorée. La durée minimale commence à cette readiness : Rust émet le fade-in, attend `fade_in`, maintient ensuite le splash pleinement visible pendant `minimum`, émet le fade-out, attend `fade_out`, puis active `main`. Ainsi `KSP_DESK_SPLASH_MINIMUM_MS=12000` et `KSP_DESK_SPLASH_FADE_IN_MS=300
KSP_DESK_SPLASH_FADE_OUT_MS=3000` donnent environ `15000 ms` de lifecycle backend. Des logs `debug` indiquent source des trois valeurs, attentes configurées/réelles et durée totale afin de vérifier ce contrat.
## Navigation principale ## Navigation principale
@@ -311,24 +313,18 @@ cargo test -p ksp-config-lib
cargo test -p ksp-logging-lib cargo test -p ksp-logging-lib
``` ```
Puis effectuer uniquement le type-check frontend depuis `crates/ksp-app-config-desk` : Aucun script npm de contrôle/dev/build nest lancé directement par lopérateur. Tauri possède le cycle frontend et déclenche les hooks npm de la crate.
Effectuer ensuite le contrôle fonctionnel depuis la racine en lançant Tauri dans la crate ciblée :
```bash ```bash
npm run check (cd crates/ksp-app-config-desk && cargo tauri dev)
```
`npm run check` exécute seulement `tsc --noEmit`. Il ne lance pas `vite build` : le build frontend de production est déjà la responsabilité de Tauri via `beforeBuildCommand = npm run build`.
Revenir ensuite à la racine et effectuer le contrôle fonctionnel :
```bash
cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json
``` ```
**Seulement lorsque tous les contrôles précédents sont validés**, exécuter en toute dernière opération : **Seulement lorsque tous les contrôles précédents sont validés**, exécuter en toute dernière opération :
```bash ```bash
cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json (cd crates/ksp-app-config-desk && cargo tauri build)
``` ```
Cette dernière commande déclenche elle-même `npm run build`, donc `tsc && vite build`, avant le build/bundling Tauri. Aucun build Vite séparé nest nécessaire dans le cycle de validation. Cette dernière commande déclenche elle-même `npm run build`, donc `tsc && vite build`, avant le build/bundling Tauri. Aucun build Vite séparé nest nécessaire dans le cycle de validation.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html --> <!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 16 --> <!-- version: 17 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -21,20 +21,25 @@
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span> <span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span>
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Vue d'ensemble</span> <span id="headerViewTitle" class="fs-5 text-body text-nowrap">Vue d'ensemble</span>
</div> </div>
<div class="app-nav nav nav-pills flex-nowrap gap-1 ms-auto" aria-label="Navigation Config Desk">
<button class="nav-link app-nav-button active" type="button" data-view="overview">Vue d'ensemble</button>
<button class="nav-link app-nav-button" type="button" data-view="documents">Documents</button>
<button class="nav-link app-nav-button" type="button" data-view="profiles">Profils</button>
<button class="nav-link app-nav-button" type="button" data-view="environment">Environnement / .env</button>
<button class="nav-link app-nav-button" type="button" data-view="logging">Logging</button>
</div>
</div> </div>
</nav> </nav>
</header> </header>
<main class="app-main"> <main class="app-main container-fluid p-0">
<div class="app-scrollable" data-simplebar> <div class="row g-0 h-100">
<div class="container-fluid px-4 py-4"> <aside class="col-auto app-sidebar border-end bg-body-tertiary">
<div class="app-sidebar-scroll h-100" data-simplebar>
<nav class="nav nav-pills flex-column gap-1 p-3" aria-label="Navigation Config Desk">
<button class="nav-link text-start active" type="button" data-view="overview"><i class="fa-solid fa-gauge-high me-2" aria-hidden="true"></i>Vue d'ensemble</button>
<button class="nav-link text-start" type="button" data-view="documents"><i class="fa-solid fa-file-code me-2" aria-hidden="true"></i>Documents</button>
<button class="nav-link text-start" type="button" data-view="profiles"><i class="fa-solid fa-layer-group me-2" aria-hidden="true"></i>Profils</button>
<button class="nav-link text-start" type="button" data-view="environment"><i class="fa-solid fa-terminal me-2" aria-hidden="true"></i>Environnement / .env</button>
<button class="nav-link text-start" type="button" data-view="logging"><i class="fa-solid fa-list-ul me-2" aria-hidden="true"></i>Logging</button>
</nav>
</div>
</aside>
<section class="col app-content app-scrollable h-100" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="card app-shell-card mx-auto shadow-sm border-0"> <div class="card app-shell-card mx-auto shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<h1 id="viewTitle" class="h3 mb-2">Vue d'ensemble</h1> <h1 id="viewTitle" class="h3 mb-2">Vue d'ensemble</h1>
@@ -547,7 +552,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</div> </div>
</main> </main>

View File

@@ -1,8 +1,9 @@
// file: crates/ksp-app-config-desk/frontend/sass/_app.scss // file: crates/ksp-app-config-desk/frontend/sass/_app.scss
// version: 5 // version: 6
$app-header-height: 72px; $app-header-height: 72px;
$app-footer-height: 42px; $app-footer-height: 42px;
$app-sidebar-width: 250px;
html, html,
body { body {
@@ -59,13 +60,19 @@ body {
font-family: var(--bs-font-monospace); font-family: var(--bs-font-monospace);
} }
.app-nav { .app-sidebar {
min-width: 0; width: $app-sidebar-width;
overflow-x: auto; min-width: $app-sidebar-width;
scrollbar-width: thin; max-width: $app-sidebar-width;
} }
.app-nav-button { .app-sidebar-scroll,
.app-content {
height: 100%;
max-height: 100%;
}
.app-sidebar .nav-link {
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/splash.scss // file: crates/ksp-app-config-desk/frontend/sass/splash.scss
// version: 3 // version: 4
@font-face { @font-face {
font-family: "Dos Amazigh"; font-family: "Dos Amazigh";
@@ -9,13 +9,23 @@
font-display: swap; font-display: swap;
} }
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body { body {
display: flex; width: 100%;
width: 100vw; height: 100%;
height: 100vh;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
}
body {
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
@@ -25,8 +35,12 @@ body {
#splash-container { #splash-container {
position: relative; position: relative;
width: 960px; width: 960px;
max-width: 100%;
height: 637px; height: 637px;
max-height: 100%;
overflow: hidden;
opacity: 0; opacity: 0;
will-change: opacity;
} }
#splash-image { #splash-image {
@@ -35,6 +49,11 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; z-index: 1;
object-fit: contain;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
} }
#app-name { #app-name {
@@ -49,17 +68,70 @@ body {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
} }
#splash-status { #debug-info {
position: absolute; position: absolute;
right: 1rem; top: 0;
bottom: 1rem; left: 0;
left: 1rem;
z-index: 3; z-index: 3;
padding: 0.55rem 0.75rem; width: 55%;
max-height: 30%;
overflow-y: auto;
padding: 8px;
color: #fff; color: #fff;
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.18);
font-family: monospace; font-family: monospace;
font-size: 0.82rem; font-size: 12px;
scrollbar-width: none;
}
#messages-container {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 3;
max-height: 30%;
overflow-y: auto;
padding: 10px 10px 36px;
background: rgba(0, 0, 0, 0.18);
font-family: Arial, sans-serif;
font-size: 14px;
scrollbar-width: none;
}
#debug-info::-webkit-scrollbar,
#messages-container::-webkit-scrollbar {
display: none;
}
.splash-message {
margin-bottom: 8px;
padding: 6px 10px;
border-radius: 4px;
color: #fff;
font-weight: bold;
animation: splashMessageFadeIn 0.3s ease-in-out;
}
.splash-message.info { background: rgba(33, 150, 243, 0.22); }
.splash-message.warning { background: rgba(255, 152, 0, 0.22); }
.splash-message.error { background: rgba(244, 67, 54, 0.22); }
.splash-message.success { background: rgba(76, 175, 80, 0.22); }
@keyframes splashMessageFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/splash.html --> <!-- file: crates/ksp-app-config-desk/frontend/splash.html -->
<!-- version: 1 --> <!-- version: 2 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -11,10 +11,11 @@
</head> </head>
<body> <body>
<div id="splash-container"> <div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Config Desk"> <img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Config Desk">
<div id="app-name">KSP</div> <div id="app-name">Config Desk</div>
<div id="splash-status" aria-live="polite">Chargement du shell frontend...</div> <div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div>
</div> </div>
<script type="module" src="ts/splash.ts" defer></script> <script type="module" src="ts/splash.ts" defer></script>
</body> </body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/splash.ts // file: crates/ksp-app-config-desk/frontend/ts/splash.ts
// version: 3 // version: 4
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
@@ -9,21 +9,14 @@ import { invokeKsp } from "./invoke";
installFrontendConsoleBridge("splash"); installFrontendConsoleBridge("splash");
const defaultFadeDurationMs = 300;
let activeOpacityFrame: number | null = null; let activeOpacityFrame: number | null = null;
function normalizeDurationMs(value: number | null | undefined): number { function easeInOut(value: number): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
return defaultFadeDurationMs;
}
return value;
} }
function easeInOut(progress: number): number { function normalizeDurationMs(value: number | null): number {
if (progress < 0.5) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
return 2 * progress * progress;
}
return 1 - Math.pow(-2 * progress + 2, 2) / 2;
} }
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> { async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
@@ -40,7 +33,7 @@ async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpaci
const opacityDelta = toOpacity - fromOpacity; const opacityDelta = toOpacity - fromOpacity;
const updateOpacity = (currentTime: number): void => { const updateOpacity = (currentTime: number): void => {
const elapsedMs = currentTime - startedAt; const elapsedMs = currentTime - startedAt;
const rawProgress = Math.min(elapsedMs / durationMs, 1); const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1);
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString(); element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
if (rawProgress >= 1) { if (rawProgress >= 1) {
element.style.opacity = toOpacity.toString(); element.style.opacity = toOpacity.toString();
@@ -56,21 +49,46 @@ async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpaci
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs }); frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
} }
function replaceStatus(message: string | null): void { function scrollToLatest(element: HTMLElement): void {
if (!message) { element.scrollTop = element.scrollHeight;
}
function addMessage(message: string, status: string | null): void {
const container = document.querySelector<HTMLElement>("#messages-container");
if (!container) {
return; return;
} }
const status = document.querySelector<HTMLElement>("#splash-status"); const line = document.createElement("div");
if (status) { const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
status.textContent = message; line.className = `splash-message ${safeStatus}`;
frontendTrace("splash", "Splash status replaced", { message }); line.textContent = message;
container.appendChild(line);
scrollToLatest(container);
}
function addDebugMessage(message: string): void {
const container = document.querySelector<HTMLElement>("#debug-info");
if (!container) {
return;
} }
container.hidden = false;
const line = document.createElement("div");
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
container.appendChild(line);
scrollToLatest(container);
} }
async function handleSplashOrder(order: SplashOrderDto): Promise<void> { async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
frontendTrace("splash", "Splash order received", { action: order.action }); frontendTrace("splash", "Splash order received", { action: order.action });
const container = document.querySelector<HTMLElement>("#splash-container"); const container = document.querySelector<HTMLElement>("#splash-container");
replaceStatus(order.message); if (order.action === "add_message" && order.message) {
addMessage(order.message, order.status);
return;
}
if (order.action === "add_debug" && order.message) {
addDebugMessage(order.message);
return;
}
if (!container) { if (!container) {
return; return;
} }
@@ -99,7 +117,7 @@ async function initializeSplash(): Promise<void> {
try { try {
await invokeKsp<void>("splash", "splash_frontend_ready"); await invokeKsp<void>("splash", "splash_frontend_ready");
} catch { } catch {
replaceStatus("Le lifecycle du splash n'a pas pu démarrer."); addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
if (container) { if (container) {
container.style.opacity = "1"; container.style.opacity = "1";
container.style.willChange = "auto"; container.style.willChange = "auto";

View File

@@ -1,14 +1,11 @@
{ {
"name": "ksp-app-config-desk", "name": "ksp-app-config-desk",
"private": true, "private": true,
"version": "0.1.4", "version": "0.2.6-pre.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build"
"preview": "vite preview",
"tauri": "tauri",
"check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3", "@fltsci/tauri-plugin-tracing": "^0.3",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/app_state.rs // file: crates/ksp-app-config-desk/src/app_state.rs
// version: 8 // version: 9
//! Shared backend state owned by the Tauri application. //! Shared backend state owned by the Tauri application.
@@ -48,8 +48,10 @@ impl AppState {
domain = crate::TRACING_DOMAIN_WINDOWS, domain = crate::TRACING_DOMAIN_WINDOWS,
minimum_ms = splash_settings.minimum_ms(), minimum_ms = splash_settings.minimum_ms(),
minimum_source = splash_settings.minimum_source(), minimum_source = splash_settings.minimum_source(),
fade_ms = splash_settings.fade_ms(), fade_in_ms = splash_settings.fade_in_ms(),
fade_source = splash_settings.fade_source(), fade_in_source = splash_settings.fade_in_source(),
fade_out_ms = splash_settings.fade_out_ms(),
fade_out_source = splash_settings.fade_out_source(),
expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(),
"resolved Config Desk splash timings" "resolved Config Desk splash timings"
); );

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-app-config-desk/src/splash.rs // file: crates/ksp-app-config-desk/src/splash.rs
// version: 5 // version: 6
//! Common splash settings and frontend event contracts for Config Desk. //! Common splash settings and frontend event contracts for Config Desk.
use ts_rs::TS; // rust-rules: trait-import use ts_rs::TS; // rust-rules: trait-import
const DEFAULT_SPLASH_FADE_MS: u32 = 300; const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200; const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS"; const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS"; const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const MAX_SPLASH_FADE_MS: u32 = 10_000; const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
@@ -15,10 +17,12 @@ const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle. /// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SplashSettings { pub(crate) struct SplashSettings {
fade_in_ms: u32,
minimum_ms: u64, minimum_ms: u64,
fade_ms: u32, fade_out_ms: u32,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
minimum_source: ksp_config_lib::ConfigEnvironmentSource, minimum_source: ksp_config_lib::ConfigEnvironmentSource,
fade_source: ksp_config_lib::ConfigEnvironmentSource, fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
} }
impl SplashSettings { impl SplashSettings {
@@ -29,13 +33,23 @@ impl SplashSettings {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
let fade_in = match fade_in {
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 = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
let minimum = match minimum { let minimum = match minimum {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
let fade = match fade { let fade_out = match fade_out {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
let fade_in_ms = match fade_in_ms {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
@@ -44,35 +58,56 @@ impl SplashSettings {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
let fade_ms = match fade_ms { let fade_out_ms = match fade_out_ms {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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() }); return std::result::Result::Ok(Self {
fade_in_ms,
minimum_ms,
fade_out_ms,
fade_in_source: fade_in.source(),
minimum_source: minimum.source(),
fade_out_source: fade_out.source(),
});
} }
/// Returns safe in-memory timings used when the managed environment cannot be resolved. /// Returns safe in-memory timings used when the managed environment cannot be resolved.
#[must_use] #[must_use]
pub(crate) const fn fallback() -> Self { pub(crate) const fn fallback() -> Self {
return Self { return Self {
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS, minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
fade_ms: DEFAULT_SPLASH_FADE_MS, fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
fade_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
}; };
} }
/// Returns the minimum visible duration after splash frontend readiness. /// Returns the configured fade-in duration.
#[must_use]
pub(crate) const fn fade_in_ms(self) -> u32 {
return self.fade_in_ms;
}
/// Returns the minimum fully-visible splash duration.
#[must_use] #[must_use]
pub(crate) const fn minimum_ms(self) -> u64 { pub(crate) const fn minimum_ms(self) -> u64 {
return self.minimum_ms; return self.minimum_ms;
} }
/// Returns the fade duration used for both fade-in and fade-out. /// Returns the configured fade-out duration.
#[must_use] #[must_use]
pub(crate) const fn fade_ms(self) -> u32 { pub(crate) const fn fade_out_ms(self) -> u32 {
return self.fade_ms; return self.fade_out_ms;
}
/// Returns the safe provenance code for the fade-in duration.
#[must_use]
pub(crate) const fn fade_in_source(self) -> &'static str {
return environment_source_code(self.fade_in_source);
} }
/// Returns the safe provenance code for the minimum duration. /// Returns the safe provenance code for the minimum duration.
@@ -81,16 +116,16 @@ impl SplashSettings {
return environment_source_code(self.minimum_source); return environment_source_code(self.minimum_source);
} }
/// Returns the safe provenance code for the fade duration. /// Returns the safe provenance code for the fade-out duration.
#[must_use] #[must_use]
pub(crate) const fn fade_source(self) -> &'static str { pub(crate) const fn fade_out_source(self) -> &'static str {
return environment_source_code(self.fade_source); return environment_source_code(self.fade_out_source);
} }
/// Returns the minimum backend lifecycle duration from readiness until main activation. /// Returns the expected additive backend lifecycle from readiness through fade-out.
#[must_use] #[must_use]
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 { pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
return self.minimum_ms + u64::from(self.fade_ms); return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
} }
} }
@@ -103,6 +138,8 @@ pub(crate) struct SplashOrderDto {
pub(crate) action: String, pub(crate) action: String,
/// Optional status text. /// Optional status text.
pub(crate) message: std::option::Option<String>, pub(crate) message: std::option::Option<String>,
/// Optional message severity used by the bottom message feed.
pub(crate) status: std::option::Option<String>,
/// Optional animation duration in milliseconds. /// Optional animation duration in milliseconds.
pub(crate) duration_ms: std::option::Option<u32>, pub(crate) duration_ms: std::option::Option<u32>,
} }
@@ -110,8 +147,13 @@ pub(crate) struct SplashOrderDto {
impl SplashOrderDto { impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value. /// Creates a new `SplashOrderDto` value.
#[must_use] #[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self { pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms }; return Self {
action: action.to_owned(),
message: message.map(std::string::ToString::to_string),
status: status.map(std::string::ToString::to_string),
duration_ms,
};
} }
} }

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs // file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 5 // version: 6
//! Tauri-window lifecycle for the Config Desk splash window. //! Tauri-window lifecycle for the Config Desk splash window.
use tauri::Emitter; // rust-rules: trait-import use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_SPLASH` constant. /// Crate-internal splash window label.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash"; pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order"; const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
@@ -23,7 +23,7 @@ pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_c
}; };
} }
/// Executes the crate-internal splash frontend ready service operation for the owning module. /// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) async fn splash_frontend_ready_service( pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle, app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow, invoking_window: tauri::WebviewWindow,
@@ -36,12 +36,7 @@ pub(crate) async fn splash_frontend_ready_service(
); );
} }
if !state.begin_splash_sequence() { if !state.begin_splash_sequence() {
ksp_logging_lib::trace!( ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
"duplicate splash frontend readiness ignored"
);
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }
let settings = state.splash_settings(); let settings = state.splash_settings();
@@ -50,49 +45,60 @@ pub(crate) async fn splash_frontend_ready_service(
target: crate::TRACING_TARGET, target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS, domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH, window_label = WINDOW_LABEL_SPLASH,
fade_in_ms = settings.fade_in_ms(),
fade_in_source = settings.fade_in_source(),
minimum_ms = settings.minimum_ms(), minimum_ms = settings.minimum_ms(),
minimum_source = settings.minimum_source(), minimum_source = settings.minimum_source(),
fade_ms = settings.fade_ms(), fade_out_ms = settings.fade_out_ms(),
fade_source = settings.fade_source(), fade_out_source = settings.fade_out_source(),
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
"splash frontend readiness accepted; starting splash to main lifecycle" "splash frontend readiness accepted; starting splash to main lifecycle"
); );
let fade_in = emit_order( let emitted = emit_order(
&invoking_window, &invoking_window,
crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Config Desk..."), std::option::Option::Some(settings.fade_ms())), crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
); );
if let std::result::Result::Err(error) = fade_in { if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let minimum_wait_started = std::time::Instant::now(); let emitted = emit_message(&invoking_window, "Initialisation de Config Desk...", "info");
tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await; if let std::result::Result::Err(error) = emitted {
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); return std::result::Result::Err(error);
} }
let fade_wait_started = std::time::Instant::now(); let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await; if let std::result::Result::Err(error) = emitted {
ksp_logging_lib::debug!( return std::result::Result::Err(error);
target: crate::TRACING_TARGET, }
domain = crate::TRACING_DOMAIN_WINDOWS, sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
window_label = WINDOW_LABEL_SPLASH, let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
configured_wait_ms = settings.fade_ms(), if let std::result::Result::Err(error) = emitted {
actual_wait_ms = fade_wait_started.elapsed().as_secs_f64() * 1000.0, return std::result::Result::Err(error);
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, }
"splash fade-out wait completed" let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Gestion Config prête.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
); );
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
let show_main = crate::show_main_window(&app); let show_main = crate::show_main_window(&app);
if let std::result::Result::Err(error) = show_main { if let std::result::Result::Err(error) = show_main {
return std::result::Result::Err(error); return std::result::Result::Err(error);
@@ -116,18 +122,45 @@ pub(crate) async fn splash_frontend_ready_service(
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
return emit_order(
window,
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
);
}
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
if !cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
return emit_order(
window,
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
);
}
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
let wait_started = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = window.label(),
phase,
configured_wait_ms = duration_ms,
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash lifecycle wait completed"
);
return;
}
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> { fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
let action = order.action.clone(); let action = order.action.clone();
let emitted = window.emit(SPLASH_EVENT_NAME, order); let emitted = window.emit(SPLASH_EVENT_NAME, order);
return match emitted { return match emitted {
std::result::Result::Ok(()) => { std::result::Result::Ok(()) => {
ksp_logging_lib::trace!( 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");
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::Ok(())
}, },
std::result::Result::Err(error) => std::result::Result::Err( std::result::Result::Err(error) => std::result::Result::Err(

View File

@@ -1,12 +1,18 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Config Desk", "productName": "KSP Config Desk",
"version": "0.1.4", "version": "0.2.6-pre.14",
"identifier": "com.sasedev.ksp-app-config-desk", "identifier": "com.sasedev.ksp-app-config-desk",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1430", "devUrl": "http://localhost:1430",
"beforeBuildCommand": "npm run build", "beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist" "frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist"
}, },
"app": { "app": {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs // file: crates/ksp-app-config-desk/tests/desktop_contract.rs
// version: 2 // version: 3
//! Desktop build/shell contract audits for Config Desk. //! Desktop build/shell contract audits for Config Desk.
@@ -31,8 +31,10 @@ fn tauri_and_frontend_build_contracts_remain_explicit() {
let root = app_root(); let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path()); 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:1430")); assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1430"));
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/beforeDevCommand/script").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("/build/beforeDevCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
assert_eq!(tauri.pointer("/build/beforeBuildCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
assert_eq!(tauri.pointer("/build/beforeBuildCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array); let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
assert!(windows.is_some()); assert!(windows.is_some());
let windows = match windows { let windows = match windows {
@@ -47,11 +49,13 @@ fn tauri_and_frontend_build_contracts_remain_explicit() {
if let std::option::Option::Some(main) = main { if let std::option::Option::Some(main) = main {
assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false)); assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false));
} }
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("name = \"ksp-app-config-desk\""));
let package = read_json(root.join("package.json").as_path()); let package = read_json(root.join("package.json").as_path());
let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str); let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str);
let check = package.pointer("/scripts/check").and_then(serde_json::Value::as_str);
assert!(build.is_some_and(|value| return value.contains("tsc") && value.contains("vite build"))); assert!(build.is_some_and(|value| return value.contains("tsc") && value.contains("vite build")));
assert_eq!(check, std::option::Option::Some("tsc --noEmit")); assert!(package.pointer("/scripts/check").is_none());
assert!(package.pointer("/scripts/preview").is_none());
} }
#[test] #[test]
@@ -70,3 +74,23 @@ fn shell_registry_owns_view_activation_and_specialized_editor_dispatch() {
assert!(documents.contains("requestViewActivation")); assert!(documents.contains("requestViewActivation"));
assert!(html.contains("id=\"openSpecializedEditor\"")); assert!(html.contains("id=\"openSpecializedEditor\""));
} }
#[test]
fn pre_014_template_uses_sidebar_navigation_and_kbot_style_splash_contract() {
let root = app_root();
let main = read_text(root.join("frontend/main.html").as_path());
let splash = read_text(root.join("frontend/splash.html").as_path());
let splash_style = read_text(root.join("frontend/sass/splash.scss").as_path());
let splash_ts = read_text(root.join("frontend/ts/splash.ts").as_path());
assert!(main.starts_with("<!-- file: crates/ksp-app-config-desk/frontend/main.html -->"));
assert!(main.contains("app-sidebar"));
assert!(main.contains("nav nav-pills flex-column"));
assert!(splash.starts_with("<!-- file: crates/ksp-app-config-desk/frontend/splash.html -->"));
assert!(splash.contains("id=\"debug-info\""));
assert!(splash.contains("id=\"messages-container\""));
assert!(splash.contains("sass/splash.scss"));
assert!(splash_style.contains("overflow: hidden"));
assert!(splash_style.contains("user-select: none"));
assert!(splash_ts.contains("add_debug"));
assert!(splash_ts.contains("add_message"));
}

View File

@@ -1,22 +1,25 @@
// file: crates/ksp-app-config-desk/unit_tests/splash.rs // file: crates/ksp-app-config-desk/unit_tests/splash.rs
// version: 3 // version: 4
#[test] #[test]
fn fallback_splash_settings_are_short_and_ordered() { fn fallback_splash_settings_are_short_and_ordered() {
let settings = crate::SplashSettings::fallback(); let settings = crate::SplashSettings::fallback();
assert_eq!(settings.fade_in_ms(), 300);
assert_eq!(settings.minimum_ms(), 1200); assert_eq!(settings.minimum_ms(), 1200);
assert_eq!(settings.fade_ms(), 300); assert_eq!(settings.fade_out_ms(), 300);
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
assert_eq!(settings.fade_in_source(), "fallback");
assert_eq!(settings.minimum_source(), "fallback"); assert_eq!(settings.minimum_source(), "fallback");
assert_eq!(settings.fade_source(), "fallback"); assert_eq!(settings.fade_out_source(), "fallback");
assert_eq!(settings.expected_backend_lifecycle_ms(), 1500);
assert!(settings.minimum_ms() >= u64::from(settings.fade_ms()));
} }
#[test] #[test]
fn splash_order_contract_keeps_action_message_and_duration() { fn splash_order_contract_keeps_action_message_status_and_duration() {
let order = crate::SplashOrderDto::new("fade_out", std::option::Option::Some("done"), std::option::Option::Some(300)); let order =
assert_eq!(order.action, "fade_out"); crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
assert_eq!(order.message.as_deref(), std::option::Option::Some("done")); assert_eq!(order.action, "add_message");
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
assert_eq!(order.duration_ms, std::option::Option::Some(300)); assert_eq!(order.duration_ms, std::option::Option::Some(300));
} }
@@ -24,14 +27,10 @@ fn splash_order_contract_keeps_action_message_and_duration() {
fn splash_duration_parser_rejects_invalid_or_unbounded_values() { fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000); 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_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); let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
assert!(malformed.is_err()); assert!(malformed.is_err());
assert!(oversized_minimum.is_err()); assert!(oversized_minimum.is_err());
assert!(oversized_fade.is_err()); assert!(oversized_fade_in.is_err());
if let std::result::Result::Err(error) = oversized_minimum { assert!(oversized_fade_out.is_err());
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"));
}
} }

View File

@@ -1,4 +1,6 @@
<!doctype html> <!-- file: crates/ksp-app-wallet-desk/frontend/main.html -->
<!-- version: 1 -->
<!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@@ -6,20 +8,22 @@
<title>Wallet Desk — Dashboard</title> <title>Wallet Desk — Dashboard</title>
</head> </head>
<body> <body>
<header class="app-header bg-dark text-white shadow-sm"> <header class="app-header shadow-sm">
<div class="container-fluid h-100 d-flex align-items-center gap-3 px-4"> <nav class="navbar h-100 py-0 bg-light text-dark">
<img class="app-logo" src="./imgs/logo.png" alt="KSP"> <div class="container-fluid h-100 d-flex align-items-center gap-3 px-4 flex-nowrap">
<div class="d-flex flex-column me-auto"> <img class="app-logo" src="./imgs/logo.png" alt="KSP">
<span class="fw-semibold">Wallet Desk — <span id="headerViewTitle">Dashboard</span></span> <div class="d-flex flex-column me-auto">
<small id="shellStatus" class="text-white-50 app-shell-status">Initialisation du shell…</small> <span class="fw-semibold text-primary">Wallet Desk — <span id="headerViewTitle" class="text-body">Dashboard</span></span>
<small id="shellStatus" class="text-body-secondary app-shell-status">Initialisation du shell…</small>
</div>
<button class="btn btn-outline-secondary btn-sm" type="button" data-shell-action="deselect" disabled>
<i class="fa-solid fa-xmark me-2" aria-hidden="true"></i>Deselect
</button>
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="lock" disabled>
<i class="fa-solid fa-lock me-2" aria-hidden="true"></i>Lock
</button>
</div> </div>
<button class="btn btn-outline-light btn-sm" type="button" data-shell-action="deselect" disabled> </nav>
<i class="fa-solid fa-xmark me-2" aria-hidden="true"></i>Deselect
</button>
<button class="btn btn-outline-light btn-sm" type="button" data-shell-action="lock" disabled>
<i class="fa-solid fa-lock me-2" aria-hidden="true"></i>Lock
</button>
</div>
</header> </header>
<main class="app-main container-fluid p-0"> <main class="app-main container-fluid p-0">
<div class="row g-0 h-100"> <div class="row g-0 h-100">
@@ -42,7 +46,7 @@
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1> <h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p> <p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div> </div>
<span class="badge text-bg-secondary">0.2.6-pre.013</span> <span class="badge text-bg-secondary">0.2.6-pre.014</span>
</div> </div>
<section data-view-panel="dashboard"> <section data-view-panel="dashboard">
<div class="row g-3"> <div class="row g-3">

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/splash.scss // file: crates/ksp-app-wallet-desk/frontend/sass/splash.scss
// version: 1 // version: 2
@font-face { @font-face {
font-family: "Dos Amazigh"; font-family: "Dos Amazigh";
@@ -9,13 +9,23 @@
font-display: swap; font-display: swap;
} }
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body { body {
display: flex; width: 100%;
width: 100vw; height: 100%;
height: 100vh;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
}
body {
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
@@ -25,8 +35,12 @@ body {
#splash-container { #splash-container {
position: relative; position: relative;
width: 960px; width: 960px;
max-width: 100%;
height: 637px; height: 637px;
max-height: 100%;
overflow: hidden;
opacity: 0; opacity: 0;
will-change: opacity;
} }
#splash-image { #splash-image {
@@ -35,6 +49,11 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; z-index: 1;
object-fit: contain;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
} }
#app-name { #app-name {
@@ -49,17 +68,70 @@ body {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
} }
#splash-status { #debug-info {
position: absolute; position: absolute;
right: 1rem; top: 0;
bottom: 1rem; left: 0;
left: 1rem;
z-index: 3; z-index: 3;
padding: 0.55rem 0.75rem; width: 55%;
max-height: 30%;
overflow-y: auto;
padding: 8px;
color: #fff; color: #fff;
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.18);
font-family: monospace; font-family: monospace;
font-size: 0.82rem; font-size: 12px;
scrollbar-width: none;
}
#messages-container {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 3;
max-height: 30%;
overflow-y: auto;
padding: 10px 10px 36px;
background: rgba(0, 0, 0, 0.18);
font-family: Arial, sans-serif;
font-size: 14px;
scrollbar-width: none;
}
#debug-info::-webkit-scrollbar,
#messages-container::-webkit-scrollbar {
display: none;
}
.splash-message {
margin-bottom: 8px;
padding: 6px 10px;
border-radius: 4px;
color: #fff;
font-weight: bold;
animation: splashMessageFadeIn 0.3s ease-in-out;
}
.splash-message.info { background: rgba(33, 150, 243, 0.22); }
.splash-message.warning { background: rgba(255, 152, 0, 0.22); }
.splash-message.error { background: rgba(244, 67, 54, 0.22); }
.splash-message.success { background: rgba(76, 175, 80, 0.22); }
@keyframes splashMessageFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }

View File

@@ -1,16 +1,23 @@
<!doctype html> <!-- file: crates/ksp-app-wallet-desk/frontend/splash.html -->
<!-- version: 1 -->
<!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head>
<meta charset="UTF-8"> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="utf-8">
<title>Chargement — KSP Wallet Desk</title> <meta content="width=device-width, initial-scale=1.0" name="viewport">
</head> <link rel="stylesheet" href="sass/splash.scss">
<body> <title>KSP Wallet Desk — Chargement</title>
<div id="splash-container"> </head>
<img id="splash-image" src="./imgs/splash.png" alt="">
<div id="app-name">Wallet Desk</div> <body>
<div id="splash-status">Initialisation…</div> <div id="splash-container" style="opacity: 0;">
</div> <img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Wallet Desk">
<script type="module" src="./ts/splash.ts"></script> <div id="app-name">Wallet Desk</div>
</body> <div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div>
</div>
<script type="module" src="ts/splash.ts" defer></script>
</body>
</html> </html>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 14 // version: 15
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5"; import DataTable from "datatables.net-bs5";
@@ -190,7 +190,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.", zeroRecords: "Aucun wallet correspondant.",
}, },
}); });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.013-integration-compliance" }); frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.014-desktop-polish" });
} }
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void { function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {

View File

@@ -1,12 +1,11 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/splash.ts // file: crates/ksp-app-wallet-desk/frontend/ts/splash.ts
// version: 1 // version: 2
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import type { SplashOrderDto } from "./bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts"; import type { SplashOrderDto } from "./bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts";
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log"; import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
import "../sass/splash.scss";
installFrontendConsoleBridge("splash"); installFrontendConsoleBridge("splash");
@@ -50,21 +49,46 @@ async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpaci
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs }); frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
} }
function replaceStatus(message: string | null): void { function scrollToLatest(element: HTMLElement): void {
if (!message) { element.scrollTop = element.scrollHeight;
}
function addMessage(message: string, status: string | null): void {
const container = document.querySelector<HTMLElement>("#messages-container");
if (!container) {
return; return;
} }
const status = document.querySelector<HTMLElement>("#splash-status"); const line = document.createElement("div");
if (status) { const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
status.textContent = message; line.className = `splash-message ${safeStatus}`;
frontendTrace("splash", "Splash status replaced", { message }); line.textContent = message;
container.appendChild(line);
scrollToLatest(container);
}
function addDebugMessage(message: string): void {
const container = document.querySelector<HTMLElement>("#debug-info");
if (!container) {
return;
} }
container.hidden = false;
const line = document.createElement("div");
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
container.appendChild(line);
scrollToLatest(container);
} }
async function handleSplashOrder(order: SplashOrderDto): Promise<void> { async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
frontendTrace("splash", "Splash order received", { action: order.action }); frontendTrace("splash", "Splash order received", { action: order.action });
const container = document.querySelector<HTMLElement>("#splash-container"); const container = document.querySelector<HTMLElement>("#splash-container");
replaceStatus(order.message); if (order.action === "add_message" && order.message) {
addMessage(order.message, order.status);
return;
}
if (order.action === "add_debug" && order.message) {
addDebugMessage(order.message);
return;
}
if (!container) { if (!container) {
return; return;
} }
@@ -93,7 +117,7 @@ async function initializeSplash(): Promise<void> {
try { try {
await invokeKsp<void>("splash", "splash_frontend_ready"); await invokeKsp<void>("splash", "splash_frontend_ready");
} catch { } catch {
replaceStatus("Le lifecycle du splash n'a pas pu démarrer."); addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
if (container) { if (container) {
container.style.opacity = "1"; container.style.opacity = "1";
container.style.willChange = "auto"; container.style.willChange = "auto";

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs // file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 17 // version: 18
//! Shared backend state owned by the Wallet Desk Tauri application. //! Shared backend state owned by the Wallet Desk Tauri application.
@@ -57,7 +57,7 @@ impl AppState {
crate::SplashSettings::fallback() 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"); 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_in_ms = splash_settings.fade_in_ms(), fade_in_source = splash_settings.fade_in_source(), fade_out_ms = splash_settings.fade_out_ms(), fade_out_source = splash_settings.fade_out_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Wallet Desk splash timings");
return std::result::Result::Ok(Self { return std::result::Result::Ok(Self {
config_management, config_management,
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState { logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
@@ -126,7 +126,7 @@ impl AppState {
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(), effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
shell_phase: "pre.013-integration-compliance".to_owned(), shell_phase: "pre.014-desktop-polish".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(), startup_diagnostic: runtime.startup_diagnostic.clone(),
transport_available_endpoint_count, transport_available_endpoint_count,
transport_clusters: self.transport_runtime.clusters(), transport_clusters: self.transport_runtime.clusters(),

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-app-wallet-desk/src/splash.rs // file: crates/ksp-app-wallet-desk/src/splash.rs
// version: 1 // version: 2
//! Common splash settings and frontend event contracts for Wallet Desk. //! Common splash settings and frontend event contracts for Wallet Desk.
use ts_rs::TS; // rust-rules: trait-import use ts_rs::TS; // rust-rules: trait-import
const DEFAULT_SPLASH_FADE_MS: u32 = 300; const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200; const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS"; const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS"; const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const MAX_SPLASH_FADE_MS: u32 = 10_000; const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
@@ -15,10 +17,12 @@ const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle. /// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SplashSettings { pub(crate) struct SplashSettings {
fade_in_ms: u32,
minimum_ms: u64, minimum_ms: u64,
fade_ms: u32, fade_out_ms: u32,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
minimum_source: ksp_config_lib::ConfigEnvironmentSource, minimum_source: ksp_config_lib::ConfigEnvironmentSource,
fade_source: ksp_config_lib::ConfigEnvironmentSource, fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
} }
impl SplashSettings { impl SplashSettings {
@@ -29,13 +33,23 @@ impl SplashSettings {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
let fade_in = match fade_in {
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 = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
let minimum = match minimum { let minimum = match minimum {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
let fade = match fade { let fade_out = match fade_out {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
let fade_in_ms = match fade_in_ms {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
@@ -44,35 +58,56 @@ impl SplashSettings {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
let fade_ms = match fade_ms { let fade_out_ms = match fade_out_ms {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), 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() }); return std::result::Result::Ok(Self {
fade_in_ms,
minimum_ms,
fade_out_ms,
fade_in_source: fade_in.source(),
minimum_source: minimum.source(),
fade_out_source: fade_out.source(),
});
} }
/// Returns safe in-memory timings used when the managed environment cannot be resolved. /// Returns safe in-memory timings used when the managed environment cannot be resolved.
#[must_use] #[must_use]
pub(crate) const fn fallback() -> Self { pub(crate) const fn fallback() -> Self {
return Self { return Self {
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS, minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
fade_ms: DEFAULT_SPLASH_FADE_MS, fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
fade_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
}; };
} }
/// Returns the minimum visible duration after splash frontend readiness. /// Returns the configured fade-in duration.
#[must_use]
pub(crate) const fn fade_in_ms(self) -> u32 {
return self.fade_in_ms;
}
/// Returns the minimum fully-visible splash duration.
#[must_use] #[must_use]
pub(crate) const fn minimum_ms(self) -> u64 { pub(crate) const fn minimum_ms(self) -> u64 {
return self.minimum_ms; return self.minimum_ms;
} }
/// Returns the fade duration used for both fade-in and fade-out. /// Returns the configured fade-out duration.
#[must_use] #[must_use]
pub(crate) const fn fade_ms(self) -> u32 { pub(crate) const fn fade_out_ms(self) -> u32 {
return self.fade_ms; return self.fade_out_ms;
}
/// Returns the safe provenance code for the fade-in duration.
#[must_use]
pub(crate) const fn fade_in_source(self) -> &'static str {
return environment_source_code(self.fade_in_source);
} }
/// Returns the safe provenance code for the minimum duration. /// Returns the safe provenance code for the minimum duration.
@@ -81,16 +116,16 @@ impl SplashSettings {
return environment_source_code(self.minimum_source); return environment_source_code(self.minimum_source);
} }
/// Returns the safe provenance code for the fade duration. /// Returns the safe provenance code for the fade-out duration.
#[must_use] #[must_use]
pub(crate) const fn fade_source(self) -> &'static str { pub(crate) const fn fade_out_source(self) -> &'static str {
return environment_source_code(self.fade_source); return environment_source_code(self.fade_out_source);
} }
/// Returns the minimum backend lifecycle duration from readiness until main activation. /// Returns the expected additive backend lifecycle from readiness through fade-out.
#[must_use] #[must_use]
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 { pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
return self.minimum_ms + u64::from(self.fade_ms); return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
} }
} }
@@ -103,6 +138,8 @@ pub(crate) struct SplashOrderDto {
pub(crate) action: String, pub(crate) action: String,
/// Optional status text. /// Optional status text.
pub(crate) message: std::option::Option<String>, pub(crate) message: std::option::Option<String>,
/// Optional message severity used by the bottom message feed.
pub(crate) status: std::option::Option<String>,
/// Optional animation duration in milliseconds. /// Optional animation duration in milliseconds.
pub(crate) duration_ms: std::option::Option<u32>, pub(crate) duration_ms: std::option::Option<u32>,
} }
@@ -110,8 +147,13 @@ pub(crate) struct SplashOrderDto {
impl SplashOrderDto { impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value. /// Creates a new `SplashOrderDto` value.
#[must_use] #[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self { pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms }; return Self {
action: action.to_owned(),
message: message.map(std::string::ToString::to_string),
status: status.map(std::string::ToString::to_string),
duration_ms,
};
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/tw_splash.rs // file: crates/ksp-app-wallet-desk/src/tw_splash.rs
// version: 1 // version: 2
//! Tauri-window lifecycle for the Wallet Desk splash window. //! Tauri-window lifecycle for the Wallet Desk splash window.
@@ -41,27 +41,64 @@ pub(crate) async fn splash_frontend_ready_service(
} }
let settings = state.splash_settings(); let settings = state.splash_settings();
let lifecycle_started = std::time::Instant::now(); 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"); ksp_logging_lib::debug!(
let fade_in = emit_order( target: crate::TRACING_TARGET,
&invoking_window, domain = crate::TRACING_DOMAIN_WINDOWS,
crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Wallet Desk..."), std::option::Option::Some(settings.fade_ms())), window_label = WINDOW_LABEL_SPLASH,
fade_in_ms = settings.fade_in_ms(),
fade_in_source = settings.fade_in_source(),
minimum_ms = settings.minimum_ms(),
minimum_source = settings.minimum_source(),
fade_out_ms = settings.fade_out_ms(),
fade_out_source = settings.fade_out_source(),
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
"splash frontend readiness accepted; starting splash to main lifecycle"
); );
if let std::result::Result::Err(error) = fade_in { let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let minimum_wait_started = std::time::Instant::now(); let emitted = emit_message(&invoking_window, "Initialisation de Wallet Desk...", "info");
tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await; if let std::result::Result::Err(error) = emitted {
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); return std::result::Result::Err(error);
} }
let fade_wait_started = std::time::Instant::now(); let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await; if let std::result::Result::Err(error) = emitted {
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"); return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Wallet et Transport prêts.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
let show_main = crate::show_main_window(&app); let show_main = crate::show_main_window(&app);
if let std::result::Result::Err(error) = show_main { if let std::result::Result::Err(error) = show_main {
return std::result::Result::Err(error); return std::result::Result::Err(error);
@@ -74,10 +111,50 @@ pub(crate) async fn splash_frontend_ready_service(
.with_source(error), .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"); 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(()); return std::result::Result::Ok(());
} }
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
return emit_order(
window,
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
);
}
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
if !cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
return emit_order(
window,
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
);
}
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
let wait_started = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = window.label(),
phase,
configured_wait_ms = duration_ms,
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash lifecycle wait completed"
);
return;
}
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> { fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
let action = order.action.clone(); let action = order.action.clone();
let emitted = window.emit(SPLASH_EVENT_NAME, order); let emitted = window.emit(SPLASH_EVENT_NAME, order);

View File

@@ -1,12 +1,18 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk", "productName": "KSP Wallet Desk",
"version": "0.2.6-pre.13.fix.2", "version": "0.2.6-pre.14",
"identifier": "com.sasedev.ksp-app-wallet-desk", "identifier": "com.sasedev.ksp-app-wallet-desk",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1432", "devUrl": "http://localhost:1432",
"beforeBuildCommand": "npm run build", "beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist" "frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist"
}, },
"app": { "app": {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 14 // version: 15
//! Desktop build, shell and Config-status contract audits for Wallet Desk. //! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -31,9 +31,13 @@ fn tauri_shell_uses_reserved_wallet_desk_ports_and_template_windows() {
let root = app_root(); let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path()); 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/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/beforeDevCommand/script").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("/build/beforeDevCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
assert_eq!(tauri.pointer("/build/beforeBuildCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
assert_eq!(tauri.pointer("/build/beforeBuildCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
assert_eq!(tauri.pointer("/app/security/csp"), std::option::Option::Some(&serde_json::Value::Null)); assert_eq!(tauri.pointer("/app/security/csp"), std::option::Option::Some(&serde_json::Value::Null));
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("name = \"ksp-app-wallet-desk\""));
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array); let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
assert!(windows.is_some()); assert!(windows.is_some());
let windows = match windows { let windows = match windows {
@@ -347,3 +351,24 @@ fn pre_013_integration_compliance_and_devnet_smoke_are_durable_release_gates() {
assert!(smoke.contains("#[ignore")); assert!(smoke.contains("#[ignore"));
assert!(validation.contains("Config -> Wallet -> Transport -> Devnet getBalance")); assert!(validation.contains("Config -> Wallet -> Transport -> Devnet getBalance"));
} }
#[test]
fn pre_014_template_has_normalized_html_headers_light_header_and_kbot_style_splash_contract() {
let root = app_root();
let main = read_text(root.join("frontend/main.html").as_path());
let splash = read_text(root.join("frontend/splash.html").as_path());
let splash_style = read_text(root.join("frontend/sass/splash.scss").as_path());
let splash_ts = read_text(root.join("frontend/ts/splash.ts").as_path());
assert!(main.starts_with("<!-- file: crates/ksp-app-wallet-desk/frontend/main.html -->"));
assert!(main.contains("navbar h-100 py-0 bg-light text-dark"));
assert!(main.contains("app-sidebar"));
assert!(splash.starts_with("<!-- file: crates/ksp-app-wallet-desk/frontend/splash.html -->"));
assert!(splash.contains("id=\"debug-info\""));
assert!(splash.contains("id=\"messages-container\""));
assert!(splash.contains("sass/splash.scss"));
assert!(splash_style.contains("overflow: hidden"));
assert!(splash_style.contains("user-select: none"));
assert!(splash_ts.contains("add_debug"));
assert!(splash_ts.contains("add_message"));
assert!(!splash_ts.contains("../sass/splash.scss"));
}

View File

@@ -1,22 +1,25 @@
// file: crates/ksp-app-wallet-desk/unit_tests/splash.rs // file: crates/ksp-app-wallet-desk/unit_tests/splash.rs
// version: 1 // version: 2
#[test] #[test]
fn fallback_splash_settings_are_short_and_ordered() { fn fallback_splash_settings_are_short_and_ordered() {
let settings = crate::SplashSettings::fallback(); let settings = crate::SplashSettings::fallback();
assert_eq!(settings.fade_in_ms(), 300);
assert_eq!(settings.minimum_ms(), 1200); assert_eq!(settings.minimum_ms(), 1200);
assert_eq!(settings.fade_ms(), 300); assert_eq!(settings.fade_out_ms(), 300);
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
assert_eq!(settings.fade_in_source(), "fallback");
assert_eq!(settings.minimum_source(), "fallback"); assert_eq!(settings.minimum_source(), "fallback");
assert_eq!(settings.fade_source(), "fallback"); assert_eq!(settings.fade_out_source(), "fallback");
assert_eq!(settings.expected_backend_lifecycle_ms(), 1500);
assert!(settings.minimum_ms() >= u64::from(settings.fade_ms()));
} }
#[test] #[test]
fn splash_order_contract_keeps_action_message_and_duration() { fn splash_order_contract_keeps_action_message_status_and_duration() {
let order = crate::SplashOrderDto::new("fade_out", std::option::Option::Some("done"), std::option::Option::Some(300)); let order =
assert_eq!(order.action, "fade_out"); crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
assert_eq!(order.message.as_deref(), std::option::Option::Some("done")); assert_eq!(order.action, "add_message");
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
assert_eq!(order.duration_ms, std::option::Option::Some(300)); assert_eq!(order.duration_ms, std::option::Option::Some(300));
} }
@@ -24,14 +27,10 @@ fn splash_order_contract_keeps_action_message_and_duration() {
fn splash_duration_parser_rejects_invalid_or_unbounded_values() { fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000); 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_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); let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
assert!(malformed.is_err()); assert!(malformed.is_err());
assert!(oversized_minimum.is_err()); assert!(oversized_minimum.is_err());
assert!(oversized_fade.is_err()); assert!(oversized_fade_in.is_err());
if let std::result::Result::Err(error) = oversized_minimum { assert!(oversized_fade_out.is_err());
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"));
}
} }

385
deltas/0.2.6/pre.014.md Normal file
View File

@@ -0,0 +1,385 @@
<!-- file: deltas/0.2.6/pre.014.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.014` — polish desktop partagé, splash kbot3 et lancement Tauri multi-app
## Base requise
```text
0.2.6-pre.013-fix.002 appliquée
workspace.package.version = 0.2.6-pre.13.fix.2
```
La validation opérateur de la base est intégralement verte :
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py clean
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
ksp-logging-lib ownership 2/2 OK
Wallet Desk release_compliance 4/4 OK
cargo test -p ksp-app-wallet-desk OK
cargo test --workspace OK
smoke Wallet Desk Devnet de pre.013 déjà validé sur fix.001
```
## Type de livraison
```text
ksp-general-0.2.6-pre.014.zip
```
Le signal technique devient :
```text
workspace.package.version = 0.2.6-pre.14
commit = v0.2.6-pre.014
```
`ksp-app-config-desk/package.json`, `ksp-app-config-desk/tauri.conf.json`, `ksp-app-wallet-desk/package.json` et `ksp-app-wallet-desk/tauri.conf.json` suivent `0.2.6-pre.14`.
Aucun tag prerelease.
## Objectifs
Cette tranche ne change aucun contrat Wallet métier. Elle ferme les défauts de gabarit desktop remontés avant la documentation finale :
```text
en-têtes HTML Khadhroony normalisés
splash sans scrollbars fenêtre
splash avec fade-in / minimum / fade-out distincts
messages généraux défilants en bas
messages debug défilants en haut
nom/titre central du splash non sélectionnable
navigation Config Desk déplacée du header vers une sidebar
palette Wallet Desk réalignée sur la référence claire Config Desk
lancement Tauri multi-app corrigé et rendu durable
prompt 0.2.7 intercalé pour le conteneur binaire .kspwallet
```
## En-têtes HTML
Les quatre fichiers HTML des deux applications possèdent désormais les deux premières lignes normalisées :
```html
<!-- file: ... -->
<!-- version: N -->
```
Les Wallet HTML, qui n'en possédaient pas, démarrent à `version: 1`.
## Splash partagé
Le comportement reprend les fonctionnalités génériques éprouvées dans kbot3 sans reprendre ses constantes codées en dur.
Les deux Desks utilisent :
```text
KSP_DESK_SPLASH_FADE_IN_MS
KSP_DESK_SPLASH_MINIMUM_MS
KSP_DESK_SPLASH_FADE_OUT_MS
```
Fallbacks :
```text
fade-in = 300 ms
minimum = 1200 ms pleinement visible
fade-out = 300 ms
```
Bornes :
```text
fade-in / fade-out <= 10 000 ms
minimum <= 60 000 ms
```
La provenance continue d'être résolue uniquement par `ksp-config-lib` : process > `.env` > fallback.
Le lifecycle backend devient additif et explicite :
```text
frontend ready
-> fade-in + attente fade-in
-> messages init + minimum pleinement visible
-> fade-out + attente fade-out
-> show/focus main
-> destroy splash
```
`expected_backend_lifecycle_ms` vaut donc :
```text
fade_in + minimum + fade_out
```
## Messages splash
Comme dans kbot3 :
- le bas du splash contient un feed de messages généraux avec statuts `info/warning/error/success` ;
- le haut contient un feed diagnostic uniquement lorsque Rust est compilé avec `debug_assertions` ;
- les deux zones auto-scrollent vers le dernier élément ;
- leurs scrollbars internes sont masquées visuellement.
Le frontend ne reçoit aucun secret dans ces messages.
## Scrollbars et sélection
Wallet Desk chargeait auparavant `splash.scss` depuis le module TypeScript, alors que Config Desk le chargeait directement dans `<head>`. Le style pouvait donc arriver après le premier rendu WebView et laisser apparaître momentanément les scrollbars natives.
Les deux applications chargent maintenant `sass/splash.scss` directement dans le HTML avant JavaScript.
Le CSS fixe également :
```text
html/body width/height 100%
overflow hidden dès le premier rendu
box-sizing border-box
container max-width/max-height 100%
overflow hidden sur le container
scrolling borné aux feeds internes
```
Le logo/image et le nom central appliquent `user-select: none`, `-webkit-user-select: none`, `-webkit-user-drag: none` et `pointer-events: none`.
## Harmonisation du main
La navigation Config Desk n'est plus placée horizontalement dans le header. Elle reprend le pattern apprécié de Wallet Desk :
```text
header : identité + vue active
main : sidebar pills verticale + contenu scrollable
footer : inchangé
```
Wallet Desk conserve sa sidebar mais son header revient sur la palette claire de référence de Config Desk au lieu du header sombre divergent.
Les `data-view` historiques de Config Desk restent inchangés ; aucune logique d'activation TypeScript n'est réécrite.
## Correction Tauri multi-app
Le défaut suivant a été reproduit conceptuellement à partir de la configuration actuelle :
```text
commande lancée depuis la racine avec -c Config Desk
-> Vite Config Desk démarre sur 1430
-> backend Tauri découvert peut être Wallet Desk
```
La cause est que Tauri v2 traite `-c/--config` comme une configuration **fusionnée avec la configuration par défaut déjà résolue** ; cette option n'est pas un sélecteur de crate/projet.
Le workflow KSP devient donc crate-local :
```bash
(cd crates/ksp-app-config-desk && cargo tauri dev)
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
et pour le build :
```bash
(cd crates/<app> && cargo tauri build)
```
Les hooks Tauri sont aussi explicités sous forme objet :
```json
{
"script": "npm run dev",
"cwd": "."
}
```
et équivalent pour `npm run build`.
Les canaris vérifient maintenant le port, le hook/cwd et le nom du package Rust propre à l'application.
`KSP-APP-026`, `KSP-APP-029` et `KSP-APP-034` sont corrigées. Le lancement racine avec `-c crates/<app>/tauri.conf.json` est désormais interdit comme méthode de sélection multi-app.
Config Desk abandonne aussi son script standalone `check` afin d'aligner les deux applications sur la règle actuelle : l'opérateur n'exécute directement npm que pour installer/mettre à jour les dépendances ; Tauri possède les scripts dev/build.
## Intercalation de `0.2.7` — conteneur binaire `.kspwallet`
La question du JSON V1 lisible dans un éditeur est dissociée de `0.2.6` et du futur WebSocket.
Décision de cadrage :
```text
Base64 seul NON
conteneur binaire versionné à auditer/implémenter en 0.2.7
compatibilité lecture JSON V1 obligatoire
Wallet Desk modifié fonctionnellement NON attendu
```
Base64 seul reste textuel, trivialement réversible, sans gain cryptographique et avec surcharge de taille.
`0.2.7` doit séparer :
```text
container_version
!=
format_version logique/cryptographique
```
Ainsi V1 conserve ses semantics VIEW/OWNER et un futur `format_version >= 2` reste disponible pour de vraies évolutions d'autorisation/crypto.
Le prompt ajouté est :
```text
prompts/012-V0_2_7_START_PROMPT.md
```
WebSocket et les releases suivantes sont décalés d'un numéro dans le roadmap/inventaire/séquence fonctionnelle globale.
## Futur second facteur
`docs/IDEAS.md` précise :
- `ksp-wallet-lib` restera propriétaire du format, des challenges et de la vérification ;
- Wallet Desk devra évoluer lorsqu'un facteur est interactif : OTP, enrollment/recovery, hardware/WebAuthn, consentement externe ;
- un seed TOTP stocké uniquement dans le même fichier ne doit pas être assimilé automatiquement à un second facteur indépendant contre un attaquant possédant ce fichier.
Aucun V2/OTP/2FA n'est implémenté dans cette tranche.
## Documentation globale
Exception justifiée à la règle « ROADMAP seulement au début/à la fin » : l'insertion d'une nouvelle release `0.2.7` change réellement la séquence globale. Sont donc mis à jour :
```text
ROADMAP.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/architecture/004-COMPONENT_INVENTORY.md
```
`CHANGELOG.md` reste inchangé et sera traité à la clôture.
## Fichiers ajoutés
```text
deltas/0.2.6/pre.014.md
prompts/012-V0_2_7_START_PROMPT.md
```
## Fichiers modifiés
```text
.env.example
Cargo.toml
ROADMAP.md
crates/ksp-app-config-desk/README.md
crates/ksp-app-config-desk/USAGE.md
crates/ksp-app-config-desk/frontend/main.html
crates/ksp-app-config-desk/frontend/sass/_app.scss
crates/ksp-app-config-desk/frontend/sass/splash.scss
crates/ksp-app-config-desk/frontend/splash.html
crates/ksp-app-config-desk/frontend/ts/splash.ts
crates/ksp-app-config-desk/package.json
crates/ksp-app-config-desk/src/app_state.rs
crates/ksp-app-config-desk/src/splash.rs
crates/ksp-app-config-desk/src/tw_splash.rs
crates/ksp-app-config-desk/tauri.conf.json
crates/ksp-app-config-desk/tests/desktop_contract.rs
crates/ksp-app-config-desk/unit_tests/splash.rs
crates/ksp-app-wallet-desk/frontend/main.html
crates/ksp-app-wallet-desk/frontend/sass/splash.scss
crates/ksp-app-wallet-desk/frontend/splash.html
crates/ksp-app-wallet-desk/frontend/ts/main.ts
crates/ksp-app-wallet-desk/frontend/ts/splash.ts
crates/ksp-app-wallet-desk/package.json
crates/ksp-app-wallet-desk/src/app_state.rs
crates/ksp-app-wallet-desk/src/splash.rs
crates/ksp-app-wallet-desk/src/tw_splash.rs
crates/ksp-app-wallet-desk/tauri.conf.json
crates/ksp-app-wallet-desk/tests/desktop_contract.rs
crates/ksp-app-wallet-desk/unit_tests/splash.rs
docs/IDEAS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
docs/rules/RULES_KSP.md
prompts/000-README.md
prompts/011-V0_2_6_START_PROMPT.md
```
## Fichiers supprimés
```text
aucun
```
## Contrôles assistant
```text
General Rust rule audit clean
Rust export completeness audit 0 candidate
KSP workspace Rust rule audit clean
TOML / JSON parse clean
HTML parse + IDs uniques
splash TypeScript aucun diagnostic syntaxique local
(imports non résolus attendus sans node_modules/bindings)
CHANGELOG inchangé
```
Cargo n'est pas disponible dans l'environnement de génération ; compilation, Clippy et tests restent autoritatifs côté opérateur.
## Validation opérateur requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-config-desk
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
Validation visuelle Config Desk :
```bash
KSP_DESK_SPLASH_FADE_IN_MS=1500 \
KSP_DESK_SPLASH_MINIMUM_MS=6000 \
KSP_DESK_SPLASH_FADE_OUT_MS=1500 \
bash -lc 'cd crates/ksp-app-config-desk && cargo tauri dev'
```
Vérifier :
```text
backend = ksp-app-config-desk
Vite = 1430 / HMR = 1431
aucune scrollbar splash fenêtre
fade-in puis minimum puis fade-out observables
messages généraux en bas
diagnostics en haut en debug
titre central non sélectionnable
navigation verticale dans le main
```
Validation visuelle Wallet Desk :
```bash
KSP_DESK_SPLASH_FADE_IN_MS=1500 \
KSP_DESK_SPLASH_MINIMUM_MS=6000 \
KSP_DESK_SPLASH_FADE_OUT_MS=1500 \
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre014 \
bash -lc 'cd crates/ksp-app-wallet-desk && cargo tauri dev'
```
Vérifier :
```text
backend = ksp-app-wallet-desk
Vite = 1432 / HMR = 1433
aucune scrollbar splash fenêtre
mêmes trois phases/messages
titre central non sélectionnable
header clair cohérent avec Config Desk
fonctionnalités Wallet pre.013 non régressées
```
**Ne pas exécuter `cargo tauri build` en `pre.014`.** Le build final reste réservé à `pre.015` et doit rester l'absolue dernière opération de validation de `0.2.6`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/IDEAS.md --> <!-- file: docs/IDEAS.md -->
<!-- version: 19 --> <!-- version: 20 -->
# Idées à explorer # Idées à explorer
@@ -202,6 +202,14 @@ Formats/cibles à inventorier et prioriser selon usage réel :
Chaque format doit être étudié côté sécurité, round-trip, secret/public, dépendances et compatibilité avant engagement. Chaque format doit être étudié côté sécurité, round-trip, secret/public, dépendances et compatibilité avant engagement.
### Conteneur binaire `.kspwallet` et formats logiques futurs
**Status :** Retenu pour audit en `0.2.7` / facteurs futurs à explorer
Le JSON V1 actuel est un format dinterop lisible. Base64 seul napporte aucune sécurité et resterait un texte trivialement décodable avec environ un tiers de surcharge. `0.2.7` doit donc étudier un **conteneur de persistence binaire versionné** séparé du `format_version` logique/cryptographique : magic/framing explicite, lecture rétrocompatible du JSON V1 historique, écriture binaire par défaut après validation, migration explicite et test vectors. Le choix du codec/framing exact est audité avant engagement ; il ne doit pas casser les transcripts/AAD, VIEW/OWNER, keypair ou import/export.
Un futur `format_version >= 2` pourra introduire dautres modèles dautorisation, notamment password + facteur supplémentaire. `ksp-wallet-lib` restera propriétaire du format, des challenges et de la vérification, mais toute interaction réelle (OTP, enrollment/recovery, hardware/WebAuthn, validation distante) exigera une évolution de Wallet Desk ou du client concerné. Un seed TOTP stocké uniquement dans le même fichier que le wallet ne doit pas être présenté automatiquement comme un second facteur indépendant contre un attaquant possédant ce fichier.
## Pipelines ## Pipelines
### Pas de pipeline monolithique ### Pas de pipeline monolithique

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md --> <!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
<!-- version: 15 --> <!-- version: 16 -->
# Inventaire initial des composants KSP # Inventaire initial des composants KSP
@@ -26,13 +26,14 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
| On-chain HTTP | `ksp-onchain-transport-lib` | lib | Stable | `0.2.1``0.2.4` | HTTP standard complet : 52/52 current + 14/14 historical | | On-chain HTTP | `ksp-onchain-transport-lib` | lib | Stable | `0.2.1``0.2.4` | HTTP standard complet : 52/52 current + 14/14 historical |
| Wallet | `ksp-wallet-lib` | lib | Stable | `0.2.5` | `.kspwallet`, VIEW/OWNER, secrets, signature, import/export | | Wallet | `ksp-wallet-lib` | lib | Stable | `0.2.5` | `.kspwallet`, VIEW/OWNER, secrets, signature, import/export |
| Wallet Desk | `ksp-app-wallet-desk` | app | Retenu | `0.2.6` | Wallet + Config composite + HTTP/balance | | Wallet Desk | `ksp-app-wallet-desk` | app | Retenu | `0.2.6` | Wallet + Config composite + HTTP/balance |
| Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.7` | WebSocket Solana complet, sessions/subscriptions | | Wallet persistence | `ksp-wallet-lib` | lib | Retenu | `0.2.7` | conteneur binaire rétrocompatible autour du payload V1 |
| Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | LaserStream WebSocket comme extension du moteur standard | | Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | WebSocket Solana complet, sessions/subscriptions |
| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.9` | client gRPC standard/provider-neutral | | Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.9` | LaserStream WebSocket comme extension du moteur standard |
| Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.10` | première abstraction/provider de prix SOL/USD, SOL/EUR | | Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.10` | client gRPC standard/provider-neutral |
| Price Desk | nom à fixer | app | Retenu | `0.2.11` | visualisation/validation des prix + intégration Wallet Desk | | Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.11` | première abstraction/provider de prix SOL/USD, SOL/EUR |
| Wire | `ksp-interface-lib` | lib | Retenu | `0.2.12` | façade wire officielle + API publique wire | | Price Desk | nom à fixer | app | Retenu | `0.2.12` | visualisation/validation des prix + intégration Wallet Desk |
| Program API | `ksp-program-api` | API | Retenu | `0.2.13` | contrats extensibles Program | | Wire | `ksp-interface-lib` | lib | Retenu | `0.2.13` | façade wire officielle + API publique wire |
| Program API | `ksp-program-api` | API | Retenu | `0.2.14` | contrats extensibles Program |
| Program impl. | `ksp-program-lib` | lib | Retenu | vertical slices ultérieurs | implementations Program officielles | | Program impl. | `ksp-program-lib` | lib | Retenu | vertical slices ultérieurs | implementations Program officielles |
| Program extension | `ksp-program-<name>-lib` | lib externe | À la demande | dès besoin | implementation externe de `ksp-program-api` | | Program extension | `ksp-program-<name>-lib` | lib externe | À la demande | dès besoin | implementation externe de `ksp-program-api` |
| Store API | `ksp-store-api` | API | Retenu | `0.3.1` | contrats persistence backend-agnostic, RAW d'abord | | Store API | `ksp-store-api` | API | Retenu | `0.3.1` | contrats persistence backend-agnostic, RAW d'abord |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md --> <!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 74 --> <!-- version: 75 -->
# Séquence des releases fonctionnelles KSP # Séquence des releases fonctionnelles KSP
@@ -361,13 +361,14 @@ Par défaut :
0.2.4 HTTP Blocks + Economics + compliance complète 0.2.4 HTTP Blocks + Economics + compliance complète
0.2.5 wallet foundation (.kspwallet) 0.2.5 wallet foundation (.kspwallet)
0.2.6 Wallet Desk 0.2.6 Wallet Desk
0.2.7 standard Solana WebSocket 0.2.7 .kspwallet binary persistence container
0.2.8 Helius LaserStream WebSocket 0.2.8 standard Solana WebSocket
0.2.9 Yellowstone gRPC standard foundation 0.2.9 Helius LaserStream WebSocket
0.2.10 off-chain price transport 0.2.10 Yellowstone gRPC standard foundation
0.2.11 price visualization desk + intégration prix dans Wallet Desk 0.2.11 off-chain price transport
0.2.12 interface/wire foundation 0.2.12 price visualization desk + intégration prix dans Wallet Desk
0.2.13 program-api foundation 0.2.13 interface/wire foundation
0.2.14 program-api foundation
``` ```
`0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1``0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2``0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées. `0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1``0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2``0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées.
@@ -448,17 +449,23 @@ rel.001 publication stable 0.2.6
La tranche `pre.014` est réservée aux défauts visuels/templating observés en usage réel, notamment les scrollbars occasionnelles du splashscreen ; son contenu précis sera borné à partir du retour opérateur avant implémentation. `ROADMAP.md` reste synthétique et `CHANGELOG.md` n'est synchronisé qu'à la phase documentaire finale. La tranche `pre.014` est réservée aux défauts visuels/templating observés en usage réel, notamment les scrollbars occasionnelles du splashscreen ; son contenu précis sera borné à partir du retour opérateur avant implémentation. `ROADMAP.md` reste synthétique et `CHANGELOG.md` n'est synchronisé qu'à la phase documentaire finale.
## `0.2.7` — WebSocket Solana standard ## `0.2.7` — `.kspwallet` binary persistence container
Mission : faire évoluer uniquement la couche de persistence de `ksp-wallet-lib` afin quun `.kspwallet` nouvellement écrit ne soit plus un document JSON directement lisible dans un éditeur texte ordinaire, sans prétendre ajouter de sécurité cryptographique par simple obfuscation.
Le gate `pre.001` doit comparer framing binaire custom, codecs binaires stables et éventuelle compression, puis fixer un conteneur explicitement versionné. Base64 seul est exclu comme solution : il reste textuel, trivialement réversible et augmente la taille. Le conteneur et le `format_version` logique/cryptographique sont séparés afin de conserver V1 pour les semantics VIEW/OWNER actuelles et de réserver les futurs formats logiques à de vraies évolutions dautorisation. La lecture rétrocompatible des JSON V1 historiques et les migrations/test vectors sont obligatoires. Wallet Desk ne doit pas être modifié fonctionnellement ; ses tests/smokes sont rejoués comme preuve consommateur.
## `0.2.8` — WebSocket Solana standard
Mission : couvrir la surface WebSocket standard officielle ciblée. Mission : couvrir la surface WebSocket standard officielle ciblée.
Une URL peut avoir plusieurs sessions physiques ; une session peut avoir plusieurs subscriptions. Un pool automatique de sessions est reporté jusqu'à besoin concret. Une URL peut avoir plusieurs sessions physiques ; une session peut avoir plusieurs subscriptions. Un pool automatique de sessions est reporté jusqu'à besoin concret.
## `0.2.8` — Helius LaserStream WebSocket ## `0.2.9` — Helius LaserStream WebSocket
Mission : étendre le moteur WebSocket standard avec les opérations/filtres/capabilities Helius ciblés sans copier le client. Mission : étendre le moteur WebSocket standard avec les opérations/filtres/capabilities Helius ciblés sans copier le client.
## `0.2.9` — Yellowstone gRPC standard ## `0.2.10` — Yellowstone gRPC standard
Mission : introduire un backend Yellowstone standard/provider-neutral. Mission : introduire un backend Yellowstone standard/provider-neutral.
@@ -466,21 +473,21 @@ Le `pre.001` est un gate de sizing : inventorier toute la surface normative cibl
Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices. Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices.
## `0.2.10` / `0.2.11` — Off-chain price + app ## `0.2.11` / `0.2.12` — Off-chain price + app
`0.2.10` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider. `0.2.11` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
`0.2.11` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé. `0.2.12` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé.
Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel. Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel.
## `0.2.12` — Interface foundation ## `0.2.13` — Interface foundation
`ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes. `ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes.
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant. Aucune `ksp-interface-api` séparée n'est retenue pour l'instant.
## `0.2.13` — Program API foundation ## `0.2.14` — Program API foundation
Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program. Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md --> <!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
<!-- version: 26 --> <!-- version: 27 -->
# Plan `0.1.4` — `ksp-app-config-desk` # Plan `0.1.4` — `ksp-app-config-desk`
@@ -233,7 +233,7 @@ Les artefacts frontend construits doivent être séparés des sources. Pour `ksp
Cette valeur est utilisée par `build.frontendDist` dans `tauri.conf.json`. `vite.config.ts` utilise la même destination comme `build.outDir` depuis son introduction en `pre.005`. Elle remplace le `../dist`/`../../dist` historique du gabarit bot3. Cette valeur est utilisée par `build.frontendDist` dans `tauri.conf.json`. `vite.config.ts` utilise la même destination comme `build.outDir` depuis son introduction en `pre.005`. Elle remplace le `../dist`/`../../dist` historique du gabarit bot3.
La gestion npm distingue le runtime frontend du tooling. Les bibliothèques consommées par le bundle appartiennent à `dependencies`; les outils de compilation/développement (`@tauri-apps/cli`, Vite, TypeScript, Sass) et les paquets `@types/*` appartiennent à `devDependencies`. npm est utilisé directement uniquement pour installer ou mettre à jour ces dépendances. Pour la première installation, l'opérateur se place dans `crates/ksp-app-config-desk`, exécute `npm i -D`, puis revient à la racine avec `cd ../../`. Le cycle normal de développement et de build passe ensuite par Tauri depuis la racine du workspace, avec configuration explicite de l'application : `cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json` et `cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json`. Tauri déclenche lui-même `npm run dev` / `npm run build` au travers des hooks configurés. Aucun lockfile npm n'est versionné. La gestion npm distingue le runtime frontend du tooling. Les bibliothèques consommées par le bundle appartiennent à `dependencies`; les outils de compilation/développement (`@tauri-apps/cli`, Vite, TypeScript, Sass) et les paquets `@types/*` appartiennent à `devDependencies`. npm est utilisé directement uniquement pour installer ou mettre à jour ces dépendances. Pour la première installation, l'opérateur se place dans `crates/ksp-app-config-desk` et exécute `npm i -D`. Le cycle normal de développement et de build reste ensuite crate-local : `(cd crates/ksp-app-config-desk && cargo tauri dev)` et `(cd crates/ksp-app-config-desk && cargo tauri build)`. `-c/--config` n'est pas utilisé pour sélectionner l'application car Tauri le traite comme un overlay de configuration. Tauri déclenche lui-même `npm run dev` / `npm run build` au travers des hooks configurés. Aucun lockfile npm n'est versionné.
Chaque application Tauri desk KSP reçoit un couple de ports Vite/HMR propre. La première application réserve : Chaque application Tauri desk KSP reçoit un couple de ports Vite/HMR propre. La première application réserve :
@@ -944,24 +944,25 @@ fade duration
close delay si techniquement distinct close delay si techniquement distinct
``` ```
`pre.008` fige les deux clés communes nécessaires au lifecycle de référence : `0.2.6-pre.014` aligne les deux Desks sur trois clés communes, en reprenant la séparation fonctionnelle du splash kbot3 :
```text ```text
KSP_DESK_SPLASH_FADE_IN_MS=300
KSP_DESK_SPLASH_MINIMUM_MS=1200 KSP_DESK_SPLASH_MINIMUM_MS=1200
KSP_DESK_SPLASH_FADE_MS=300 KSP_DESK_SPLASH_FADE_OUT_MS=300
``` ```
Elles sont résolues uniquement via `ConfigEnvironment` avec priorité process > `.env` > fallback. `KSP_DESK_SPLASH_MINIMUM_MS` est bornée à 60 000 ms et `KSP_DESK_SPLASH_FADE_MS` à 10 000 ms. Une valeur invalide ne doit pas rendre Config Desk inutilisable : lapplication conserve les timings de fallback en mémoire, journalise seulement domaine/code de lerreur et laisse la future surface `.env` permettre la réparation. Elles sont résolues uniquement via `ConfigEnvironment` avec priorité process > `.env` > fallback. `KSP_DESK_SPLASH_MINIMUM_MS` est bornée à 60 000 ms et chaque fade à 10 000 ms. Une valeur invalide ne rend pas Config Desk inutilisable : lapplication conserve les timings de fallback en mémoire et journalise seulement domaine/code de lerreur.
Le frontend ne reçoit que la durée danimation réellement nécessaire dans les ordres du splash; la durée minimale reste backend. La référence temporelle est la readiness frontend : Rust émet `fade_in`, attend `minimum`, émet `fade_out`, attend `fade`, puis affiche/focalise `main` et détruit `splash`. Aucun close delay distinct nest nécessaire. Avec `minimum=12000` et `fade=3000`, le lifecycle backend attendu est donc proche de `15000 ms`, et non des trois délais indépendants historiques de bot3. `tw_splash` journalise en `debug` la provenance des valeurs, les attentes configurées et réellement observées ainsi que la durée totale. Les variables sont ajoutées à `.env.example` dans `pre.008`. Le frontend reçoit uniquement les durées danimation nécessaires dans les ordres du splash; la durée minimale reste backend. Après readiness frontend, Rust émet `fade_in`, attend `fade_in`, maintient la splash pleinement visible pendant `minimum`, émet `fade_out`, attend `fade_out`, puis affiche/focalise `main` et détruit `splash`. Avec `fade_in=3000`, `minimum=12000` et `fade_out=3000`, le lifecycle backend attendu est proche de `18000 ms`. Le splash possède en outre le flux de messages généraux en bas et le flux diagnostic debug en haut repris du comportement kbot3.
### 13.3 Racine runtime en développement ### 13.3 Racine runtime en développement
Avec le workflow workspace `cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json`, Tauri peut lancer le binaire Rust avec la crate applicative comme current working directory. Les defaults Config `config/`, `config/schemas/` et `.env` sont cependant des chemins de projet enracinés à la racine du workspace. En build debug, le launcher replace donc le CWD sur `env!("CARGO_MANIFEST_DIR")/../..` **avant** le bootstrap Config. Il ne lit aucune ressource KSP lui-même ; `ksp-config-lib` continue à gérer paths, `.env`, precedence et validation. Un runtime distribué ne doit pas dépendre de cette racine source et sera cadré séparément. Avec le workflow crate-local `(cd crates/ksp-app-config-desk && cargo tauri dev)`, Tauri lance le projet applicatif attendu et le binaire Rust démarre avec la crate applicative comme current working directory. Les defaults Config `config/`, `config/schemas/` et `.env` sont cependant des chemins de projet enracinés à la racine du workspace. En build debug, le launcher replace donc le CWD sur `env!("CARGO_MANIFEST_DIR")/../..` **avant** le bootstrap Config. Il ne lit aucune ressource KSP lui-même ; `ksp-config-lib` continue à gérer paths, `.env`, precedence et validation. Un runtime distribué ne doit pas dépendre de cette racine source et sera cadré séparément.
### 13.4 Header et navigation ### 13.4 Header et navigation
Le logo de Config Desk porte déjà lidentité graphique KSP. Le header ne répète donc pas `KSP` en texte : il suit `Config Desk — <vue active>`. Les cinq commandes principales restent des tabs/pills alignées à droite tant que leur nombre reste compact ; les futures applications qui dépassent cette surface utiliseront un dropdown. Les clics/changements de tabs sont tracés (`trace` pour le clic/état technique, `debug` pour lactivation utilisateur significative). Le logo de Config Desk porte déjà lidentité graphique KSP. Le header ne répète donc pas `KSP` en texte : il suit `Config Desk — <vue active>`. Les cinq commandes principales utilisent désormais des pills verticales dans une sidebar du contenu principal, alignées avec Wallet Desk ; le header conserve uniquement l'identité fonctionnelle et le titre de la vue active. Les clics/changements de tabs sont tracés (`trace` pour le clic/état technique, `debug` pour lactivation utilisateur significative).
## 14. Présentation et Markdown ## 14. Présentation et Markdown
@@ -1029,7 +1030,7 @@ Le shell applicatif matérialise désormais cette architecture dans `frontend/ts
Le bootstrap Logging sépare également la **résolution du plan de démarrage** de l'**installation du subscriber**. Une source invalide peut ainsi être testée comme plan fallback sans initialiser le subscriber global du binaire de tests. Le bootstrap Logging sépare également la **résolution du plan de démarrage** de l'**installation du subscriber**. Une source invalide peut ainsi être testée comme plan fallback sans initialiser le subscriber global du binaire de tests.
La validation frontend de `pre.018` utilise `npm run check` pour **`tsc --noEmit` uniquement**. Le build Vite de production n'est pas lancé séparément : `cargo tauri build` déclenche déjà `npm run build` via `beforeBuildCommand`. Pour KSP, le build Tauri est réservé à la **dernière validation**, après `fmt/check/clippy`, les tests, le type-check frontend et le parcours fonctionnel `tauri dev`. La convention desktop actuelle nexécute plus de script npm de contrôle/dev/build directement côté opérateur. Tauri possède le cycle applicatif et déclenche `npm run dev` / `npm run build` via ses hooks crate-local. Le build Tauri reste réservé à la **dernière validation**, après `fmt/check/clippy`, les tests et le parcours fonctionnel `cargo tauri dev`.
## 16. Stratégie de tests ## 16. Stratégie de tests
@@ -1200,7 +1201,7 @@ pre.007 frontend logging commun
pre.008 shell main + splash de référence pre.008 shell main + splash de référence
- tw_splash + tw_main - tw_splash + tw_main
- lifecycle splash configurable - lifecycle splash configurable
- KSP_DESK_SPLASH_MINIMUM_MS / KSP_DESK_SPLASH_FADE_MS + .env.example - KSP_DESK_SPLASH_FADE_IN_MS / KSP_DESK_SPLASH_MINIMUM_MS / KSP_DESK_SPLASH_FADE_OUT_MS + .env.example
- normalisation du CWD debug vers la racine workspace avant bootstrap Config - normalisation du CWD debug vers la racine workspace avant bootstrap Config
- navigation monofenêtre + header `Config Desk — <vue>` - navigation monofenêtre + header `Config Desk — <vue>`
- helpers show/focus/destroy - helpers show/focus/destroy
@@ -1456,3 +1457,10 @@ Aucune question n'empêche d'ouvrir le développement après validation du prés
5. retour Rust -> console WebKit : étudié à la clôture `pre.019` puis **reporté hors `0.1.4`**. Le bridge WebView -> Rust -> KSP et le panneau Test Logging couvrent le besoin fonctionnel de cette release. Une future réflexion Rust -> WebKit devra rester sous ownership de `ksp-logging-lib`, sans second subscriber, double émission ni boucle avec le bridge `console.*`. 5. retour Rust -> console WebKit : étudié à la clôture `pre.019` puis **reporté hors `0.1.4`**. Le bridge WebView -> Rust -> KSP et le panneau Test Logging couvrent le besoin fonctionnel de cette release. Une future réflexion Rust -> WebKit devra rester sous ownership de `ksp-logging-lib`, sans second subscriber, double émission ni boucle avec le bridge `console.*`.
Ces points doivent être résolus par code/tests dans les prereleases prévues, pas par contournement applicatif. Ces points doivent être résolus par code/tests dans les prereleases prévues, pas par contournement applicatif.
### Harmonisation desktop `0.2.6-pre.014`
Le polish partagé avec Wallet Desk déplace les pills de navigation Config dans une sidebar verticale du contenu principal, tout en conservant la palette claire historique de Config Desk. Les quatre fichiers HTML des deux Desks utilisent les en-têtes normalisés `file/version`. Le splash reprend les flux kbot3 (messages généraux en bas, diagnostics debug en haut) avec trois timings Config-owned distincts `KSP_DESK_SPLASH_FADE_IN_MS`, `KSP_DESK_SPLASH_MINIMUM_MS` et `KSP_DESK_SPLASH_FADE_OUT_MS`.
Le même chantier corrige le lancement Tauri multi-app : chaque app est lancée depuis sa crate, car `-c/--config` est un overlay et non un sélecteur de backend.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md --> <!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
<!-- version: 30 --> <!-- version: 31 -->
# Plan `0.2.6` — Wallet Desk # Plan `0.2.6` — Wallet Desk
@@ -1137,7 +1137,7 @@ cd ../..
Le cycle normal de développement/validation frontend passe par Tauri depuis la racine du workspace : Le cycle normal de développement/validation frontend passe par Tauri depuis la racine du workspace :
```bash ```bash
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json (cd crates/ksp-app-wallet-desk && cargo tauri dev)
``` ```
Tauri déclenche alors automatiquement le script configuré dans `beforeDevCommand`. Les contrôles fonctionnels frontend à forte valeur portent notamment sur : Tauri déclenche alors automatiquement le script configuré dans `beforeDevCommand`. Les contrôles fonctionnels frontend à forte valeur portent notamment sur :
@@ -1754,7 +1754,7 @@ cd ../..
Le parcours normal de développement et de validation fonctionnelle frontend passe par : Le parcours normal de développement et de validation fonctionnelle frontend passe par :
```bash ```bash
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json (cd crates/ksp-app-wallet-desk && cargo tauri dev)
``` ```
Tauri exécute automatiquement le `beforeDevCommand` configuré. Aucun script npm applicatif de contrôle, développement ou build n'est lancé directement par l'opérateur. Tauri exécute automatiquement le `beforeDevCommand` configuré. Aucun script npm applicatif de contrôle, développement ou build n'est lancé directement par l'opérateur.
@@ -1768,11 +1768,11 @@ cargo check --workspace
cargo clippy --workspace --all-targets cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk cargo test -p ksp-app-wallet-desk
cargo test --workspace cargo test --workspace
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json (cd crates/ksp-app-wallet-desk && cargo tauri dev)
cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json (cd crates/ksp-app-wallet-desk && cargo tauri build)
``` ```
`cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json` reste **la toute dernière opération** de validation. Il déclenche lui-même le build frontend via `beforeBuildCommand`. Le smoke Devnet opt-in est exécuté séparément avant cette dernière opération lorsqu'il appartient au checkpoint final. `(cd crates/ksp-app-wallet-desk && cargo tauri build)` reste **la toute dernière opération** de validation. Il déclenche lui-même le build frontend via `beforeBuildCommand`. Le smoke Devnet opt-in est exécuté séparément avant cette dernière opération lorsqu'il appartient au checkpoint final.
## 26. Hors périmètre confirmé ## 26. Hors périmètre confirmé
@@ -1793,11 +1793,11 @@ RPC direct depuis frontend
prix offchain pendant 0.2.6 prix offchain pendant 0.2.6
``` ```
## 27. TODO `0.2.11` — intégration prix offchain ## 27. TODO `0.2.12` — intégration prix offchain
La visualisation de prix reste hors `0.2.6`. La visualisation de prix reste hors `0.2.6`.
Une application spécialisée doit d'abord valider les sources offchain, normalisation, cache, rafraîchissement et UX. Ensuite `0.2.11` intégrera cette capacité dans Wallet Desk via le composant partagé retenu, sans dupliquer la logique de récupération de prix. Une application spécialisée doit d'abord valider les sources offchain, normalisation, cache, rafraîchissement et UX. Ensuite `0.2.12` intégrera cette capacité dans Wallet Desk via le composant partagé retenu, sans dupliquer la logique de récupération de prix.
Aucune API fictive de prix n'est introduite dans Wallet Desk en `0.2.6`. Aucune API fictive de prix n'est introduite dans Wallet Desk en `0.2.6`.
@@ -1839,3 +1839,36 @@ v0.2.6-pre.001-fix.002
``` ```
Après application/commit du fix documentaire, `pre.002` démarre la crate Tauri et le shell complet du gabarit, avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, splash/main et logging bridge. Après application/commit du fix documentaire, `pre.002` démarre la crate Tauri et le shell complet du gabarit, avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, splash/main et logging bridge.
## `pre.014` — polish desktop partagé et lancement Tauri multi-app
La tranche harmonise Config Desk et Wallet Desk sans modifier les contrats Wallet métier :
```text
HTML : en-têtes Khadhroony file/version sur main + splash
Splash : fade-in / minimum / fade-out distincts via Config
Splash : messages généraux défilants en bas
Splash debug : diagnostics défilants en haut uniquement en debug
Splash : aucune scrollbar fenêtre, titre central non sélectionnable
Main : pills/tabs dans une sidebar du contenu, pas dans le header
Palette : Wallet Desk revient sur la palette claire de référence Config Desk
Tauri : lancement crate-local obligatoire dans le workspace multi-app
```
La cause du mélange observé Config-Vite / Wallet-backend est explicitement corrigée : `-c/--config` fusionne une configuration avec le projet Tauri déjà découvert ; il ne choisit pas la crate. Les commandes de validation visuelle sont donc :
```bash
KSP_DESK_SPLASH_FADE_IN_MS=1500 \
KSP_DESK_SPLASH_MINIMUM_MS=6000 \
KSP_DESK_SPLASH_FADE_OUT_MS=1500 \
bash -lc 'cd crates/ksp-app-config-desk && cargo tauri dev'
KSP_DESK_SPLASH_FADE_IN_MS=1500 \
KSP_DESK_SPLASH_MINIMUM_MS=6000 \
KSP_DESK_SPLASH_FADE_OUT_MS=1500 \
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre014 \
bash -lc 'cd crates/ksp-app-wallet-desk && cargo tauri dev'
```
Le build Tauri de production reste reporté à `pre.015` et doit demeurer labsolue dernière opération.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_KSP.md --> <!-- file: docs/rules/RULES_KSP.md -->
<!-- version: 36 --> <!-- version: 37 -->
# Règles spécifiques à KSP # Règles spécifiques à KSP
@@ -224,15 +224,15 @@
- **KSP-APP-023** — Le gabarit Tauri KSP place les sources web sous `frontend/`, les modules TypeScript sous `frontend/ts/`, les styles SCSS sous `frontend/sass/` et les bindings TS-RS générés sous `frontend/ts/bindings/`. Les bindings et artefacts générés ne sont pas versionnés ; les DTO Rust applicatifs restent la source des contrats TS-RS. - **KSP-APP-023** — Le gabarit Tauri KSP place les sources web sous `frontend/`, les modules TypeScript sous `frontend/ts/`, les styles SCSS sous `frontend/sass/` et les bindings TS-RS générés sous `frontend/ts/bindings/`. Les bindings et artefacts générés ne sont pas versionnés ; les DTO Rust applicatifs restent la source des contrats TS-RS.
- **KSP-APP-024** — Chaque application desk Tauri KSP possède un couple de ports Vite/HMR exclusif. L'allocation commence à `1430/1431` pour `ksp-app-config-desk` puis progresse par paires (`1432/1433`, `1434/1435`, etc.). Le port Vite est strict afin qu'une collision échoue explicitement au lieu de sélectionner silencieusement un autre port. - **KSP-APP-024** — Chaque application desk Tauri KSP possède un couple de ports Vite/HMR exclusif. L'allocation commence à `1430/1431` pour `ksp-app-config-desk` puis progresse par paires (`1432/1433`, `1434/1435`, etc.). Le port Vite est strict afin qu'une collision échoue explicitement au lieu de sélectionner silencieusement un autre port.
- **KSP-APP-025** — Dans `package.json`, les bibliothèques consommées par le bundle applicatif appartiennent à `dependencies`; les outils de build/développement et paquets `@types/*` appartiennent à `devDependencies`. npm est utilisé directement uniquement pour installer ou mettre à jour ces dépendances. Le cycle normal de développement/build passe par Tauri, qui déclenche les scripts npm configurés via `beforeDevCommand`/`beforeBuildCommand`; les lockfiles frontend restent non versionnés. - **KSP-APP-025** — Dans `package.json`, les bibliothèques consommées par le bundle applicatif appartiennent à `dependencies`; les outils de build/développement et paquets `@types/*` appartiennent à `devDependencies`. npm est utilisé directement uniquement pour installer ou mettre à jour ces dépendances. Le cycle normal de développement/build passe par Tauri, qui déclenche les scripts npm configurés via `beforeDevCommand`/`beforeBuildCommand`; les lockfiles frontend restent non versionnés.
- **KSP-APP-026** — KSP étant un workspace Rust multi-app, les commandes Tauri lancées depuis la racine sélectionnent explicitement la configuration de l'application avec `-c crates/<app>/tauri.conf.json` (par exemple `cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json`). Après une installation npm effectuée depuis `crates/<app>`, l'opérateur revient à la racine du workspace avant le cycle Cargo/Tauri. - **KSP-APP-026** — Dans un workspace Rust multi-app, `-c/--config` de Tauri est un overlay de configuration et ne sélectionne pas la crate applicative. Les commandes Tauri sont donc exécutées depuis le répertoire de la crate ciblée : `(cd crates/<app> && cargo tauri dev)` ou `(cd crates/<app> && cargo tauri build)`. Le lancement depuis la racine avec `-c crates/<app>/tauri.conf.json` est interdit comme sélecteur dapplication.
- **KSP-APP-027** — Les applications Tauri KSP instrumentent systématiquement le comportement frontend via le bridge Logging commun : actions utilisateur et transitions détat significatives en `debug`, événements techniques fins, rendu/remplacement de sections et étapes fréquentes en `trace`. Les chargements/rafraîchissements de données sont tracés au début et à la fin sans journaliser les payloads sensibles. Cette instrumentation doit permettre de reconstruire le déroulement frontend sans dépendre uniquement de létat visuel. - **KSP-APP-027** — Les applications Tauri KSP instrumentent systématiquement le comportement frontend via le bridge Logging commun : actions utilisateur et transitions détat significatives en `debug`, événements techniques fins, rendu/remplacement de sections et étapes fréquentes en `trace`. Les chargements/rafraîchissements de données sont tracés au début et à la fin sans journaliser les payloads sensibles. Cette instrumentation doit permettre de reconstruire le déroulement frontend sans dépendre uniquement de létat visuel.
- **KSP-APP-028** — Le header dune application desk ne répète pas inutilement lidentité déjà portée par son logo : il affiche le nom ou labréviation fonctionnelle de lapplication, puis un tiret cadratin `—` et le titre de la vue active. Les commandes principales peu nombreuses peuvent utiliser des tabs/pills alignées à droite ; lorsque leur nombre nuit à la lisibilité ou à lespace disponible, un dropdown est préféré. - **KSP-APP-028** — Le header dune application desk ne répète pas inutilement lidentité déjà portée par son logo : il affiche le nom ou labréviation fonctionnelle de lapplication, puis un tiret cadratin `—` et le titre de la vue active. Les commandes principales peu nombreuses peuvent utiliser des tabs/pills alignées à droite ; lorsque leur nombre nuit à la lisibilité ou à lespace disponible, un dropdown est préféré.
- **KSP-APP-029** — En développement workspace, une application desk Tauri normalise le current working directory du processus Rust vers la racine du workspace avant le bootstrap Config lorsque `cargo tauri ... -c crates/<app>/tauri.conf.json` lance le binaire depuis la crate. Cette adaptation ne lit ni ne parse directement `config/`, `.env` ou les variables `KSP_*`/`KSPB_*` : `ksp-config-lib` reste seul propriétaire de ces ressources. Le comportement de distribution/release reste défini séparément et ne doit pas dépendre dun checkout source. - **KSP-APP-029** — En développement workspace, Tauri est lancé depuis `crates/<app>` conformément à `KSP-APP-026`. Le processus Rust peut ensuite normaliser son current working directory vers la racine du workspace avant le bootstrap Config lorsque les chemins de développement KSP y sont enracinés. Cette adaptation ne lit ni ne parse directement `config/`, `.env` ou les variables `KSP_*`/`KSPB_*` : `ksp-config-lib` reste seul propriétaire de ces ressources. Le runtime distribué ne doit pas dépendre dun checkout source.
- **KSP-APP-030** — Lorsquune application KSP persiste des logs applicatifs, chaque lancement doit disposer dun fichier propre et non partagé avec un lancement précédent. Le nom encode au minimum lidentité applicative et un horodatage de démarrage, par exemple `app-name.YYYYMMDD-HHMMSS.log` / `.json` / `.jsonl` selon le format du sink ; une rotation quotidienne ne doit pas fusionner plusieurs exécutions applicatives dans le même fichier. - **KSP-APP-030** — Lorsquune application KSP persiste des logs applicatifs, chaque lancement doit disposer dun fichier propre et non partagé avec un lancement précédent. Le nom encode au minimum lidentité applicative et un horodatage de démarrage, par exemple `app-name.YYYYMMDD-HHMMSS.log` / `.json` / `.jsonl` selon le format du sink ; une rotation quotidienne ne doit pas fusionner plusieurs exécutions applicatives dans le même fichier.
- **KSP-APP-031** — Le niveau de logging spécifique à une application/crate peut être élevé temporairement à `debug` ou `trace` pendant une phase de développement ou correction. Avant la clôture/release de cette application/crate, son niveau de référence est ramené à `info` ou `warn` selon le besoin opératoire ; il nest remonté que lorsquun développement/correctif est explicitement rouvert. - **KSP-APP-031** — Le niveau de logging spécifique à une application/crate peut être élevé temporairement à `debug` ou `trace` pendant une phase de développement ou correction. Avant la clôture/release de cette application/crate, son niveau de référence est ramené à `info` ou `warn` selon le besoin opératoire ; il nest remonté que lorsquun développement/correctif est explicitement rouvert.
- **KSP-APP-032** — Les interfaces desk KSP nutilisent pas les dialogues bloquants natifs du navigateur (`window.alert`, `window.confirm`, `window.prompt`) pour les interactions applicatives normales. Les confirmations destructives ou privilégiées utilisent un modal Bootstrap intégré à lUI, instrumenté par le bridge Logging ; toute exception doit être explicitement justifiée et documentée. - **KSP-APP-032** — Les interfaces desk KSP nutilisent pas les dialogues bloquants natifs du navigateur (`window.alert`, `window.confirm`, `window.prompt`) pour les interactions applicatives normales. Les confirmations destructives ou privilégiées utilisent un modal Bootstrap intégré à lUI, instrumenté par le bridge Logging ; toute exception doit être explicitement justifiée et documentée.
- **KSP-APP-033** — Un test dune application ou dun manager qui peut modifier un document Config du workspace ne traite jamais les valeurs courantes de ce document comme une fixture immuable. Les tests de valeurs exactes utilisent une fixture isolée ; les tests qui lisent la Config workspace vérifient uniquement des invariants, la validité et la cohérence source → résolution → runtime afin de rester valides après une édition légitime par Config Desk. - **KSP-APP-033** — Un test dune application ou dun manager qui peut modifier un document Config du workspace ne traite jamais les valeurs courantes de ce document comme une fixture immuable. Les tests de valeurs exactes utilisent une fixture isolée ; les tests qui lisent la Config workspace vérifient uniquement des invariants, la validité et la cohérence source → résolution → runtime afin de rester valides après une édition légitime par Config Desk.
- **KSP-APP-034** — Pour une application Tauri KSP, npm n'est jamais invoqué directement pour lancer les scripts applicatifs de développement, contrôle ou build. Conformément à `KSP-APP-025`, les seules commandes npm directes sont celles nécessaires à l'installation ou à la mise à jour des dépendances déclarées dans `package.json` (`npm i -D` lors de l'installation initiale retenue par le gabarit, ou `npm update` lors d'une mise à jour). Le cycle applicatif passe par `cargo tauri dev -c crates/<app>/tauri.conf.json` et `cargo tauri build -c crates/<app>/tauri.conf.json`; Tauri déclenche lui-même les scripts configurés via `beforeDevCommand` et `beforeBuildCommand`. Dans une séquence de validation finale, `cargo tauri build -c crates/<app>/tauri.conf.json` est exécuté **en toute dernière opération**, après `fmt/check/clippy`, les tests et le parcours fonctionnel `cargo tauri dev`. - **KSP-APP-034** — Pour une application Tauri KSP, npm nest jamais invoqué directement pour lancer les scripts applicatifs de développement, contrôle ou build. Les seules commandes npm directes servent à installer ou mettre à jour les dépendances déclarées. Le cycle applicatif est crate-local : `(cd crates/<app> && cargo tauri dev)` et `(cd crates/<app> && cargo tauri build)` ; Tauri déclenche lui-même les hooks `beforeDevCommand` / `beforeBuildCommand`, dont le `cwd` reste explicitement la crate. Dans une validation finale, le `cargo tauri build` crate-local est exécuté **en toute dernière opération**, après fmt/audit/check/clippy, tests et parcours fonctionnel.
## Data plane / control plane ## Data plane / control plane

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/000-README.md --> <!-- file: prompts/000-README.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Prompts KSP # Prompts KSP
@@ -31,4 +31,6 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
- [`008-V0_2_3_START_PROMPT.md`](008-V0_2_3_START_PROMPT.md) — prompt préparé par la dernière prerelease de `0.2.2`, destiné à ouvrir `0.2.3 — HTTP Transactions` après publication stable de `0.2.2`; il cible les 11 méthodes Transactions et impose un audit actuel ainsi que la politique no-resend des write submissions. - [`008-V0_2_3_START_PROMPT.md`](008-V0_2_3_START_PROMPT.md) — prompt préparé par la dernière prerelease de `0.2.2`, destiné à ouvrir `0.2.3 — HTTP Transactions` après publication stable de `0.2.2`; il cible les 11 méthodes Transactions et impose un audit actuel ainsi que la politique no-resend des write submissions.
- [`009-V0_2_4_START_PROMPT.md`](009-V0_2_4_START_PROMPT.md) — prompt préparé par `0.2.3-pre.009`, destiné à ouvrir `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale` après publication stable de `0.2.3`; il cible les 15 wrappers restants et impose `KSP-TRANSPORT-007` ainsi qu'un nouvel audit/sizing à `pre.001`. - [`009-V0_2_4_START_PROMPT.md`](009-V0_2_4_START_PROMPT.md) — prompt préparé par `0.2.3-pre.009`, destiné à ouvrir `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale` après publication stable de `0.2.3`; il cible les 15 wrappers restants et impose `KSP-TRANSPORT-007` ainsi qu'un nouvel audit/sizing à `pre.001`.
- [`010-V0_2_5_START_PROMPT.md`](010-V0_2_5_START_PROMPT.md) — prompt préparé par `0.2.4-pre.009` puis finalisé en version 2 par `pre.009-fix.001`, destiné à ouvrir `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`; il impose audit/threat-model/sizing avant choix cryptographiques et cadre `.kspwallet` interopérable, capacités indépendantes VIEW/OWNER, metadata protégées, key slots/rotations, signature, persistence atomique et import/export extensible sans `WalletPolicy`. - [`010-V0_2_5_START_PROMPT.md`](010-V0_2_5_START_PROMPT.md) — prompt préparé par `0.2.4-pre.009` puis finalisé en version 2 par `pre.009-fix.001`, destiné à ouvrir `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`; il impose audit/threat-model/sizing avant choix cryptographiques et cadre `.kspwallet` interopérable, capacités indépendantes VIEW/OWNER, metadata protégées, key slots/rotations, signature, persistence atomique et import/export extensible sans `WalletPolicy`.
- [`011-V0_2_6_START_PROMPT.md`](011-V0_2_6_START_PROMPT.md) — prompt préparé par `0.2.5-pre.010` puis renforcé pendant `pre.010-fix.001``fix.003`, destiné à ouvrir `0.2.6 — Wallet Desk` après le tag stable `v0.2.5`; il impose une première tranche audit/sizing, rappelle les règles Rust/audit structurel, cadre Config composite + Wallet + HTTP `getBalance`, lifecycle VIEW/OWNER, sécurité password/export, validation frontend/Tauri et conserve le TODO `0.2.11` dintégration des prix offchain dans Wallet Desk après validation de la Price Desk spécialisée. - [`011-V0_2_6_START_PROMPT.md`](011-V0_2_6_START_PROMPT.md) — prompt préparé par `0.2.5-pre.010` puis renforcé pendant `pre.010-fix.001``fix.003`, destiné à ouvrir `0.2.6 — Wallet Desk` après le tag stable `v0.2.5`; il impose une première tranche audit/sizing, rappelle les règles Rust/audit structurel, cadre Config composite + Wallet + HTTP `getBalance`, lifecycle VIEW/OWNER, sécurité password/export, validation frontend/Tauri et conserve le TODO `0.2.12` dintégration des prix offchain dans Wallet Desk après validation de la Price Desk spécialisée.
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt préparé pendant `0.2.6-pre.014`, destiné à ouvrir `0.2.7 — conteneur binaire .kspwallet` après publication stable de `0.2.6`; il sépare version de conteneur et `format_version` logique V1, exclut Base64 comme pseudo-sécurité, impose compatibilité JSON V1/migration/interop et utilise Wallet Desk comme canari consommateur sans modification fonctionnelle attendue.

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/011-V0_2_6_START_PROMPT.md --> <!-- file: prompts/011-V0_2_6_START_PROMPT.md -->
<!-- version: 3 --> <!-- version: 4 -->
# Prompt de démarrage `0.2.6` — Wallet Desk # Prompt de démarrage `0.2.6` — Wallet Desk
@@ -387,7 +387,7 @@ Ne pas copier aveuglément les dépendances inutiles. Réutiliser les patterns
Le port de développement doit être réservé distinctement de Config Desk après audit des ports déjà utilisés. Le port de développement doit être réservé distinctement de Config Desk après audit des ports déjà utilisés.
Le build frontend standalone `npm run check` reste un type-check/lint/test, pas un production build. Le build production final est déclenché par Tauri via `beforeBuildCommand`. Aucun script npm de contrôle/dev/build nest lancé directement par lopérateur. Tauri possède le cycle frontend et déclenche ses hooks npm ; le build production final reste déclenché via `beforeBuildCommand`.
## 13. Logging ## 13. Logging
@@ -512,7 +512,6 @@ Couvrir au minimum :
Au minimum : Au minimum :
```text ```text
npm run check
``` ```
Ajouter des tests ciblés seulement lorsque leur valeur est démontrée ; ne pas construire le bundle production dans ce script. Ajouter des tests ciblés seulement lorsque leur valeur est démontrée ; ne pas construire le bundle production dans ce script.
@@ -621,13 +620,12 @@ cargo check --workspace
cargo clippy --workspace --all-targets cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk cargo test -p ksp-app-wallet-desk
cargo test --workspace cargo test --workspace
npm run check --prefix crates/ksp-app-wallet-desk
``` ```
Le build Tauri de production est la **dernière** opération de validation : Le build Tauri de production est la **dernière** opération de validation :
```bash ```bash
cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json (cd crates/ksp-app-wallet-desk && cargo tauri build)
``` ```
Le smoke Devnet opt-in, s'il est livré, est exécuté séparément avec une commande documentée. Le smoke Devnet opt-in, s'il est livré, est exécuté séparément avec une commande documentée.
@@ -654,7 +652,7 @@ Le gate n'est considéré franchi que si ces livrables sont écrits/consignés d
## 24. Hors périmètre ## 24. Hors périmètre
```text ```text
modification du wire .kspwallet V1 sans défaut démontré modification du format logique/cryptographique .kspwallet V1 sans défaut démontré (le conteneur de persistence est traité séparément en 0.2.7)
WalletPolicy / execution policy WalletPolicy / execution policy
construction/simulation/envoi de transaction construction/simulation/envoi de transaction
WebSocket/gRPC WebSocket/gRPC
@@ -668,11 +666,11 @@ trading
Le Wallet Desk valide et administre Wallet ; il ne devient pas l'application globale KSP. Le Wallet Desk valide et administre Wallet ; il ne devient pas l'application globale KSP.
### TODO futur `0.2.11` — prix offchain dans Wallet Desk ### TODO futur `0.2.12` — prix offchain dans Wallet Desk
La visualisation de prix offchain n'appartient pas au scope de `0.2.6`. Une application spécialisée de visualisation de prix offchain doit d'abord être réalisée et valider durablement ses sources, contrats, rafraîchissement, cache et UX. La visualisation de prix offchain n'appartient pas au scope de `0.2.6`. Une application spécialisée de visualisation de prix offchain doit d'abord être réalisée et valider durablement ses sources, contrats, rafraîchissement, cache et UX.
Pour `0.2.11`, prévoir explicitement un chantier d'intégration de cette capacité dans `ksp-app-wallet-desk` afin qu'un wallet puisse afficher les informations de prix offchain pertinentes sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. Le futur travail devra réauditer la frontière entre application, bibliothèque/service partagé et Transport avant implémentation ; le TODO n'autorise pas l'ajout anticipé de logique de prix dans Wallet Desk pendant `0.2.6`. Pour `0.2.12`, prévoir explicitement un chantier d'intégration de cette capacité dans `ksp-app-wallet-desk` afin qu'un wallet puisse afficher les informations de prix offchain pertinentes sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. Le futur travail devra réauditer la frontière entre application, bibliothèque/service partagé et Transport avant implémentation ; le TODO n'autorise pas l'ajout anticipé de logique de prix dans Wallet Desk pendant `0.2.6`.
## 25. Résultat attendu de `0.2.6` ## 25. Résultat attendu de `0.2.6`

View File

@@ -0,0 +1,268 @@
<!-- file: prompts/012-V0_2_7_START_PROMPT.md -->
<!-- version: 1 -->
# Prompt de démarrage `0.2.7` — conteneur binaire `.kspwallet`
## 1. Contexte de reprise
La base attendue est la release stable :
```text
v0.2.6
```
`0.2.5` a stabilisé `ksp-wallet-lib` et le format logique/cryptographique `.kspwallet` V1 : capacités VIEW/OWNER indépendantes, Argon2id, XChaCha20-Poly1305, autorité OWNER Ed25519, persistence atomique/no-clobber, rotations, révocation VIEW forte, signature et transferts Solana CLI JSON/Base58.
`0.2.6` a validé cette surface au travers de `ksp-app-wallet-desk` sans déplacer la propriété du format ou des secrets hors de Wallet.
La release à ouvrir est :
```text
0.2.7 — conteneur binaire de persistence .kspwallet
```
La première tranche est `0.2.7-pre.001` et commence obligatoirement par **audit + threat-model + options de framing/codec + compatibilité/migration + sizing**, avant toute modification du wire de persistence.
## 2. Motivation
Le document `.kspwallet` V1 actuellement persisté est un JSON strict. Ses valeurs sensibles restent cryptographiquement protégées, mais le fichier peut être ouvert et lu comme texte par un éditeur ordinaire.
L'objectif de `0.2.7` est de rendre la **persistence physique** binaire et explicitement framed, sans prétendre que cette transformation ajoute une protection cryptographique supplémentaire.
### Non-solution explicite : Base64 seul
Base64 seul n'est pas une solution acceptable :
- il reste textuel/ASCII ;
- il est trivialement réversible ;
- il ne fournit aucune confidentialité ni intégrité supplémentaire ;
- il augmente typiquement la taille d'environ un tiers.
Le changement recherché est un vrai **conteneur binaire de persistence**, pas une obfuscation textuelle.
## 3. Séparation normative : conteneur vs format logique
Ne pas consommer artificiellement `format_version = 2` uniquement parce que la persistence devient binaire.
Distinguer explicitement :
```text
container/framing version
!=
wallet logical/cryptographic format_version
```
Le payload logique V1 doit pouvoir conserver ses invariants actuels :
```text
VIEW / OWNER
KDF / salts / slots
AEAD / nonces / AAD
OWNER signature/transcript
metadata/secret compartments
Solana keypair identity
rotation / strong VIEW administration
```
Un futur `format_version >= 2` reste disponible pour de vraies évolutions d'autorisation/crypto, notamment second facteur.
## 4. Audit obligatoire `pre.001`
Comparer au minimum :
1. framing binaire custom KSP avec grammar explicite ;
2. codecs binaires Serde adaptés et stables ;
3. formats self-describing éventuels ;
4. éventuelle compression du payload si elle apporte une vraie valeur ;
5. impact interop externe Rust/Python/Go/C ;
6. complexité de parsing borné et rejet des inputs adversariaux ;
7. dépendances nouvelles et leur maturité/maintenance ;
8. capacité à préserver les test vectors cryptographiques V1 ;
9. migration et détection sûre du JSON V1 legacy ;
10. règles d'unknown container version / taille / trailing bytes / canonicalité.
Ne choisir aucun codec uniquement parce qu'il est pratique dans Rust : le format doit rester spécifiable indépendamment de l'implémentation.
## 5. Compatibilité obligatoire
`ksp-wallet-lib` doit rester capable de lire les `.kspwallet` JSON V1 déjà créés.
Le candidat doit définir explicitement :
```text
legacy JSON V1 read
new binary container read
new binary container write default
migration explicite legacy -> binary
no-clobber / atomic replace
rollback/interruption behavior
```
Une simple ouverture d'un legacy wallet ne doit pas le réécrire silencieusement sans contrat explicite.
## 6. Sécurité
Le conteneur binaire n'est **pas** présenté comme une nouvelle couche de chiffrement.
Le threat model V1 reste notamment :
- attaquant possédant le fichier ;
- connaissance complète de la spec ;
- essais offline illimités sur les passwords ;
- connaissance éventuelle d'un seul credential VIEW ou OWNER ;
- capacité à modifier/remplacer/rollback le fichier.
Les gains de `0.2.7` sont :
```text
framing déterministe
non-textual persistence
parsing/versioning physique explicite
meilleure séparation container/payload
préparation des futures migrations
```
Pas :
```text
obscurity = security
Base64 = encryption
binaire = protection contre attaque offline
```
## 7. Frontière `ksp-wallet-lib`
Le changement doit rester possédé par `ksp-wallet-lib`.
`ksp-app-wallet-desk` ne doit pas connaître :
- le codec du conteneur ;
- sa magic/version physique ;
- son layout ;
- ses offsets ;
- la distinction legacy/new lors des opérations ordinaires.
Les APIs de haut niveau Wallet doivent absorber cette évolution autant que possible.
## 8. Wallet Desk comme canari consommateur
Aucune modification fonctionnelle de `ksp-app-wallet-desk` n'est attendue uniquement pour ce changement de persistence.
Après modification de Wallet, rejouer au minimum :
```bash
cargo test -p ksp-wallet-lib
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
Puis un parcours fonctionnel Wallet Desk doit prouver au minimum :
```text
inventory
create
locked inspect
VIEW unlock
OWNER unlock
metadata
rotation
strong VIEW disable/recreate
export/import
balance HTTP
```
Si le Desk doit être modifié pour comprendre le conteneur physique, considérer cela comme un signal d'abstraction insuffisante et réauditer la frontière Wallet avant d'accepter le changement.
## 9. Interop et test vectors
Mettre à jour la spec `.kspwallet` avec une grammar byte-level du conteneur :
- magic ;
- version du conteneur ;
- longueur(s) ;
- codec/payload kind ;
- endianess ;
- bornes ;
- trailing bytes ;
- canonicalité ;
- unknown-version policy.
Conserver les test vectors cryptographiques V1 lorsque les transcripts/AAD ne changent pas, et ajouter des fixtures déterministes container + payload.
Prévoir au moins un parseur externe de canari afin d'éviter un format binaire « Rust-only » accidentel.
## 10. Futur `format_version >= 2` / second facteur
Ce chantier **ne réalise pas** password + OTP/2FA.
Pour une future version logique :
- `ksp-wallet-lib` possède le format, les facteurs, challenges et vérifications ;
- un client interactif tel que Wallet Desk devra évoluer lorsqu'un OTP, enrollment/recovery, hardware/WebAuthn ou consentement externe doit être saisi/présenté ;
- un seed TOTP enfermé uniquement dans le même fichier ne doit pas être assimilé automatiquement à un second facteur indépendant contre un attaquant qui possède ce fichier.
Le conteneur `0.2.7` doit simplement laisser cette évolution possible sans la pré-concevoir excessivement.
## 11. Dépendances
Toute nouvelle dépendance externe commune est déclarée dans `[workspace.dependencies]` puis consommée avec `.workspace = true`.
Avant ajout :
- vérifier la version actuelle ;
- auditer maintenance/licence/graphe ;
- éviter les générations obsolètes inutiles ;
- vérifier qu'une petite grammar custom n'est pas plus durable qu'un codec lourd, ou inversement.
## 12. Discipline Rust / tests
Conserver toutes les règles KSP actuelles : Rust 2024, async-first, pas de `unsafe`, `unwrap`, `expect`, `panic` runtime, audits structurels, dépendance Logging via `ksp-logging-lib`, Config hors Wallet.
Validation générale :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-wallet-lib
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
## 13. Forecast souple à recalibrer en `pre.001`
Point de départ recommandé :
```text
pre.001 audit framing/codec/threat-model/interop/sizing
pre.002 spec container + parser bounded + fixtures
pre.003 writer binary + persistence integration
pre.004 legacy JSON read + migration explicite
pre.005 adversarial/interop/test vectors
pre.006 Wallet Desk regression + live functional smoke
pre.007 compliance/docs/prompt suivant
rel.001
```
Ce découpage est indicatif ; le gate `pre.001` doit le recalibrer selon le choix de format réel.
## 14. Résultat attendu
À la clôture stable :
```text
les nouveaux .kspwallet sont persistés dans un conteneur binaire spécifié
les anciens JSON V1 restent lisibles
aucune sécurité fictive n'est attribuée au binaire/Base64
les semantics cryptographiques V1 restent intactes sauf défaut démontré
les migrations sont explicites et testées
Wallet Desk fonctionne sans connaissance du format physique
interop externe et parsing adversarial sont prouvés
```
## 15. Instruction d'ouverture
Commencer par relire la spec `.kspwallet` V1, les modules `wire`, `transcript`, `persistence`, `wallet`, `owner`, `transfer` et tous les test vectors actuels.
Ensuite produire l'audit `0.2.7-pre.001` avant de coder le nouveau conteneur.