152 Commits

Author SHA1 Message Date
4d77b607ea v0.2.7-rel.001 2026-08-23 11:38:52 +02:00
307711f873 v0.2.7-pre.014-fix.001 2026-08-23 11:28:01 +02:00
5aa7b45840 v0.2.7-pre.014 2026-08-23 11:15:57 +02:00
3c5786f273 v0.2.7-pre.013 2026-08-23 10:24:28 +02:00
3b4d355537 v0.2.7-pre.012-fix.001 2026-08-23 09:55:34 +02:00
628b4f12f2 v0.2.7-pre.012 2026-08-23 09:40:44 +02:00
66deaf8245 v0.2.7-pre.011-fix.001 2026-08-23 09:17:34 +02:00
0e256a8ecf v0.2.7-pre.011-fix.001 2026-08-23 09:17:13 +02:00
9eb0e19d81 v0.2.7-pre.011 2026-08-23 08:58:03 +02:00
74686892e9 v0.2.7-pre.010-fix.001 2026-08-23 00:26:17 +02:00
1391858972 v0.2.7-pre.010 2026-08-23 00:12:39 +02:00
98bf88e431 v0.2.7-pre.009-fix.002 2026-08-22 23:26:09 +02:00
f0f444bc86 v0.2.7-pre.009-fix.001 2026-08-22 23:14:32 +02:00
d17161234a v0.2.7-pre.009 2026-08-22 23:08:47 +02:00
93199d1856 v0.2.7-pre.008-fix.001 2026-08-22 21:54:27 +02:00
6fefc64e75 v0.2.7-pre.008 2026-08-22 21:44:18 +02:00
4c540d67a7 v0.2.7-pre.007-fix.001 2026-08-22 21:00:29 +02:00
b67fa89f44 v0.2.7-pre.007 2026-08-22 20:42:06 +02:00
435126f67a v0.2.7-pre.006-fix.001 2026-08-22 20:08:55 +02:00
8721e54b18 v0.2.7-pre.006 2026-08-22 19:56:20 +02:00
6e3a0fa034 v0.2.7-pre.005 2026-08-22 19:16:41 +02:00
34637848eb v0.2.7-pre.004-fix.001 2026-08-22 18:34:52 +02:00
778ea58ee1 v0.2.7-pre.004 2026-08-22 18:21:20 +02:00
b0461f15ec v0.2.7-pre.003 2026-08-22 17:56:17 +02:00
d5df0fe9af v0.2.7-pre.003 2026-08-22 17:56:05 +02:00
ab29dc51bb v0.2.7-pre.002-fix.001 2026-08-22 16:55:21 +02:00
b6908cb573 v0.2.7-pre.002-fix.001 2026-08-22 16:49:43 +02:00
b64a799c85 v0.2.7-pre.002 2026-08-22 16:39:13 +02:00
cf4b28df2b v0.2.7-pre.001-fix.001 2026-08-22 16:00:56 +02:00
9e8fd53291 v0.2.7-pre.001 2026-08-22 15:34:03 +02:00
3c9c1d1349 v0.2.6-rel.001 2026-08-22 14:16:31 +02:00
79fee574d9 v0.2.6-pre.018-fix.002 2026-08-22 13:43:43 +02:00
fca9abb11a v0.2.6-pre.018-fix.001 2026-08-22 12:52:03 +02:00
ba8f42f9b1 v0.2.6-pre.018 2026-08-22 10:56:26 +02:00
362123f357 v0.2.6-pre.017 2026-08-22 09:39:13 +02:00
5c0be421da v0.2.6-pre.016-fix.002 2026-08-22 08:37:56 +02:00
738b340cc7 v0.2.6-pre.016-fix.001 2026-08-22 08:24:57 +02:00
92a2c4fff3 v0.2.6-pre.016 2026-08-22 08:18:38 +02:00
946d88322b v0.2.6-pre.015-fix.001 2026-08-22 03:29:10 +02:00
d370027c02 v0.2.6-pre.015 2026-08-22 03:21:49 +02:00
bd561cad47 v0.2.6-pre.014-fix.001 2026-08-22 03:04:25 +02:00
6907e4bbb8 v0.2.6-pre.014 2026-08-22 00:21:14 +02:00
c07db925b1 v0.2.6-pre.013-fix.002 2026-08-21 23:14:13 +02:00
263464ddd7 v0.2.6-pre.013-fix.001 2026-08-21 17:27:20 +02:00
4b350b4161 v0.2.6-pre.013 2026-08-21 17:15:40 +02:00
ecbd6b746d v0.2.6-pre.012 2026-08-21 16:22:16 +02:00
8ba554306a v0.2.6-pre.011 2026-08-21 15:22:42 +02:00
6dcd278459 v0.2.6-pre.010-fix.001 2026-08-21 14:53:50 +02:00
4dcdbfaa55 v0.2.6-pre.010 2026-08-21 13:11:16 +02:00
42326e7f02 v0.2.6-pre.009-fix.001 2026-08-21 12:45:08 +02:00
2b7afab663 v0.2.6-pre.009 2026-08-21 12:35:01 +02:00
7d03825ec6 v0.2.6-pre.008-fix.001 2026-08-21 12:26:06 +02:00
4db4087b8a v0.2.6-pre.008-fix.001 2026-08-21 11:50:18 +02:00
1a9edd14e1 v0.2.6-pre.008 2026-08-21 11:36:28 +02:00
787df2861f v0.2.6-pre.007-fix.002 2026-08-21 11:01:07 +02:00
60b346c552 v0.2.6-pre.007-fix.001 2026-08-21 10:55:51 +02:00
f11f0921b0 v0.2.6-pre.007 2026-08-21 10:50:36 +02:00
4c9086e2f0 v0.2.6-pre.006-fix.001 2026-08-21 10:15:29 +02:00
a207cf7161 v0.2.6-pre.006 2026-08-21 10:01:11 +02:00
63e711629f v0.2.6-pre.005-fix.001 2026-08-21 09:43:48 +02:00
7aa38b0daf v0.2.6-pre.005 2026-08-21 09:33:04 +02:00
2132dfd884 v0.2.6-pre.004-fix.001 2026-08-21 08:44:21 +02:00
76bfce17c1 v0.2.6-pre.004 2026-08-20 23:01:41 +02:00
6eb4d71043 v0.2.6-pre.003-fix.004 2026-08-20 21:55:41 +02:00
7eb0381932 v0.2.6-pre.003-fix.003 2026-08-20 21:42:03 +02:00
3fa4ab251a v0.2.6-pre.003-fix.002 2026-08-20 21:33:46 +02:00
0e72914237 v0.2.6-pre.003-fix.001 2026-08-20 21:23:09 +02:00
f9f9ac79b6 v0.2.6-pre.003 2026-08-20 17:08:32 +02:00
1a57cdeef0 v0.2.6-pre.002 2026-08-20 16:51:08 +02:00
927b9e4709 v0.2.6-pre.001-fix.002 2026-08-20 15:23:32 +02:00
b7d9e7fd1f v0.2.6-pre.001-fix.001 2026-08-20 14:49:09 +02:00
136ea6d76e v0.2.6-pre.001 2026-08-20 14:00:34 +02:00
8c65096f4c v0.2.5-rel.001 2026-08-20 12:31:08 +02:00
cb142b1cb8 v0.2.5-pre.010.fix.003 2026-08-20 12:15:36 +02:00
08c8046262 v0.2.5-pre.010.fix.002 2026-08-20 11:53:55 +02:00
c0b131bf6f v0.2.5-pre.010.fix.001 2026-08-20 11:17:12 +02:00
5bf9651038 v0.2.5-pre.010 2026-08-20 09:58:14 +02:00
e91bf36e9e v0.2.5-pre.009 2026-08-20 09:24:10 +02:00
1125ade4c1 v0.2.5-pre.008 2026-08-19 18:47:06 +02:00
518cc9257f v0.2.5-pre.007-fix.001 2026-08-19 18:12:31 +02:00
46afe10423 v0.2.5-pre.007 2026-08-19 18:08:01 +02:00
25edcc231e v0.2.5-pre.006-fix.001 2026-08-19 17:35:14 +02:00
6094d33120 v0.2.5-pre.006 2026-08-19 17:31:29 +02:00
dcd23abb92 v0.2.5-pre.005-fix.001 2026-08-19 13:15:09 +02:00
36b98e0abc v0.2.5-pre.005 2026-08-19 13:09:16 +02:00
3c069347b7 v0.2.5-pre.004-fix.002 2026-08-19 11:47:32 +02:00
a0a5595186 v0.2.5-pre.004-fix.001 2026-08-19 11:37:36 +02:00
6c1172efc3 v0.2.5-pre.004 2026-08-19 11:29:05 +02:00
d5536b0778 v0.2.5-pre.003-fix.001 2026-08-19 11:24:32 +02:00
c6794af05b v0.2.5-pre.003 2026-08-19 10:52:21 +02:00
bcda6db11f v0.2.5-pre.002-fix.002 2026-08-19 10:08:47 +02:00
51b5114ae2 v0.2.5-pre.002-fix.001 2026-08-19 10:03:37 +02:00
c97a8739ab v0.2.5-pre.002 2026-08-19 09:56:01 +02:00
c2ccef3849 v0.2.5-pre.001-fix.001 2026-08-19 09:37:07 +02:00
58134c81d1 v0.2.5-pre.001 2026-08-19 09:11:36 +02:00
5b3967327a v0.2.4-rel.001 2026-08-18 23:51:56 +02:00
804b92b957 v0.2.4-pre.009-fix.001 2026-08-18 23:26:17 +02:00
e6772c109d v0.2.4-pre.009 2026-08-18 22:43:09 +02:00
7e84522787 v0.2.4-pre.008-fix.001 2026-08-18 21:53:33 +02:00
37461ec248 v0.2.4-pre.008 2026-08-18 21:52:00 +02:00
4252a21140 v0.2.4-pre.007 2026-08-18 21:33:21 +02:00
7a889dd466 v0.2.4-pre.006 2026-08-18 21:21:02 +02:00
d3cc7c93f0 v0.2.4-pre.005 2026-08-18 21:03:00 +02:00
561b6678ed v0.2.4-pre.004 2026-08-18 20:54:16 +02:00
e500024c3a v0.2.4-pre.003 2026-08-18 20:51:52 +02:00
033800f897 v0.2.4-pre.002-fix.001 2026-08-18 20:21:01 +02:00
cf8d79567d v0.2.4-pre.002 2026-08-18 20:12:46 +02:00
cb19742cf7 v0.2.4-pre.001-fix.001 2026-08-18 19:51:00 +02:00
c03a3eaf5d v0.2.4-pre.001 2026-08-18 19:36:57 +02:00
f7acac3fc8 v0.2.3-rel.001 2026-08-18 17:42:12 +02:00
039b93cfb3 v0.2.3-pre.009 2026-08-18 17:33:46 +02:00
c9a0f7fd37 v0.2.3-pre.008 2026-08-18 16:47:54 +02:00
704110d509 v0.2.3-pre.007 2026-08-18 16:12:07 +02:00
fb32ed03f8 v0.2.3-pre.006-fix.001 2026-08-18 12:40:24 +02:00
4439589f0d v0.2.3-pre.006 2026-08-18 12:35:29 +02:00
1e1bc0d421 v0.2.3-pre.005 2026-08-18 12:33:16 +02:00
bd26d4f636 v0.2.3-pre.004 2026-08-18 12:14:59 +02:00
2a7f3e8f40 v0.2.3-pre.003 2026-08-18 11:35:51 +02:00
1d37f04d63 v0.2.3-pre.002-fix.001 2026-08-18 11:27:24 +02:00
1ccf11bcc1 v0.2.3-pre.002 2026-08-18 11:13:04 +02:00
d06284fca2 v0.2.3-pre.001 2026-08-18 10:47:19 +02:00
fff69ead9c v0.2.2-rel.001 2026-08-18 10:14:43 +02:00
c93a833bac v0.2.2-pre.007-fix.002 2026-08-18 10:04:18 +02:00
05e0e96083 v0.2.2-pre.007-fix.001 2026-08-18 09:56:55 +02:00
b57f796187 v0.2.2-pre.007 2026-08-18 09:48:11 +02:00
ecfb9500eb v0.2.2-pre.006-fix.001 2026-08-18 09:37:03 +02:00
e14ce1c36f v0.2.2-pre.006 2026-08-18 08:27:42 +02:00
83cb861e54 v0.2.2-pre.005 2026-08-18 08:17:10 +02:00
1d1bc6a4d6 v0.2.2-pre.004 2026-08-18 08:09:20 +02:00
c4636ac8b9 v0.2.2-pre.003-fix.001 2026-08-18 07:53:17 +02:00
f15448ff6e v0.2.2-pre.003 2026-08-18 07:31:07 +02:00
bec1f2ec08 v0.2.2-pre.002-fix.002 2026-08-18 07:20:44 +02:00
f625ee5979 v0.2.2-pre.002-fix.001 2026-08-18 07:17:49 +02:00
e68f073505 v0.2.2-pre.002 2026-08-18 07:15:59 +02:00
bffb4f9a31 v0.2.2-pre.001-fix.001 2026-08-18 06:46:59 +02:00
9059a2dc45 v0.2.2-pre.001 2026-08-18 06:41:41 +02:00
6c3ecf1f18 v0.2.1-rel.001 2026-08-17 22:50:56 +02:00
79b67f8eae v0.2.1-pre.007 2026-08-17 22:43:07 +02:00
e0a7ac0bf8 v0.2.1-pre.006-fix.002 2026-08-17 21:41:20 +02:00
598474438b v0.2.1-pre.006-fix.001 2026-08-17 21:16:26 +02:00
f5d98c4e69 v0.2.1-pre.006 2026-08-17 21:03:51 +02:00
14bcbf2cfb v0.2.1-pre.005-fix.001 2026-08-17 20:40:08 +02:00
ac1b1033c4 v0.2.1-pre.005 2026-08-17 20:27:44 +02:00
bc71fba289 v0.2.1-pre.004-fix.002 2026-08-17 19:47:24 +02:00
9c1568ee1c v0.2.1-pre.004-fix.001 2026-08-17 19:40:12 +02:00
c1cea6e813 v0.2.1-pre.004 2026-08-17 19:26:26 +02:00
ed978179d8 v0.2.1-pre.003-fix.001 2026-08-17 18:59:01 +02:00
babe7d9f2b v0.2.1-pre.003 2026-08-17 18:55:14 +02:00
d3fc0c6d69 v0.2.1-pre.002-fix.001 2026-08-17 18:46:52 +02:00
0cff0406ab v0.2.1-pre.002 2026-08-17 18:02:18 +02:00
d98d152f08 v0.2.1-pre.001-fix.001 2026-08-17 17:36:35 +02:00
624202c363 0.2.1-pre.1 2026-08-17 17:20:41 +02:00
620 changed files with 99418 additions and 1322 deletions

View File

@@ -1,12 +1,48 @@
# file: .env.example # file: .env.example
# version: 2 # version: 5
# 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.
KSP_LOGS_DIRECTORY=logs KSP_LOGS_DIRECTORY=logs
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready. # KSP Wallet root directory. Used by config/std.wallet.json before an optional profile subdirectory is appended.
# The committed Wallet document falls back to "wallets" when neither the process environment nor .env defines this variable.
KSP_WALLETS_DIRECTORY=wallets
# 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.
# 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.
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
# 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
# Optional complete private-provider WebSocket endpoint URL used only by the Transport example when explicitly selected.
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
# KSP_SECRET_SOLANA_WS_URL=wss://provider.example/?api-key=replace-me
# 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

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@ yarn.lock
*.swp *.swp
*.swo *.swo
*~ *~
.pydevproject
# Operating system files # Operating system files
.DS_Store .DS_Store

View File

@@ -1,10 +1,44 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 4 --> <!-- version: 11 -->
# Changelog KSP # Changelog KSP
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`. Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
## 0.2.7 — WebSocket Solana standard — 2026-08-23
`0.2.7` stabilise dans `ksp-onchain-transport-lib` le moteur WebSocket Solana standard en complément de la surface HTTP déjà complète. La release couvre exactement les **9 familles subscribe + 9 unsubscribe** de linventaire officiel ciblé : `account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates` et `vote`. Les wrappers sont typés, les IDs KSP de session/subscription restent locaux et stables, les IDs serveur restent internes/remappables, et plusieurs sessions physiques peuvent coexister explicitement sur la même URL sans introduire de pool/scheduler automatique. Les familles `block`, `slotsUpdates` et `vote` restent identifiées comme unstable selon laudit normatif courant et utilisent le warning KSP centralisé.
Le lifecycle WebSocket est borné : actor unique propriétaire du socket, pending JSON-RPC et queues de notifications bornés, `WsSession::close().await`, control frames Ping/Pong/Close, reconnect fini avec backoff borné, resubscribe déterministe par ID local, `continuity_gap_count`, isolation des erreurs applicatives et du backpressure par subscription, nettoyage des late ACK/notifications et terminaison one-shot de `signatureSubscribe`. Config passe à `std.transport` V2 pour composer HTTP + WebSocket tout en gardant la lecture V1 HTTP-only ; la direction reste `Config -> Transport`. La compliance finale conserve simultanément **52 méthodes HTTP courantes + 14 historiques**, les canaries Transport (`309` unit, `36` public API, `24` release completeness), le smoke WebSocket Devnet opt-in `slotSubscribe -> notification -> unsubscribe -> close`, les frontières de dépendances et la redaction des URLs/credentials. `prompts/013-V0_2_8_START_PROMPT.md`, renforcé par `0.2.7-pre.014-fix.001`, ouvre ensuite `0.2.8 — Helius LaserStream WebSocket` uniquement depuis le tag stable `v0.2.7`.
## 0.2.6 — Wallet Desk + `.kspwallet` V2 — 2026-08-22
`0.2.6` stabilise `ksp-app-wallet-desk` comme seconde application Tauri KSP spécialisée et étend `ksp-wallet-lib` avec le wire binaire `.kspwallet` V2. Wallet Desk compose Config, Wallet, Transport HTTP et Logging sans déplacer leurs responsabilités : inventory root-scoped et symlink-safe, création/import V2 par défaut, ouverture VIEW/OWNER V1/V2, candidats secrets résolus exclusivement par Config, `getBalance` Devnet via Transport, administration alias/notes, rotations OWNER/VIEW, disable/recreate VIEW fort et export Solana CLI JSON/Base58. Les secrets, keypairs, handles, chemins complets import/export et credentials Config restent côté Rust ; le frontend ne reçoit que des projections sûres et utilise des modals Bootstrap pour les opérations privilégiées.
Le format V1 JSON reste stable et lisible ; V2 ajoute un framing binaire KSP canonique, des IDs numériques, longueurs big-endian et domains/transcripts distincts tout en conservant Argon2id, XChaCha20-Poly1305 et l'autorité Ed25519 OWNER. Les APIs génériques créent/importent explicitement en V2 et auto-détectent V1/V2 à la lecture, tandis que les variantes `_v1`/`_v2` imposent un format exact et que `DEFAULT_WALLET_FORMAT` reste indépendant de `LATEST_SUPPORTED_WALLET_FORMAT`. La migration V1 -> V2 est explicite, OWNER-authentifiée, no-clobber ou atomique in-place avec protection stale-state ; aucune ouverture ne migre silencieusement. La release ferme également le runtime Tauri distribué : les documents Config/schemas sont embarqués comme resources, `ksp-config-lib` prépare une racine KSP user-writable commune sans écraser les Config utilisateur ni embarquer `.env`, et les bundles Linux `.deb`, `.rpm` et `.AppImage` ont été produits avec succès après le gate final `pre.018-fix.001`. Le prompt `prompts/012-V0_2_7_START_PROMPT.md` ouvre `0.2.7 — WebSocket Solana standard` par lectures, audit officiel, threat-model et sizing avant implémentation lourde.
## 0.2.5 — Wallet foundation — 2026-08-20
`0.2.5` publie `ksp-wallet-lib` et le format natif interopérable `.kspwallet` V1. La release stabilise les capacités cryptographiquement indépendantes VIEW/OWNER, le masquage de la Pubkey/alias/notes à létat verrouillé, Argon2id v19 avec paramètres sérialisés et profil de création `65 536 KiB / 3 / 1`, XChaCha20-Poly1305, une autorité Ed25519 OWNER distincte de la keypair Solana, les compartiments `owner_control`/`metadata`/`secret`, la création et louverture mémoire/fichier, la persistence no-clobber, la signature Solana OWNER sans getter secret, ladministration alias/notes, les rotations OWNER/VIEW, la révocation forte VIEW avec rekey metadata, la détection de handles stale et les adapters Solana CLI JSON/Base58 pour inspection/import/export. Le wire V1 est documenté indépendamment de Rust dans `docs/formats/KSPWALLET_V1.md` et couvert par des vecteurs publics ainsi que des canaris adversariaux et une reproduction externe. La release conserve Wallet indépendant de Config/Transport/ExecutionPolicy/Store/Tauri et expose la Pubkey via `ksp-core-lib` tandis que `solana-keypair` reste encapsulée dans Wallet.
La clôture technique `pre.010-fix.001` à `fix.003` met la dépendance directe KSP à `ed25519-dalek 3.0.0`, accepte la génération `2.2.0` transitive encore imposée par `solana-keypair 3.1.2`, normalise les sources Rust du workspace et introduit `python3 scripts/audit_rust_workspace_rules.py` comme gate structurel complémentaire à rustfmt/Clippy. Le checkpoint final `pre.010-fix.003` est validé avec audit Python clean, `cargo check`, Clippy, tests Wallet et workspace complet verts. Le prompt `prompts/011-V0_2_6_START_PROMPT.md` ouvre ensuite `0.2.6 — Wallet Desk` par audit/sizing avant implémentation.
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.
## 0.2.3 — HTTP Transactions — 2026-08-18
`0.2.3` complète la famille HTTP Transactions de `ksp-onchain-transport-lib` avec 11 wrappers typés, portant la surface HTTP typée courante à 37 méthodes : 4 foundation, 22 Accounts/Tokens/Cluster et 11 Transactions. La release stabilise la classification exacte `8 Read / RetrySafe`, `2 WriteSubmission / NeverAfterDispatch` (`requestAirdrop`, `sendTransaction`) et `1 Simulation / RetrySafe` (`simulateTransaction`), avec preuves end-to-end qu'une write submission n'est jamais resoumise après dispatch ambigu mais peut retenter lorsqu'un échec est prouvé `NotDispatched`. `getTransaction` couvre la config moderne complète et la forme bare encoding legacy dépréciée ; les variantes de transaction, metadata, version et `transactionIndex` restent préservées losslessly. `simulateTransaction` couvre sa config complète, ses invariants déterministes et son résultat riche sans introduire de client RPC Solana haut niveau ni de décodage transactionnel dans Transport. La release formalise `KSP-TRANSPORT-007` et réaudite rétroactivement les 37 wrappers courants sans remédiation fonctionnelle. Les canaries conservent 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et 15 Blocks/Economics réservées à `0.2.4`. Les graphes Cargo et les deux smokes Devnet — Transport pur read-only et composition Config -> Transport transitoire — ont été validés avant publication. Le prompt `prompts/009-V0_2_4_START_PROMPT.md` ouvre `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`.
## 0.2.2 — HTTP Accounts + Tokens + Cluster — 2026-08-18
`0.2.2` complète la surface HTTP typée de `ksp-onchain-transport-lib` avec 22 wrappers supplémentaires : 5 Accounts, 5 Tokens et 12 Cluster, portant la couverture typée totale à 26 méthodes avec les quatre canaris foundation de `0.2.1`. La release stabilise les DTOs/configs wire communs, préserve les formes Account encodées/`jsonParsed`, les `null` et champs optionnels, les selectors Token exclusifs, les contextes RPC, les structures Cluster/leader/vote et les limites locales auditées (`getMultipleAccounts <= 100`, `getProgramAccounts <= 4` filtres, `memcmp` raw <= 128 octets, `getSlotLeaders` entre 1 et 5000). Les 22 descriptors restent `Read / RetrySafe` et passent exclusivement par le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`. La release n'ajoute aucune dépendance externe, ne crée aucun client HTTP parallèle et conserve le firewall Transport -> Config/Store/Program. Elle ajoute un smoke Devnet pur à Transport avec settings programmatiques couvrant Accounts/Tokens/Cluster, conserve séparément le smoke historique Config -> Transport comme exception transitoire, et valide les deux explicitement avant publication. Les canaries figent toujours 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et la partition typed `4 / 22 / 11 / 15`. Le prompt `prompts/008-V0_2_3_START_PROMPT.md` ouvre `0.2.3 — HTTP Transactions`.
## 0.2.1 — HTTP Solana foundation — 2026-08-17
`0.2.1` stabilise `ksp-onchain-transport-lib` comme foundation HTTP JSON-RPC Solana provider-neutral : settings publics, endpoints/pool/rôles, priorités et fairness, RPS/burst/concurrence/cooldown, deadline commune, retry/backoff, classification no-resend après dispatch ambigu, snapshots sûrs et exécution HTTP réelle via `reqwest`/rustls. La release fige un registre audité de 52 méthodes HTTP courantes et 14 méthodes historiques Deprecated/Removed, avec quatre wrappers typés canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) et une partition explicite des 48 méthodes restantes sur `0.2.2``0.2.4`. Elle ajoute `std.transport`, son schema/exemple et l'adapter `ksp-config-lib -> ksp-onchain-transport-lib`, sans dépendance inverse, ainsi que la redaction des URLs/provider credentials, la neutralisation des URLs contenues dans les `reqwest::Error`, un sink Logging Transport dédié à `info`, des fixtures HTTP déterministes, des canaries de complétude et un smoke Devnet opt-in validant la composition Config -> Transport. Le smoke cross-crates reste temporairement hébergé dans Config et doit migrer vers une future surface d'intégration/orchestration ; ce placement n'est pas un modèle pour les futurs smokes `Config + autre crate`. Le prompt `prompts/007-V0_2_2_START_PROMPT.md` ouvre `0.2.2 — HTTP Accounts + Tokens + Cluster`.
## 0.2.0 — Audit bot3 et planification de la série `0.2.x` — 2026-08-17 ## 0.2.0 — Audit bot3 et planification de la série `0.2.x` — 2026-08-17
`0.2.0` stabilise le cadrage de la prochaine phase fonctionnelle de KSP après audit de `khadhroony-bot3`. La release fixe l'ordre `0.2.1+` autour du transport HTTP Solana, du Wallet `.kspwallet`, de Wallet Desk, des transports WebSocket/LaserStream/Yellowstone, du transport off-chain de prix, de `ksp-interface-lib` et de `ksp-program-api`; elle impose la couverture exhaustive des surfaces Transport documentées avec warnings KSP pour les opérations deprecated/obsolete encore fonctionnelles et unstable/experimental. Elle stabilise également la progression durable `RAW -> CORE -> DECODE -> SPECIALIZED`, RAW/CORE sans décodage Program, puis des vertical slices complets par groupe à partir de DECODE, avec priorité Solana Core, SPL token/trading, metadata token, Anchor, Meteora/Raydium/Pump/Orca, routing et Market Desk progressive. Le prompt `prompts/006-V0_2_1_START_PROMPT.md` ouvre `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` avec un gate de sizing imposant qu'une release concrète reste clôturable dans une seule session. `0.2.0` stabilise le cadrage de la prochaine phase fonctionnelle de KSP après audit de `khadhroony-bot3`. La release fixe l'ordre `0.2.1+` autour du transport HTTP Solana, du Wallet `.kspwallet`, de Wallet Desk, des transports WebSocket/LaserStream/Yellowstone, du transport off-chain de prix, de `ksp-interface-lib` et de `ksp-program-api`; elle impose la couverture exhaustive des surfaces Transport documentées avec warnings KSP pour les opérations deprecated/obsolete encore fonctionnelles et unstable/experimental. Elle stabilise également la progression durable `RAW -> CORE -> DECODE -> SPECIALIZED`, RAW/CORE sans décodage Program, puis des vertical slices complets par groupe à partir de DECODE, avec priorité Solana Core, SPL token/trading, metadata token, Anchor, Meteora/Raydium/Pump/Orca, routing et Market Desk progressive. Le prompt `prompts/006-V0_2_1_START_PROMPT.md` ouvre `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` avec un gate de sizing imposant qu'une release concrète reste clôturable dans une seule session.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 95 # version: 216
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-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.0" version = "0.2.7"
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"
@@ -14,20 +14,33 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
publish = false publish = false
[workspace.dependencies] [workspace.dependencies]
argon2 = { version = "^0.5", default-features = false }
chacha20poly1305 = { version = "^0.11", default-features = false }
directories = { version = "^6.0" }
ed25519-dalek = { version = "^3.0", default-features = false }
getrandom = { version = "^0.4", default-features = false }
base64 = { version = "^0.23" }
fs2 = { version = "^0.4" } fs2 = { version = "^0.4" }
serde = { version = "^1.0", features = ["derive"] } futures-util = { version = "^0.3", default-features = false }
serde = { version = "^1.0" }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
jsonschema = { version = "^0.49", default-features = false } jsonschema = { version = "^0.50", default-features = false }
reqwest = { version = "^0.13", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false } solana-pubkey = { version = "^4.3", default-features = false }
tracing = { version = "^0.1", default-features = false, features = ["std"] } tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] } tracing-subscriber = { version = "^0.3", default-features = false }
tracing-appender = { version = "^0.2", default-features = false } tracing-appender = { version = "^0.2", default-features = false }
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time"] } tokio = { version = "^1.53", default-features = false }
chrono = { version = "^0.4", default-features = false, features = ["std", "now"] } tokio-tungstenite = { version = "^0.30", default-features = false }
tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" } tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" } tauri-build = { version = "^2.6" }
tauri-plugin-dialog = { version = "^2.7" }
tauri-plugin-tracing = { version = "^0.3" } tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
zeroize = { version = "^1.9" }
[workspace.lints.rust] [workspace.lints.rust]
missing_docs = "warn" missing_docs = "warn"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 24 --> <!-- version: 82 -->
# Roadmap KSP # Roadmap KSP
@@ -17,6 +17,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- Une prerelease vise environ **15 à 20 minutes de travail effectif**. - Une prerelease vise environ **15 à 20 minutes de travail effectif**.
- Une release concrète doit être dimensionnée pour pouvoir être ouverte et clôturée dans **une seule session de chat**. - Une release concrète doit être dimensionnée pour pouvoir être ouverte et clôturée dans **une seule session de chat**.
- Cette règle fixe une durée maximale par release, pas une obligation de changer de session après chaque release : si une release est clôturée plus vite que prévu, la même session peut ouvrir puis clôturer la release suivante si son propre sizing reste positif et si toutes les frontières version/delta/validation sont conservées.
- Si `pre.001` révèle qu'une release est trop grosse, elle est scindée avant implémentation fonctionnelle lourde. - Si `pre.001` révèle qu'une release est trop grosse, elle est scindée avant implémentation fonctionnelle lourde.
- À partir des couches Program/Decode, KSP progresse verticalement groupe par groupe plutôt que par grandes vagues horizontales de decoders/materializers/executors séparés. - À partir des couches Program/Decode, KSP progresse verticalement groupe par groupe plutôt que par grandes vagues horizontales de decoders/materializers/executors séparés.
@@ -39,24 +40,24 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
### Cadrage ### Cadrage
- [X] `0.2.0` — Audit bot3, ordre de `0.2.x`, architecture durable et prompt `0.2.1` stabilisés par `0.2.0-rel.001`. - [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
- [X] `0.2.0-pre.001` — Méthode d'audit, cartographie initiale et matrice provisoire. - [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2``0.2.4`.
- [X] `0.2.0-pre.002` — Fixer l'ordre fonctionnel, la discipline de sizing, le pipeline RAW/CORE/DECODE/SPECIALIZED et préparer le prompt `0.2.1`.
- [X] `0.2.0-pre.003` — Audit de cohérence final : règles résiduelles supersédées corrigées, fiches `0.2.1+` complétées, TODO bot3 utiles préservés et prompt `0.2.1` finalisé.
- [X] `0.2.0-rel.001` — Publication stable du cadrage `0.2.x`; prochaine release : `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation`.
### Releases fonctionnelles décidées/pressenties ### Releases fonctionnelles décidées/pressenties
- [ ] `0.2.1`Introduire `ksp-onchain-transport-lib` avec HTTP Solana/JSON-RPC, settings publics, document Config standard + adapter, pools, rôles, priorités, limites, retry/backoff et couverture complète de la documentation HTTP ciblée. - [X] `0.2.1`**HTTP transport foundation réduite par le gate `pre.001`** : crate/settings/JSON-RPC/registry 52 current + 14 deprecated historiques, pool/rôles/limites/retry, Config adapter, documentation et 4 méthodes typées canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) publiés stables.
- [ ] `0.2.2`Introduire `ksp-wallet-lib`, le format `.kspwallet`, la gestion sûre des secrets et une architecture d'import/export extensible ; exclure `WalletPolicy`. - [X] `0.2.2`HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
- [ ] `0.2.3`Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet. - [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.
- [ ] `0.2.4`É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. - [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.
- [ ] `0.2.5`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [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`Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [X] `0.2.6``ksp-app-wallet-desk` + `.kspwallet` V2 stables : composition Config/Wallet/HTTP/Logging, lifecycle VIEW/OWNER, balance, administration/import/export, wire binaire V2, APIs multi-version, migration V1 -> V2 explicite et runtime Tauri packagé user-writable validés ; bundles Linux `.deb`/`.rpm`/`.AppImage` produits avant publication. Plan clôturé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
- [ ] `0.2.7`Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [X] `0.2.7`WebSocket Solana standard stable : 9 familles subscribe/unsubscribe typées (18/18 opérations), sessions physiques multiples explicites, subscriptions logiques typées, lifecycle/reconnect/resubscribe/backpressure/shutdown bornés, Config V2, non-régression HTTP 52+14, compliance finale, smoke WebSocket Devnet et audit de dépendances validés ; publication `rel.001` et prompt `0.2.8` prêts.
- [ ] `0.2.8`Introduire une petite application desk de visualisation des prix. - [ ] `0.2.8`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9`Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. - [ ] `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.10` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
- [ ] `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.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.13` — 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

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "default"
}
]
},
{
"profile_id": "temporary",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "temporary"
}
]
},
{
"profile_id": "tests",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "tests"
}
]
}
]
}

View File

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "default"
}
]
},
{
"profile_id": "temporary",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "temporary"
}
]
},
{
"profile_id": "tests",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "tests"
}
]
}
]
}

View File

@@ -0,0 +1,107 @@
{
"format_version": 2,
"retry": {
"max_retries": 3,
"initial_backoff_ms": 150,
"max_backoff_ms": 3000
},
"ws_defaults": {
"command_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 128,
"notification_queue_capacity": 256,
"max_active_subscriptions": 1024,
"max_pending_requests": 128,
"max_message_size_bytes": 67108864,
"max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576
},
"default_profile": "mainnet_mixed",
"profiles": [
{
"profile_id": "mainnet_mixed",
"endpoints": [
{
"name": "mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 200,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "mainnet_private",
"enabled": true,
"provider": "private-provider",
"cluster": "mainnet-beta",
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 10000,
"max_idle_connections_per_host": 16,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 20,
"burst_capacity": 40,
"max_concurrent_requests": 16,
"pause_after_rate_limit_ms": 750
}
}
]
}
],
"ws_endpoints": [
{
"name": "mainnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
},
{
"name": "mainnet_private_ws",
"enabled": true,
"provider": "private-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_SECRET_SOLANA_WS_URL:-wss://example.invalid}",
"session": {
"notification_queue_capacity": 512,
"max_active_subscriptions": 2048
}
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"format_version": 1,
"wallets_directory": "${KSP_WALLETS_DIRECTORY:-wallets}",
"default_profile": "tests",
"profiles": [
{
"profile_id": "default"
},
{
"profile_id": "scenarios",
"wallets_subdirectory": "scenarios/ephemeral"
},
{
"profile_id": "tests",
"wallets_subdirectory": "tests"
}
]
}

View File

@@ -0,0 +1,459 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.transport:v2",
"title": "KSP standard HTTP + WebSocket Transport configuration",
"oneOf": [
{
"$ref": "#/$defs/documentV1"
},
{
"$ref": "#/$defs/documentV2"
}
],
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"descriptor": {
"type": "string",
"minLength": 1,
"pattern": "^\\S(?:.*\\S)?$"
},
"positiveMs": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"positiveU32": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"positiveUsize": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"retry": {
"type": "object",
"additionalProperties": false,
"required": [
"max_retries",
"initial_backoff_ms",
"max_backoff_ms"
],
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"limits": {
"type": "object",
"additionalProperties": false,
"properties": {
"requests_per_second": {
"$ref": "#/$defs/positiveU32"
},
"burst_capacity": {
"$ref": "#/$defs/positiveU32"
},
"max_concurrent_requests": {
"$ref": "#/$defs/positiveU32"
},
"pause_after_rate_limit_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"role": {
"type": "object",
"additionalProperties": false,
"required": [
"role",
"enabled",
"request_kinds",
"priority",
"limits"
],
"properties": {
"role": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"request_kinds": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"$ref": "#/$defs/descriptor"
},
"allOf": [
{
"if": {
"contains": {
"const": "*"
}
},
"then": {
"maxItems": 1
}
}
]
},
"priority": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"limits": {
"$ref": "#/$defs/limits"
}
}
},
"httpEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"url",
"connect_timeout_ms",
"request_timeout_ms",
"roles"
],
"properties": {
"name": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"provider": {
"$ref": "#/$defs/descriptor"
},
"cluster": {
"$ref": "#/$defs/descriptor"
},
"url": {
"type": "string",
"minLength": 1
},
"connect_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"request_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_idle_connections_per_host": {
"type": "integer",
"minimum": 1
},
"roles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/role"
}
}
}
},
"wsReconnect": {
"type": "object",
"additionalProperties": false,
"required": [
"max_retries",
"initial_backoff_ms",
"max_backoff_ms"
],
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"wsReconnectOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"wsSession": {
"type": "object",
"additionalProperties": false,
"required": [
"command_timeout_ms",
"close_timeout_ms",
"reconnect",
"resubscribe",
"command_queue_capacity",
"notification_queue_capacity",
"max_active_subscriptions",
"max_pending_requests",
"max_message_size_bytes",
"max_frame_size_bytes",
"max_write_buffer_size_bytes"
],
"properties": {
"command_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"reconnect": {
"$ref": "#/$defs/wsReconnect"
},
"resubscribe": {
"enum": [
"never",
"active_subscriptions"
]
},
"command_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"notification_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"max_active_subscriptions": {
"$ref": "#/$defs/positiveUsize"
},
"max_pending_requests": {
"$ref": "#/$defs/positiveUsize"
},
"max_message_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_frame_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_write_buffer_size_bytes": {
"$ref": "#/$defs/positiveUsize"
}
}
},
"wsSessionOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"command_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"reconnect": {
"$ref": "#/$defs/wsReconnectOverride"
},
"resubscribe": {
"enum": [
"never",
"active_subscriptions"
]
},
"command_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"notification_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"max_active_subscriptions": {
"$ref": "#/$defs/positiveUsize"
},
"max_pending_requests": {
"$ref": "#/$defs/positiveUsize"
},
"max_message_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_frame_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_write_buffer_size_bytes": {
"$ref": "#/$defs/positiveUsize"
}
}
},
"wsEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"kind",
"url"
],
"properties": {
"name": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"provider": {
"$ref": "#/$defs/descriptor"
},
"cluster": {
"$ref": "#/$defs/descriptor"
},
"kind": {
"enum": [
"solana_standard"
]
},
"url": {
"type": "string",
"minLength": 1
},
"session": {
"$ref": "#/$defs/wsSessionOverride"
}
}
},
"profileV1": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"endpoints"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/httpEndpoint"
}
}
}
},
"profileV2": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"endpoints",
"ws_endpoints"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/httpEndpoint"
}
},
"ws_endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/wsEndpoint"
}
}
}
},
"documentV1": {
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"retry",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 1
},
"retry": {
"$ref": "#/$defs/retry"
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profileV1"
}
}
}
},
"documentV2": {
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"retry",
"ws_defaults",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 2
},
"retry": {
"$ref": "#/$defs/retry"
},
"ws_defaults": {
"$ref": "#/$defs/wsSession"
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profileV2"
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.wallet:v1",
"title": "KSP standard Wallet configuration",
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"wallets_directory",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 1
},
"wallets_directory": {
"type": "string",
"minLength": 1
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profile"
}
}
},
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"wallets_subdirectory": {
"type": "string",
"minLength": 1
}
}
}
}
}

View File

@@ -3,6 +3,156 @@
"logs_directory": "${KSP_LOGS_DIRECTORY:-logs}", "logs_directory": "${KSP_LOGS_DIRECTORY:-logs}",
"default_profile": "local_dev", "default_profile": "local_dev",
"profiles": [ "profiles": [
{
"profile_id": "console_error",
"default_filter": "error",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "error",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [],
"target_filters": []
},
{
"profile_id": "console_warn",
"default_filter": "warn",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "warn",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [],
"target_filters": []
},
{
"profile_id": "console_info",
"default_filter": "info",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "info",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [],
"target_filters": []
},
{
"profile_id": "console_debug",
"default_filter": "debug",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "debug",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [],
"target_filters": []
},
{
"profile_id": "console_trace",
"default_filter": "trace",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "trace",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [],
"target_filters": []
},
{
"profile_id": "file_info",
"default_filter": "info",
"span_events": "new_and_close",
"console": {
"enabled": false,
"output": "stderr",
"ansi": false,
"format": "compact",
"filter": {
"level": "off",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [
{
"output_id": "file.all.info",
"enabled": true,
"path": "info/ksp-info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "info",
"targets": [
"*"
],
"domains": [
"*"
]
}
}
],
"target_filters": []
},
{ {
"profile_id": "local_dev", "profile_id": "local_dev",
"default_filter": "warn", "default_filter": "warn",
@@ -40,6 +190,23 @@
] ]
} }
}, },
{
"output_id": "file.onchain_transport.info",
"enabled": true,
"path": "transport/onchain/ksp-onchain-transport.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "info",
"targets": [
"ksp-onchain-transport-lib"
],
"domains": [
"*"
]
}
},
{ {
"output_id": "file.config.error", "output_id": "file.config.error",
"enabled": true, "enabled": true,
@@ -70,8 +237,296 @@
{ {
"target_prefix": "ksp-app-config-desk", "target_prefix": "ksp-app-config-desk",
"level": "info" "level": "info"
},
{
"target_prefix": "ksp-onchain-transport-lib",
"level": "info"
} }
] ]
},
{
"profile_id": "superdev",
"default_filter": "debug",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "debug",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [
{
"output_id": "file.ksp_app_config_desk.debug",
"enabled": true,
"path": "targets/debug/ksp-app-config-desk.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-app-config-desk"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_app_wallet_desk.debug",
"enabled": true,
"path": "targets/debug/ksp-app-wallet-desk.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-app-wallet-desk"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_config_lib.debug",
"enabled": true,
"path": "targets/debug/ksp-config-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-config-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_core_lib.debug",
"enabled": true,
"path": "targets/debug/ksp-core-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-core-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_logging_lib.debug",
"enabled": true,
"path": "targets/debug/ksp-logging-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-logging-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_onchain_transport_lib.debug",
"enabled": true,
"path": "targets/debug/ksp-onchain-transport-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-onchain-transport-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_wallet_lib.debug",
"enabled": true,
"path": "targets/debug/ksp-wallet-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"ksp-wallet-lib"
],
"domains": [
"*"
]
}
}
],
"target_filters": []
},
{
"profile_id": "supertrace",
"default_filter": "trace",
"span_events": "new_and_close",
"console": {
"enabled": true,
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "debug",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [
{
"output_id": "file.ksp_app_config_desk.trace",
"enabled": true,
"path": "targets/trace/ksp-app-config-desk.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-app-config-desk"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_app_wallet_desk.trace",
"enabled": true,
"path": "targets/trace/ksp-app-wallet-desk.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-app-wallet-desk"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_config_lib.trace",
"enabled": true,
"path": "targets/trace/ksp-config-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-config-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_core_lib.trace",
"enabled": true,
"path": "targets/trace/ksp-core-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-core-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_logging_lib.trace",
"enabled": true,
"path": "targets/trace/ksp-logging-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-logging-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_onchain_transport_lib.trace",
"enabled": true,
"path": "targets/trace/ksp-onchain-transport-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-onchain-transport-lib"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.ksp_wallet_lib.trace",
"enabled": true,
"path": "targets/trace/ksp-wallet-lib.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "trace",
"targets": [
"ksp-wallet-lib"
],
"domains": [
"*"
]
}
}
],
"target_filters": []
} }
] ]
} }

110
config/std.transport.json Normal file
View File

@@ -0,0 +1,110 @@
{
"format_version": 2,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"ws_defaults": {
"command_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 128,
"notification_queue_capacity": 256,
"max_active_subscriptions": 1024,
"max_pending_requests": 128,
"max_message_size_bytes": 67108864,
"max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576
},
"default_profile": "devnet_public",
"profiles": [
{
"profile_id": "devnet_public",
"endpoints": [
{
"name": "solana_devnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_devnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
}
]
},
{
"profile_id": "mainnet_public",
"endpoints": [
{
"name": "solana_mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_mainnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
}
]
}
]
}

18
config/std.wallet.json Normal file
View File

@@ -0,0 +1,18 @@
{
"format_version": 1,
"wallets_directory": "${KSP_WALLETS_DIRECTORY:-wallets}",
"default_profile": "default",
"profiles": [
{
"profile_id": "default"
},
{
"profile_id": "temporary",
"wallets_subdirectory": "temporary"
},
{
"profile_id": "tests",
"wallets_subdirectory": "tests"
}
]
}

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-config-desk/Cargo.toml # file: crates/ksp-app-config-desk/Cargo.toml
# version: 7 # version: 8
[package] [package]
name = "ksp-app-config-desk" name = "ksp-app-config-desk"
@@ -26,12 +26,12 @@ fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" } ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-logging-lib = { path = "../ksp-logging-lib" }
serde.workspace = true serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
tauri.workspace = true tauri.workspace = true
tauri-plugin-tracing.workspace = true tauri-plugin-tracing.workspace = true
chrono.workspace = true chrono = { workspace = true, features = ["std", "now"] }
tokio.workspace = true tokio = { workspace = true, features = ["time"] }
ts-rs.workspace = true ts-rs.workspace = true
[lints] [lints]

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: 28 -->
# `ksp-app-config-desk` # `ksp-app-config-desk`
@@ -78,11 +78,11 @@ Les DTO Rust applicatifs restent la source de vérité et les bindings généré
## Robustesse desktop et extensibilité ## Robustesse desktop et extensibilité
`pre.018` rend le fallback de bootstrap Logging testable avant installation du subscriber : la résolution produit d'abord un plan `managed` ou `fallback`, puis seulement l'initialisation runtime est tentée. Une source Logging invalide peut ainsi être couverte par test sans installer un subscriber global dans le processus de tests. Le fallback reste transitoire, console-only, niveau `Info`, sans file sink. Le fallback de bootstrap Logging est testable avant installation du subscriber : la résolution produit d'abord un plan `managed` ou `fallback`, puis seulement l'initialisation runtime est tentée. Une source Logging invalide peut ainsi être couverte par test sans installer un subscriber global dans le processus de tests. Le fallback reste transitoire, console-only, niveau `Info`, sans file sink.
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
@@ -109,8 +109,7 @@ Le port HTTP est strict. Les futures applications desk utilisent les couples sui
Les bibliothèques consommées par le bundle applicatif sont déclarées sous `dependencies` : `@fltsci/tauri-plugin-tracing`, `@fortawesome/fontawesome-free`, `@tauri-apps/api`, `bootstrap`, `simplebar` et `resize-observer-polyfill`. Les bibliothèques consommées par le bundle applicatif sont déclarées sous `dependencies` : `@fltsci/tauri-plugin-tracing`, `@fortawesome/fontawesome-free`, `@tauri-apps/api`, `bootstrap`, `simplebar` et `resize-observer-polyfill`.
Les outils de compilation/développement et déclarations de types appartiennent à `devDependencies`, notamment `@tauri-apps/cli`, `@types/bootstrap`, `@types/node`, `sass-embedded`, `typescript` et `vite`. Les commandes npm directes sont réservées à l'installation ou à la mise à jour de ces dépendances ; le cycle normal de développement et de build est lancé depuis la racine du workspace par Tauri avec la configuration explicite de l'application. Les outils de compilation/développement et déclarations de types appartiennent à `devDependencies`, notamment `@tauri-apps/cli`, `@types/bootstrap`, `@types/node`, `sass-embedded`, `typescript` et `vite`. Les commandes npm directes sont réservées à l'installation ou à la mise à jour de ces dépendances. Le cycle normal de développement et de build est **crate-local** : l'opérateur peut rester positionné à la racine du workspace, mais la commande exécute d'abord `cd crates/ksp-app-config-desk` puis `cargo tauri dev`/`build`. `-c/--config` n'est pas utilisé comme sélecteur de crate.
## Logging frontend KSP ## Logging frontend KSP
@@ -124,36 +123,42 @@ main -> ksp-app-config-desk.frontend.main
splash -> ksp-app-config-desk.frontend.splash splash -> ksp-app-config-desk.frontend.splash
``` ```
Rust valide le niveau et le `targetId`, choisit un callsite statique puis émet exclusivement via les macros de `ksp-logging-lib`. Le package applicatif n'importe pas directement `tracing`. Le pont actuel garantit donc le trajet WebView -> Rust tout en conservant l'affichage local des appels `console.*` dans la console WebKit. Le retour général Rust -> console WebKit est volontairement hors périmètre de `0.1.4` : une future intégration devra passer par une couche possédée par `ksp-logging-lib`, sans second subscriber, double émission ni boucle avec le bridge KSP. Le panneau **Test Logging** complète désormais ce bridge : il peut émettre des événements backend via `ksp-logging-lib` avec target KSP statique et domain contrôlé, ou réutiliser le bridge frontend existant avec son target/domain fixes. Rust valide le niveau et le `targetId`, choisit un callsite statique puis émet exclusivement via les macros de `ksp-logging-lib`. Le package applicatif n'importe pas directement `tracing`. Le pont actuel garantit donc le trajet WebView -> Rust tout en conservant l'affichage local des appels `console.*` dans la console WebKit. Le retour général Rust -> console WebKit reste volontairement hors de la surface actuelle : une future intégration devra passer par une couche possédée par `ksp-logging-lib`, sans second subscriber, double émission ni boucle avec le bridge KSP. Le panneau **Test Logging** complète désormais ce bridge : il peut émettre des événements backend via `ksp-logging-lib` avec target KSP statique et domain contrôlé, ou réutiliser le bridge frontend existant avec son target/domain fixes.
## Panneau Test Logging ## Panneau Test Logging
`pre.017` ajoute une surface de validation volontairement explicite. Le champ **Message** est le contenu qui sera réellement journalisé ; il ne doit donc jamais recevoir de Secret. **Log backend** appelle `emit_logging_test`, qui valide le niveau (`trace/debug/info/warn/error/tous`), choisit un target statique (`ksp-app-config-desk` ou `ksp-app-config-desk.logging-test`) et applique un domain absent, connu ou personnalisé borné avant d'émettre exclusivement avec les macros `ksp-logging-lib`. Le résultat retourne uniquement des métadonnées sûres : nombre d'événements, niveau demandé, target/domain effectifs et génération runtime. Le panneau fournit une surface de validation volontairement explicite. Le champ **Message** est le contenu qui sera réellement journalisé ; il ne doit donc jamais recevoir de Secret. **Log backend** appelle `emit_logging_test`, qui valide le niveau (`trace/debug/info/warn/error/tous`), choisit un target statique (`ksp-app-config-desk` ou `ksp-app-config-desk.logging-test`) et applique un domain absent, connu ou personnalisé borné avant d'émettre exclusivement avec les macros `ksp-logging-lib`. Le résultat retourne uniquement des métadonnées sûres : nombre d'événements, niveau demandé, target/domain effectifs et génération runtime.
**Log via bridge frontend** réutilise le bridge déjà installé. Son contrat reste volontairement fixe : `target=ksp-app-config-desk.frontend.main`, `domain=frontend`. Cela permet de comparer dans le même panneau le routing backend et le chemin WebView -> Rust -> `ksp-logging-lib`, avant et après modification/hot reload des filtres. **Log via bridge frontend** réutilise le bridge déjà installé. Son contrat reste volontairement fixe : `target=ksp-app-config-desk.frontend.main`, `domain=frontend`. Cela permet de comparer dans le même panneau le routing backend et le chemin WebView -> Rust -> `ksp-logging-lib`, avant et après modification/hot reload des filtres.
## 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`.
Le `LoggingGuard` est conservé dans `AppState` et sert désormais au hot reload transactionnel du runtime Logging après sauvegarde. Le `LoggingGuard` est conservé dans `AppState` et sert désormais au hot reload transactionnel du runtime Logging après sauvegarde.
## Runtime packagé
Le parcours release ne dépend pas du checkout source ni du répertoire depuis lequel le binaire est lancé. Les documents Config et schemas enregistrés sont embarqués comme resources Tauri. Avant `AppState::initialize`, `tauri.rs` résout le répertoire de resources puis délègue à `ksp-config-lib::prepare_packaged_runtime`. Config résout un répertoire de données utilisateur KSP writable via `ProjectDirs`, y seed les documents Config absents et y synchronise les schemas possédés par le package courant. Le process adopte ensuite ce répertoire writable comme current working directory.
Les documents Config déjà présents restent user-managed et ne sont jamais écrasés silencieusement lors d'une mise à jour. Les schemas sont resynchronisés à chaque lancement packagé. Le `.env` n'est jamais embarqué ni seedé ; les chemins relatifs `logs/` et `wallets/` restent ancrés dans ce runtime partagé sauf override Config. Config Desk et Wallet Desk utilisent le même identifiant de projet KSP afin de partager cette racine runtime sans dupliquer l'ownership des ressources.
## Lifecycle des fenêtres ## Lifecycle des fenêtres
`tw_splash.rs` valide l'origine de `splash_frontend_ready`, ignore les readiness dupliquées et orchestre le passage à `main`. Les timings sont résolus par `ksp-config-lib` depuis : `tw_splash.rs` valide l'origine de `splash_frontend_ready`, ignore les readiness dupliquées et orchestre le passage à `main`. Les timings sont résolus par `ksp-config-lib` depuis :
```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.
Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — <vue active>` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant un brouillon typé éditable, sa persistence atomique et lapplication runtime immédiate par hot reload. Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — <vue active>` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant un brouillon typé éditable, sa persistence atomique et lapplication runtime immédiate par hot reload.
## Panneau Profils ## Panneau Profils
La vue **Profils** inspecte les documents validés qui exposent le contrat standard `default_profile` / `profiles`. Elle ne revalide ni ne fusionne les valeurs dans le frontend : le backend utilise `ConfigDocumentEngine::load_resolved_profile()` puis `ResolvedConfigProfile::resolve_effective_environment_detailed()`. La vue **Profils** inspecte les documents validés qui exposent le contrat standard `default_profile` / `profiles`. Elle ne revalide ni ne fusionne les valeurs dans le frontend : le backend utilise `ConfigDocumentEngine::load_resolved_profile()` puis `ResolvedConfigProfile::resolve_effective_environment_detailed()`.
@@ -166,13 +171,13 @@ La vue **Logging** charge le document standard exclusivement avec `ConfigManagem
Le panneau expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. Le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée. Le panneau expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. Le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée.
`pre.016` distingue en plus le **profil default persistant** du **profil runtime actif**. La section **Runtime actif** expose le profil actuellement appliqué, `selection_source` (`default_profile`, `explicit` ou `fallback`), la génération, l'état console, les compteurs de lignes abandonnées et les file sinks réellement actifs. Un profil déjà persisté peut être appliqué explicitement sans modifier `default_profile` ni écrire le document ; cette action est désactivée tant que le brouillon contient des changements non sauvegardés. Le panneau distingue le **profil default persistant** du **profil runtime actif**. La section **Runtime actif** expose le profil actuellement appliqué, `selection_source` (`default_profile`, `explicit` ou `fallback`), la génération, l'état console, les compteurs de lignes abandonnées et les file sinks réellement actifs. Un profil déjà persisté peut être appliqué explicitement sans modifier `default_profile` ni écrire le document ; cette action est désactivée tant que le brouillon contient des changements non sauvegardés.
Chaque lancement de Config Desk crée aussi une `LoggingRuntimeIdentity` stable : `application_id` + timestamp UTC de démarrage + PID. `ksp-logging-lib` utilise cette identité pour préfixer les noms des fichiers actifs et la conserve pendant tous les hot reloads du même processus. Deux lancements distincts ne partagent donc plus le même fichier persistant, même avec une rotation `daily`. Avec la configuration de release `info/ksp-info.log`, un prefix effectif peut être `ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-info.log`. Le path Config reste inchangé ; l'identité appartient au runtime, pas au document source. Chaque lancement de Config Desk crée aussi une `LoggingRuntimeIdentity` stable : `application_id` + timestamp UTC de démarrage + PID. `ksp-logging-lib` utilise cette identité pour préfixer les noms des fichiers actifs et la conserve pendant tous les hot reloads du même processus. Deux lancements distincts ne partagent donc plus le même fichier persistant, même avec une rotation `daily`. Avec la configuration de release `info/ksp-info.log`, un prefix effectif peut être `ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-info.log`. Le path Config reste inchangé ; l'identité appartient au runtime, pas au document source.
## Baseline Logging de release ## Baseline Logging de release
La configuration canonique livrée avec `0.1.4` revient à une baseline opératoire `info` conformément à KSP-APP-031 : console `info`, sink général `info/ksp-info.log` au niveau `info`, et overrides `ksp-config-lib`, `ksp-logging-lib`, `ksp-app-config-desk` à `info`. Le `default_filter` reste `warn` pour les autres targets KSP. Les niveaux `debug`/`trace` restent disponibles et peuvent être remontés temporairement depuis Config Desk lors dun développement ou diagnostic, puis redescendus avant la release suivante. La configuration canonique de release utilise une baseline opératoire `info` conformément à KSP-APP-031 : console `info`, sink général `info/ksp-info.log` au niveau `info`, et overrides `ksp-config-lib`, `ksp-logging-lib`, `ksp-app-config-desk` à `info`. Le `default_filter` reste `warn` pour les autres targets KSP. Les niveaux `debug`/`trace` restent disponibles et peuvent être remontés temporairement depuis Config Desk lors dun développement ou diagnostic, puis redescendus avant la release suivante.
## Traçabilité frontend ## Traçabilité frontend
@@ -182,7 +187,6 @@ Les actions utilisateur significatives et changements d'état sont journalisés
Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle du splash est désormais actif et utilise cette police pendant la transition vers la fenêtre principale. Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle du splash est désormais actif et utilise cette police pendant la transition vers la fenêtre principale.
## Panneau Environnement / `.env` ## Panneau Environnement / `.env`
La vue **Environnement / `.env`** expose la projection sûre de `ConfigManagement::environment_report()` puis une zone de management qui appelle exclusivement `ConfigManagement::set_dotenv_value()` et `ConfigManagement::remove_dotenv_value()`. Elle ne lit jamais directement le process environment ni le fichier `.env`. Pour chaque variable KSP/KSPB actuellement présente dans le process ou `.env`, le frontend reçoit uniquement : nom, namespace, sensibilité, valeur desired sûre, valeur effective sûre, source effective et indicateur de shadowing. La vue **Environnement / `.env`** expose la projection sûre de `ConfigManagement::environment_report()` puis une zone de management qui appelle exclusivement `ConfigManagement::set_dotenv_value()` et `ConfigManagement::remove_dotenv_value()`. Elle ne lit jamais directement le process environment ni le fichier `.env`. Pour chaque variable KSP/KSPB actuellement présente dans le process ou `.env`, le frontend reçoit uniquement : nom, namespace, sensibilité, valeur desired sûre, valeur effective sûre, source effective et indicateur de shadowing.

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: 28 -->
# Utilisation de `ksp-app-config-desk` # Utilisation de `ksp-app-config-desk`
@@ -24,14 +24,14 @@ npm i -D
cd ../../ cd ../../
``` ```
`npm i -D` sans nom de package installe les dépendances déclarées du package ; la classification `dependencies` / `devDependencies` reste celle de `package.json`. Les lockfiles frontend restent ignorés et ne sont pas versionnés. Après cette installation, les commandes Cargo/Tauri sont exécutées depuis la racine du workspace. `npm i -D` sans nom de package installe les dépendances déclarées du package ; la classification `dependencies` / `devDependencies` reste celle de `package.json`. Les lockfiles frontend restent ignorés et ne sont pas versionnés. Après cette installation, l'opérateur peut revenir à la racine du workspace, mais chaque commande Tauri entre explicitement dans la crate ciblée avant d'exécuter le CLI.
## Développement normal ## Développement normal
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 : `-c/--config` ne sélectionne pas une crate Tauri. Depuis la racine du workspace, la crate applicative est donc sélectionnée en changeant explicitement de répertoire :
```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 :
@@ -85,7 +85,7 @@ splash
Ils sont convertis côté Rust vers des targets KSP statiques `ksp-app-config-desk.frontend*` et émis uniquement via `ksp-logging-lib`. Un niveau différent de `trace`, `debug`, `info`, `warn` ou `error`, ou un `targetId` non whitelisté, est rejeté avec un `CommandErrorDto` sûr. Ils sont convertis côté Rust vers des targets KSP statiques `ksp-app-config-desk.frontend*` et émis uniquement via `ksp-logging-lib`. Un niveau différent de `trace`, `debug`, `info`, `warn` ou `error`, ou un `targetId` non whitelisté, est rejeté avec un `CommandErrorDto` sûr.
`main.ts` et `splash.ts` installent aussi le bridge `console.*`; l'échec éventuel d'un `invoke` est écrit uniquement sur la console WebView originale afin d'éviter une boucle de logging. Ce bridge conserve les messages JavaScript dans la console WebKit et les transmet vers Rust. Le retour général Rust -> console WebKit est reporté hors `0.1.4`; il devra être ajouté ultérieurement sous ownership de `ksp-logging-lib`, sans installer de subscriber Tauri parallèle ni créer de boucle avec ce bridge. `main.ts` et `splash.ts` installent aussi le bridge `console.*`; l'échec éventuel d'un `invoke` est écrit uniquement sur la console WebView originale afin d'éviter une boucle de logging. Ce bridge conserve les messages JavaScript dans la console WebKit et les transmet vers Rust. Le retour général Rust -> console WebKit reste hors de la surface actuelle ; il devra être ajouté ultérieurement sous ownership de `ksp-logging-lib`, sans installer de subscriber Tauri parallèle ni créer de boucle avec ce bridge.
## Bindings TS-RS ## Bindings TS-RS
@@ -97,11 +97,16 @@ frontend/ts/bindings/ksp_app_config_desk/...
Les bindings sont générés au premier DTO Tauri réel ; aucune structure factice n'est ajoutée uniquement pour créer le répertoire. Les bindings sont générés au premier DTO Tauri réel ; aucune structure factice n'est ajoutée uniquement pour créer le répertoire.
## Bootstrap backend ## Bootstrap backend
Les arguments `--cfgpath`, `--schemapath` et `--filemap=...` sont transmis tels quels à `ksp-config-lib`. En build debug, le launcher replace le current working directory Rust à la racine du workspace avant ce bootstrap afin que les defaults relatifs `config/`, `config/schemas/` et `.env` désignent les ressources racine même lorsque Tauri lance `cargo run` depuis la crate de l'application. Le launcher ne lit pas ces ressources lui-même. Le profil Logging initial est le `default_profile` de `std.logging.json`. Les arguments `--cfgpath`, `--schemapath` et `--filemap=...` sont transmis tels quels à `ksp-config-lib`. En build debug, le launcher replace le current working directory Rust à la racine du workspace avant ce bootstrap afin que les defaults relatifs `config/`, `config/schemas/` et `.env` désignent les ressources racine même lorsque Tauri lance `cargo run` depuis la crate de l'application. Le launcher ne lit pas ces ressources lui-même. Le profil Logging initial est le `default_profile` de `std.logging.json`.
### Runtime packagé
En release, le CWD de développement n'est pas réutilisé. Les resources Tauri embarquent les documents Config et schemas enregistrés ; `ksp-config-lib::prepare_packaged_runtime` prépare une racine de données utilisateur KSP writable résolue par `ProjectDirs`, seed les Config uniquement lorsqu'elles sont absentes et synchronise les schemas depuis le package courant. `tauri.rs` active ensuite cette racine comme current working directory avant le bootstrap Config.
Le `.env` n'est pas inclus dans le bundle. Les Config existantes restent intactes lors d'une mise à jour et les chemins relatifs de logs/wallets sont résolus depuis la racine runtime writable, sauf configuration explicite contraire. Le chemin physique dépend de la plateforme et ne doit pas être codé en dur par l'application ou l'utilisateur.
Si cette configuration ne peut pas être utilisée, l'application doit rester démarrable pour permettre sa réparation : elle utilise alors un fallback Logging console/stderr en mémoire. Le fallback n'écrit aucun fichier de configuration et n'écrase aucune valeur utilisateur. Si cette configuration ne peut pas être utilisée, l'application doit rester démarrable pour permettre sa réparation : elle utilise alors un fallback Logging console/stderr en mémoire. Le fallback n'écrit aucun fichier de configuration et n'écrase aucune valeur utilisateur.
La commande Tauri `get_app_snapshot` expose un état sûr du bootstrap. Ses types TypeScript sont générés par `cargo test -p ksp-app-config-desk` sous `frontend/ts/bindings/`. La commande Tauri `get_app_snapshot` expose un état sûr du bootstrap. Ses types TypeScript sont générés par `cargo test -p ksp-app-config-desk` sous `frontend/ts/bindings/`.
@@ -112,12 +117,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
@@ -154,12 +161,10 @@ La sélection charge le source brut via `ConfigManagement::read_source()`, y com
Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source. Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source.
### Ouvrir un éditeur spécialisé depuis Documents ### Ouvrir un éditeur spécialisé depuis Documents
Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton. Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton.
## Panneau Profils ## Panneau Profils
La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné : La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné :
@@ -173,7 +178,6 @@ La vue **Profils** charge l'inventaire des documents Config validés qui possèd
Avec le `std.logging.json` actuel et `KSP_LOGS_DIRECTORY` défini dans `.env`, la provenance attendue pour `/logs_directory` est `KSP_LOGS_DIRECTORY`, source `dotenv`, sensibilité `Internal`. Le contenu des vues JSON n'est pas recopié dans les logs ; seuls les identifiants de document/profil et métadonnées de résolution sont tracés. Avec le `std.logging.json` actuel et `KSP_LOGS_DIRECTORY` défini dans `.env`, la provenance attendue pour `/logs_directory` est `KSP_LOGS_DIRECTORY`, source `dotenv`, sensibilité `Internal`. Le contenu des vues JSON n'est pas recopié dans les logs ; seuls les identifiants de document/profil et métadonnées de résolution sont tracés.
## Panneau Environnement / `.env` ## Panneau Environnement / `.env`
La vue **Environnement / `.env`** charge `get_environment_report`, qui projette exclusivement `ConfigManagement::environment_report()`. La table affiche : La vue **Environnement / `.env`** charge `get_environment_report`, qui projette exclusivement `ConfigManagement::environment_report()`. La table affiche :
@@ -217,8 +221,6 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document
Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`. Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`.
## Test Logging — routing backend et bridge frontend ## Test Logging — routing backend et bridge frontend
La section **Test Logging — routing contrôlé** utilise un message explicitement destiné aux logs. Ne jamais y copier une valeur `KSP_SECRET_*` / `KSPB_SECRET_*`. La section **Test Logging — routing contrôlé** utilise un message explicitement destiné aux logs. Ne jamais y copier une valeur `KSP_SECRET_*` / `KSPB_SECRET_*`.
@@ -285,7 +287,7 @@ Si la résolution ou la préparation du profil explicite échoue, `ksp_logging_l
## Baseline Logging de release ## Baseline Logging de release
Après les tests `debug`/`trace`, la configuration canonique `0.1.4` revient à : Après les tests `debug`/`trace`, la configuration canonique revient à :
```text ```text
default_filter = warn default_filter = warn
@@ -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: 18 -->
<!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 flex-nowrap">
<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: 7
$app-header-height: 72px; $app-header-height: 72px;
$app-footer-height: 42px; $app-footer-height: 42px;
$app-sidebar-width: 250px;
html, html,
body { body {
@@ -40,6 +41,11 @@ body {
overflow: hidden; overflow: hidden;
} }
.app-main > .row {
flex-wrap: nowrap;
min-width: 0;
}
.app-scrollable { .app-scrollable {
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;
@@ -59,13 +65,23 @@ 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-content {
min-width: 0;
}
.app-sidebar .nav-link {
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss // file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss
// version: 1 // version: 2
// Pulse 5.3.8 // Pulse 5.3.8
// Bootswatch // Bootswatch

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss // file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss
// version: 1 // version: 2
//@use '@fortawesome/fontawesome-free/scss/variables' with ( //@use '@fortawesome/fontawesome-free/scss/variables' with (
// // customizing $font-path - make sure it points to where your webfonts are stored in your project // // customizing $font-path - make sure it points to where your webfonts are stored in your project

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss // file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss
// version: 1 // version: 2
/* Rtl support */ /* Rtl support */
[data-simplebar] { [data-simplebar] {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_variables.scss // file: crates/ksp-app-config-desk/frontend/sass/_variables.scss
// version: 1 // version: 2
// Pulse 5.3.8 // Pulse 5.3.8
// Bootswatch // Bootswatch

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: 5
@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: 100%;
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",
"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,17 +1,8 @@
// file: crates/ksp-app-config-desk/src/app_state.rs // file: crates/ksp-app-config-desk/src/app_state.rs
// version: 6 // version: 9
//! Shared backend state owned by the Tauri application. //! Shared backend state owned by the Tauri application.
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
/// Shared Config Desk application state managed by Tauri. /// Shared Config Desk application state managed by Tauri.
pub(crate) struct AppState { pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement, config_management: ksp_config_lib::ConfigManagement,
@@ -28,7 +19,7 @@ impl AppState {
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 runtime_identity = crate::logging_runtime::launch_identity(); let runtime_identity = crate::launch_identity();
let runtime_identity = match runtime_identity { let runtime_identity = match runtime_identity {
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),
@@ -57,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"
); );
@@ -175,7 +168,7 @@ impl AppState {
let dropped = runtime.guard.dropped_lines(); let dropped = runtime.guard.dropped_lines();
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new(); let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
for metadata in runtime.guard.active_file_outputs() { for metadata in runtime.guard.active_file_outputs() {
files.push(crate::logging_runtime::project_file(&metadata)); files.push(crate::project_logging_file(&metadata));
} }
return std::result::Result::Ok(crate::LoggingRuntimeStatusDto { return std::result::Result::Ok(crate::LoggingRuntimeStatusDto {
active_profile: runtime.active_profile_id.clone(), active_profile: runtime.active_profile_id.clone(),
@@ -185,9 +178,9 @@ impl AppState {
application_id: identity.application_id().to_owned(), application_id: identity.application_id().to_owned(),
launch_timestamp: identity.launch_timestamp().to_owned(), launch_timestamp: identity.launch_timestamp().to_owned(),
console_enabled, console_enabled,
dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()), dropped_console_lines: crate::count_to_u64(dropped.console()),
dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()), dropped_file_lines: crate::count_to_u64(dropped.file()),
dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()), dropped_total_lines: crate::count_to_u64(dropped.total()),
files, files,
}); });
} }
@@ -206,3 +199,12 @@ impl AppState {
.is_ok(); .is_ok();
} }
} }
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}

View File

@@ -1,13 +1,19 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs // file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 3 // version: 6
//! Config and Logging bootstrap for the desktop application. //! Config and Logging bootstrap for the desktop application.
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) struct LoggingStartup { pub(crate) struct LoggingStartup {
/// Stores the guard value for this state.
pub(crate) guard: ksp_logging_lib::LoggingGuard, pub(crate) guard: ksp_logging_lib::LoggingGuard,
/// Stores the active profile id value for this state.
pub(crate) active_profile_id: std::option::Option<String>, pub(crate) active_profile_id: std::option::Option<String>,
/// Stores the selection source value for this state.
pub(crate) selection_source: String, pub(crate) selection_source: String,
/// Stores the fallback active value for this state.
pub(crate) fallback_active: bool, pub(crate) fallback_active: bool,
/// Stores the startup diagnostic value for this state.
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>, pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
} }
@@ -23,6 +29,7 @@ enum LoggingStartupPlan {
}, },
} }
/// Executes the crate-internal config management operation for the owning module.
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> { pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments); let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap { let bootstrap = match bootstrap {
@@ -38,10 +45,11 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
} }
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) fn initialize_logging( pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement, management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let plan = resolve_logging_startup(management); let plan = resolve_logging_startup(management);
return match plan { return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => { LoggingStartupPlan::Managed { active_profile_id, settings } => {
@@ -71,6 +79,15 @@ pub(crate) fn initialize_logging(
}; };
} }
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan { fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
@@ -93,7 +110,7 @@ fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPl
fn initialize_fallback_logging( fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error, initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error); let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity); return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
} }
@@ -103,7 +120,7 @@ fn initialize_planned_fallback_logging(
diagnostic: crate::CommandErrorDto, diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings, settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard { let guard = match guard {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -132,15 +149,6 @@ fn initialize_planned_fallback_logging(
}); });
} }
pub(crate) fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/bootstrap.rs"] #[path = "../unit_tests/bootstrap.rs"]
mod tests; mod tests;

View File

@@ -1,8 +1,28 @@
// file: crates/ksp-app-config-desk/src/constants.rs // file: crates/ksp-app-config-desk/src/constants.rs
// version: 9 // version: 10
//! Application-owned tracing targets and domains. //! Application-owned tracing targets and domains.
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Config Desk. /// Owning target for backend events emitted by Config Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk"; pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk";
/// Owning target for generic frontend events emitted through the KSP bridge. /// Owning target for generic frontend events emitted through the KSP bridge.
@@ -13,23 +33,3 @@ pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-config-desk.front
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash"; pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash";
/// Dedicated target used by the controlled Logging test panel. /// Dedicated target used by the controlled Logging test panel.
pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test"; pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-app-config-desk/src/documents.rs // file: crates/ksp-app-config-desk/src/documents.rs
// version: 1 // version: 3
//! Generic Config document inventory, diagnostics and validated repair services. //! Generic Config document inventory, diagnostics and validated repair services.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
const STATUS_VALID: &str = "valid"; const STAGE_EFFECTIVE: &str = "effective";
const STATUS_INVALID: &str = "invalid";
const STAGE_VALID: &str = "valid";
const STAGE_READ: &str = "read";
const STAGE_JSON: &str = "json"; const STAGE_JSON: &str = "json";
const STAGE_OTHER: &str = "other";
const STAGE_READ: &str = "read";
const STAGE_SCHEMA: &str = "schema"; const STAGE_SCHEMA: &str = "schema";
const STAGE_SEMANTIC: &str = "semantic"; const STAGE_SEMANTIC: &str = "semantic";
const STAGE_EFFECTIVE: &str = "effective"; const STAGE_VALID: &str = "valid";
const STAGE_OTHER: &str = "other"; const STATUS_INVALID: &str = "invalid";
const STATUS_VALID: &str = "valid";
/// Safe inventory row for one Config document registered by `ksp-config-lib`. /// Safe inventory row for one Config document registered by `ksp-config-lib`.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -78,7 +78,7 @@ impl ConfigDocumentErrorDto {
} }
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state. /// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> { pub(crate) fn document_inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
let management = state.config_management(); let management = state.config_management();
let engine = management.engine(); let engine = management.engine();
let mut documents = std::vec::Vec::new(); let mut documents = std::vec::Vec::new();
@@ -98,7 +98,7 @@ pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocument
} }
/// Loads one Config document detail, including raw source when Config can read it. /// Loads one Config document detail, including raw source when Config can read it.
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> { pub(crate) fn document_detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
let file_id = parse_config_file_id(file_id_text); let file_id = parse_config_file_id(file_id_text);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -133,7 +133,7 @@ pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result
} }
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail. /// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) fn save_source( pub(crate) fn save_document_source(
state: &crate::AppState, state: &crate::AppState,
file_id_text: &str, file_id_text: &str,
source: &str, source: &str,
@@ -148,7 +148,7 @@ pub(crate) fn save_source(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)), std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
}; };
let document = detail(state, file_id.as_str()); let document = crate::document_detail(state, file_id.as_str());
let document = match document { let document = match document {
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),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/dto_common.rs // file: crates/ksp-app-config-desk/src/dto_common.rs
// version: 1 // version: 2
//! Common Tauri DTOs shared by Config Desk commands. //! Common Tauri DTOs shared by Config Desk commands.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values. /// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/environment.rs // file: crates/ksp-app-config-desk/src/environment.rs
// version: 2 // version: 4
//! Safe Config environment reports and `.env` management projections for Config Desk. //! Safe Config environment reports and `.env` management projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend. /// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -63,17 +63,17 @@ impl EnvironmentMutationOperation {
} }
/// Returns the safe environment report owned by Config. /// Returns the safe environment report owned by Config.
pub(crate) fn report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> { pub(crate) fn environment_report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
return report_from_management(state.config_management()); return report_from_management(state.config_management());
} }
/// Creates or replaces one `.env` entry exclusively through ConfigManagement. /// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) fn set_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn set_environment_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return set_value_from_management(state.config_management(), variable_name, value); return set_value_from_management(state.config_management(), variable_name, value);
} }
/// Removes one `.env` entry exclusively through ConfigManagement. /// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) fn remove_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn remove_environment_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return remove_value_from_management(state.config_management(), variable_name); return remove_value_from_management(state.config_management(), variable_name);
} }

View File

@@ -1,44 +1,42 @@
// file: crates/ksp-app-config-desk/src/errors.rs // file: crates/ksp-app-config-desk/src/errors.rs
// version: 9 // version: 10
//! Application-local error codes for the configuration desktop shell. //! Application-local error codes for the configuration desktop shell.
/// Tauri runtime assembly or execution failed. /// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed"); pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Config Desk could not install the managed Logging runtime or its safe fallback. /// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed"); pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed");
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed. /// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed"); ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed");
/// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Controlled Logging test request contains an unsupported or invalid selector. /// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid"); pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid");
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel. /// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing"); pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
/// Config profile data could not be projected safely for the frontend. /// Config profile data could not be projected safely for the frontend.
pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed"); pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace. /// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret"); ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret");
/// Privileged reveal requested an unsupported source selector. /// Privileged reveal requested an unsupported source selector.
pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid"); pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/frontend_logging.rs // file: crates/ksp-app-config-desk/src/frontend_logging.rs
// version: 1 // version: 2
//! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts. //! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Config Desk frontend scripts. /// Log payload sent by Config Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs // file: crates/ksp-app-config-desk/src/lib.rs
// version: 14 // version: 16
//! Tauri desktop application for managing and validating KSP configuration. //! Tauri desktop application for managing and validating KSP configuration.
@@ -28,61 +28,173 @@ mod tw_splash;
/// Runs the KSP configuration desktop application. /// Runs the KSP configuration desktop application.
pub use self::tauri::run; pub use self::tauri::run;
/// Shared Config Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState; pub(crate) use self::app_state::AppState;
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) use self::bootstrap::LoggingStartup;
/// Executes the crate-internal config management operation for the owning module.
pub(crate) use self::bootstrap::config_management; pub(crate) use self::bootstrap::config_management;
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) use self::bootstrap::initialize_logging; pub(crate) use self::bootstrap::initialize_logging;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS; pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
/// Structured domain for safe Config environment reports.
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT; pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND; pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain for typed Logging editor inspection.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
/// Structured domain used by controlled Logging test events.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST;
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES; pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS; pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Config Desk.
pub(crate) use self::constants::TRACING_TARGET; pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND; pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Dedicated target used by the controlled Logging test panel.
pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST; pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
/// Detailed management view for one registered Config document.
pub(crate) use self::documents::ConfigDocumentDetailDto; pub(crate) use self::documents::ConfigDocumentDetailDto;
/// Safe document-specific command error with backend-owned diagnostic classification.
pub(crate) use self::documents::ConfigDocumentErrorDto; pub(crate) use self::documents::ConfigDocumentErrorDto;
/// Result of one validated raw-source repair attempt that reached persistence successfully.
pub(crate) use self::documents::ConfigDocumentSaveResultDto; pub(crate) use self::documents::ConfigDocumentSaveResultDto;
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
pub(crate) use self::documents::ConfigDocumentSummaryDto; pub(crate) use self::documents::ConfigDocumentSummaryDto;
/// Loads one Config document detail, including raw source when Config can read it.
pub(crate) use self::documents::document_detail;
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) use self::documents::document_inventory;
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) use self::documents::save_document_source;
/// Initial application/runtime snapshot exposed to the frontend.
pub(crate) use self::dto_common::AppSnapshotDto; pub(crate) use self::dto_common::AppSnapshotDto;
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
pub(crate) use self::dto_common::CommandErrorDto; pub(crate) use self::dto_common::CommandErrorDto;
/// Safe result metadata for one `.env` mutation.
pub(crate) use self::environment::ConfigEnvironmentChangeDto; pub(crate) use self::environment::ConfigEnvironmentChangeDto;
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
pub(crate) use self::environment::ConfigEnvironmentReportDto; pub(crate) use self::environment::ConfigEnvironmentReportDto;
/// Returns the safe environment report owned by Config.
pub(crate) use self::environment::environment_report;
/// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::remove_environment_value;
/// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::set_environment_value;
/// Shared Config Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED; pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID; pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED;
/// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID; pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID;
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING; pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
/// Config profile data could not be projected safely for the frontend.
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED; pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
/// Privileged reveal requested an unsupported source selector.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Config Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event; pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Console sink configuration exposed to the editor.
pub(crate) use self::logging_editor::LoggingConsoleDto;
/// Complete editable Logging candidate accepted from the frontend.
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto; pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
/// Complete typed Logging document exposed to the editor.
pub(crate) use self::logging_editor::LoggingDocumentDto; pub(crate) use self::logging_editor::LoggingDocumentDto;
/// Result of one validated typed Logging persistence operation.
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto; pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto}; /// One persistent file sink exposed to the editor.
pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto}; pub(crate) use self::logging_editor::LoggingFileDto;
/// One Logging sink selector/filter exposed to the editor.
pub(crate) use self::logging_editor::LoggingOutputFilterDto;
/// One typed Logging profile exposed to the editor.
pub(crate) use self::logging_editor::LoggingProfileDto;
/// One global Logging target override exposed to the editor.
pub(crate) use self::logging_editor::LoggingTargetFilterDto;
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) use self::logging_editor::logging_document;
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) use self::logging_editor::save_logging_document;
/// Metadata for one currently active persistent Logging file output.
pub(crate) use self::logging_runtime::LoggingRuntimeFileDto;
/// Safe observable state of the currently active KSP Logging runtime.
pub(crate) use self::logging_runtime::LoggingRuntimeStatusDto;
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) use self::logging_runtime::apply_logging_profile;
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) use self::logging_runtime::count_to_u64;
/// Creates the stable runtime identity for this Config Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Returns safe metadata for the currently active Logging runtime.
pub(crate) use self::logging_runtime::logging_runtime_status;
/// Executes the crate-internal project logging file operation for the owning module.
pub(crate) use self::logging_runtime::project_logging_file;
/// Controlled request emitted through the backend KSP Logging facade.
pub(crate) use self::logging_test::LoggingTestRequestDto;
/// Safe result metadata for one controlled backend Logging test request.
pub(crate) use self::logging_test::LoggingTestResultDto;
/// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) use self::logging_test::emit_logging_test;
/// Safe detailed view of one resolved Config profile.
pub(crate) use self::profiles::ConfigProfileDetailDto; pub(crate) use self::profiles::ConfigProfileDetailDto;
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
pub(crate) use self::profiles::ConfigProfileDocumentDto; pub(crate) use self::profiles::ConfigProfileDocumentDto;
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) use self::profiles::profile_detail;
/// Lists validated Config documents that expose standard profiles.
pub(crate) use self::profiles::profile_inventory;
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
pub(crate) use self::secrets::SecretRevealRequestDto; pub(crate) use self::secrets::SecretRevealRequestDto;
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
pub(crate) use self::secrets::SecretRevealResponseDto; pub(crate) use self::secrets::SecretRevealResponseDto;
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) use self::secrets::reveal_secret;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto; pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings; pub(crate) use self::splash::SplashSettings;
pub(crate) use self::tw_main::show_and_focus as show_main_window; /// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service; pub(crate) use self::tw_main::require_main_window;
/// Shows main window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Executes the crate-internal splash frontend ready service operation for the owning module.
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs // file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 4 // version: 6
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor. //! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Logging sink selector/filter exposed to the editor. /// One Logging sink selector/filter exposed to the editor.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -32,7 +32,7 @@ pub(crate) struct LoggingConsoleDto {
/// Console format source text. /// Console format source text.
pub(crate) format: String, pub(crate) format: String,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One persistent file sink exposed to the editor. /// One persistent file sink exposed to the editor.
@@ -53,7 +53,7 @@ pub(crate) struct LoggingFileDto {
/// Whether ANSI output is requested. /// Whether ANSI output is requested.
pub(crate) ansi: bool, pub(crate) ansi: bool,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One global Logging target override exposed to the editor. /// One global Logging target override exposed to the editor.
@@ -79,11 +79,11 @@ pub(crate) struct LoggingProfileDto {
/// Span lifecycle source text. /// Span lifecycle source text.
pub(crate) span_events: String, pub(crate) span_events: String,
/// Console sink configuration. /// Console sink configuration.
pub(crate) console: LoggingConsoleDto, pub(crate) console: crate::LoggingConsoleDto,
/// Persistent file sinks in source order. /// Persistent file sinks in source order.
pub(crate) files: std::vec::Vec<LoggingFileDto>, pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
/// Global target overrides in source order. /// Global target overrides in source order.
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>, pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
} }
/// Complete typed Logging document exposed to the editor. /// Complete typed Logging document exposed to the editor.
@@ -102,7 +102,7 @@ pub(crate) struct LoggingDocumentDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Complete editable Logging candidate accepted from the frontend. /// Complete editable Logging candidate accepted from the frontend.
@@ -115,7 +115,7 @@ pub(crate) struct LoggingDocumentCandidateDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Result of one validated typed Logging persistence operation. /// Result of one validated typed Logging persistence operation.
@@ -138,12 +138,12 @@ pub(crate) struct LoggingDocumentSaveResultDto {
} }
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend. /// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> { pub(crate) fn logging_document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
return document_from_management(state.config_management()); return document_from_management(state.config_management());
} }
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`. /// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) fn save(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> { pub(crate) fn save_logging_document(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING); let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -255,7 +255,7 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
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 mut profiles = std::vec::Vec::<LoggingProfileDto>::with_capacity(source.profiles().len()); let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
let mut file_count = 0usize; let mut file_count = 0usize;
let mut target_filter_count = 0usize; let mut target_filter_count = 0usize;
for profile in source.profiles() { for profile in source.profiles() {
@@ -291,7 +291,7 @@ fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::L
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles); return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
} }
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig { fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len()); let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
for file in profile.files { for file in profile.files {
let mut config = let mut config =
@@ -319,14 +319,14 @@ fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileC
); );
} }
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig { fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains); return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
} }
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto { fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len()); let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
for file in profile.files() { for file in profile.files() {
files.push(LoggingFileDto { files.push(crate::LoggingFileDto {
output_id: file.output_id().to_owned(), output_id: file.output_id().to_owned(),
enabled: file.enabled(), enabled: file.enabled(),
path: file.path().to_owned(), path: file.path().to_owned(),
@@ -336,15 +336,15 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
filter: project_output_filter(file.filter()), filter: project_output_filter(file.filter()),
}); });
} }
let mut target_filters = std::vec::Vec::<LoggingTargetFilterDto>::with_capacity(profile.target_filters().len()); let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
for target_filter in profile.target_filters() { for target_filter in profile.target_filters() {
target_filters.push(LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() }); target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
} }
return LoggingProfileDto { return crate::LoggingProfileDto {
profile_id: profile.profile_id().to_owned(), profile_id: profile.profile_id().to_owned(),
default_filter: profile.default_filter().to_owned(), default_filter: profile.default_filter().to_owned(),
span_events: profile.span_events().to_owned(), span_events: profile.span_events().to_owned(),
console: LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: profile.console().enabled(), enabled: profile.console().enabled(),
output: profile.console().output().to_owned(), output: profile.console().output().to_owned(),
ansi: profile.console().ansi(), ansi: profile.console().ansi(),
@@ -356,8 +356,8 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
}; };
} }
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> LoggingOutputFilterDto { fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
return LoggingOutputFilterDto { return crate::LoggingOutputFilterDto {
level: filter.level().to_owned(), level: filter.level().to_owned(),
targets: filter.targets().to_vec(), targets: filter.targets().to_vec(),
domains: filter.domains().to_vec(), domains: filter.domains().to_vec(),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs // file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 2 // version: 4
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk. //! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Metadata for one currently active persistent Logging file output. /// Metadata for one currently active persistent Logging file output.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -56,12 +56,12 @@ pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::Logging
} }
/// Returns safe metadata for the currently active Logging runtime. /// Returns safe metadata for the currently active Logging runtime.
pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn logging_runtime_status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document. /// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn apply_logging_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -88,7 +88,8 @@ pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_co
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto { /// Executes the crate-internal project logging file operation for the owning module.
pub(crate) fn project_logging_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
return LoggingRuntimeFileDto { return LoggingRuntimeFileDto {
output_id: metadata.output_id().to_owned(), output_id: metadata.output_id().to_owned(),
directory: metadata.directory().display().to_string(), directory: metadata.directory().display().to_string(),
@@ -97,7 +98,7 @@ pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> L
}; };
} }
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str { const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
return match rotation { return match rotation {
ksp_logging_lib::FileRotation::Never => "never", ksp_logging_lib::FileRotation::Never => "never",
ksp_logging_lib::FileRotation::Hourly => "hourly", ksp_logging_lib::FileRotation::Hourly => "hourly",
@@ -105,6 +106,7 @@ pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &
}; };
} }
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) fn count_to_u64(value: usize) -> u64 { pub(crate) fn count_to_u64(value: usize) -> u64 {
let converted = u64::try_from(value); let converted = u64::try_from(value);
return match converted { return match converted {

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_test.rs // file: crates/ksp-app-config-desk/src/logging_test.rs
// version: 1 // version: 2
//! Controlled Logging test events for validating KSP runtime routing from Config Desk. //! Controlled Logging test events for validating KSP runtime routing from Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Controlled request emitted through the backend KSP Logging facade. /// Controlled request emitted through the backend KSP Logging facade.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ enum LoggingTestTarget {
} }
/// Emits one controlled backend test request through `ksp-logging-lib` only. /// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) fn emit(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> { pub(crate) fn emit_logging_test(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> {
let level = parse_level(request.level.as_str()); let level = parse_level(request.level.as_str());
let level = match level { let level = match level {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/profiles.rs // file: crates/ksp-app-config-desk/src/profiles.rs
// version: 1 // version: 4
//! Safe Config profile inspection and provenance projections for Config Desk. //! Safe Config profile inspection and provenance projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Config document that exposes the standard `default_profile` / `profiles` contract. /// One Config document that exposes the standard `default_profile` / `profiles` contract.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -77,13 +77,22 @@ pub(crate) struct ConfigProfileDetailDto {
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>, pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
/// Lists validated Config documents that expose standard profiles. /// Lists validated Config documents that expose standard profiles.
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> { pub(crate) fn profile_inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
return inventory_from_management(state.config_management()); return inventory_from_management(state.config_management());
} }
/// Resolves one default or explicitly selected profile into a safe inspection DTO. /// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) fn detail(state: &crate::AppState, file_id: &str, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigProfileDetailDto> { pub(crate) fn profile_detail(
state: &crate::AppState,
file_id: &str,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
return detail_from_management(state.config_management(), file_id, requested_profile); return detail_from_management(state.config_management(), file_id, requested_profile);
} }
@@ -213,11 +222,6 @@ fn detail_from_management(
}); });
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> { fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
let root = match document.value().as_object() { let root = match document.value().as_object() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/secrets.rs // file: crates/ksp-app-config-desk/src/secrets.rs
// version: 1 // version: 2
//! Privileged, explicitly requested Config Secret reveal boundary for Config Desk. //! Privileged, explicitly requested Config Secret reveal boundary for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Privileged reveal request kept separate from ordinary environment-report DTOs. /// Privileged reveal request kept separate from ordinary environment-report DTOs.
#[derive(serde::Deserialize, TS)] #[derive(serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ impl SecretRevealSource {
} }
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked. /// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) fn reveal(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> { pub(crate) fn reveal_secret(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
return reveal_from_management(state.config_management(), request); return reveal_from_management(state.config_management(), request);
} }

View File

@@ -1,24 +1,28 @@
// file: crates/ksp-app-config-desk/src/splash.rs // file: crates/ksp-app-config-desk/src/splash.rs
// version: 3 // 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: derive-import use ts_rs::TS; // rust-rules: trait-import
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS"; const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS"; const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200; const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const DEFAULT_SPLASH_FADE_MS: u32 = 300; const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; 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 MAX_SPLASH_FADE_MS: u32 = 10_000; const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle. /// 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,27 +116,19 @@ 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);
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
/// Command emitted by Rust to the splash frontend. /// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -111,17 +138,33 @@ 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>,
} }
impl SplashOrderDto { impl SplashOrderDto {
/// 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,
};
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> { fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>(); let parsed = value.parse::<u64>();
let parsed = match parsed { let parsed = match parsed {

View File

@@ -1,11 +1,16 @@
// file: crates/ksp-app-config-desk/src/tauri.rs // file: crates/ksp-app-config-desk/src/tauri.rs
// version: 15 // version: 17
//! Tauri runtime assembly for the KSP configuration desktop application. //! Tauri runtime assembly for the KSP configuration desktop application.
/// Runs the configuration desktop application. /// Runs the configuration desktop application.
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> { pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
let context = tauri::generate_context!();
let runtime_layout = configure_packaged_runtime(&context);
if let std::result::Result::Err(error) = runtime_layout {
return std::result::Result::Err(error);
}
let app_state = crate::AppState::initialize(arguments); let app_state = crate::AppState::initialize(arguments);
let app_state = match app_state { let app_state = match app_state {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -16,7 +21,7 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
builder = configure_plugins(builder); builder = configure_plugins(builder);
builder = configure_commands(builder); builder = configure_commands(builder);
builder = configure_setup(builder); builder = configure_setup(builder);
let run_result = builder.run(tauri::generate_context!()); let run_result = builder.run(context);
return match run_result { return match run_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()), 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(
@@ -26,6 +31,36 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
}; };
} }
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
if cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
let resource_root = match resource_root {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
.with_context("tauri_error", error.to_string()),
);
},
};
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
let layout = match layout {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let working_directory = std::env::set_current_dir(layout.runtime_root());
return match working_directory {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
.with_source(error),
),
};
}
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> { fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
return builder.manage(app_state); return builder.manage(app_state);
} }
@@ -60,11 +95,11 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| { return builder.setup(|app| {
let splash = crate::tw_splash::require_window(app); let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash { if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
let main = crate::tw_main::require_window(app); let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main { if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
@@ -83,7 +118,7 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
#[tauri::command] #[tauri::command]
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> { fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
return crate::documents::inventory(&state); return crate::document_inventory(&state);
} }
#[tauri::command] #[tauri::command]
@@ -91,7 +126,7 @@ fn get_config_document_detail(
file_id: String, file_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
return crate::documents::detail(&state, file_id.as_str()); return crate::document_detail(&state, file_id.as_str());
} }
#[tauri::command] #[tauri::command]
@@ -100,14 +135,14 @@ fn save_config_document_source(
source: String, source: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
return crate::documents::save_source(&state, file_id.as_str(), source.as_str()); return crate::save_document_source(&state, file_id.as_str(), source.as_str());
} }
#[tauri::command] #[tauri::command]
fn get_config_profile_documents( fn get_config_profile_documents(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> {
let result = crate::profiles::inventory(&state); let result = crate::profile_inventory(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -120,7 +155,7 @@ fn get_config_profile_detail(
profile_id: std::option::Option<String>, profile_id: std::option::Option<String>,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> {
let result = crate::profiles::detail(&state, file_id.as_str(), profile_id.as_deref()); let result = crate::profile_detail(&state, file_id.as_str(), profile_id.as_deref());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -131,7 +166,7 @@ fn get_config_profile_detail(
fn get_environment_report( fn get_environment_report(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> {
let result = crate::environment::report(&state); let result = crate::environment_report(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -144,7 +179,7 @@ fn set_environment_value(
value: String, value: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::set_value(&state, variable_name.as_str(), value.as_str()); let result = crate::set_environment_value(&state, variable_name.as_str(), value.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -156,7 +191,7 @@ fn remove_environment_value(
variable_name: String, variable_name: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::remove_value(&state, variable_name.as_str()); let result = crate::remove_environment_value(&state, variable_name.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -168,7 +203,7 @@ fn reveal_environment_value(
request: crate::SecretRevealRequestDto, request: crate::SecretRevealRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
let result = crate::secrets::reveal(&state, request); let result = crate::reveal_secret(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -177,7 +212,7 @@ fn reveal_environment_value(
#[tauri::command] #[tauri::command]
fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> { fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> {
let result = crate::logging_editor::document(&state); let result = crate::logging_document(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -186,7 +221,7 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
#[tauri::command] #[tauri::command]
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::status(&state); let result = crate::logging_runtime_status(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -198,7 +233,7 @@ fn apply_logging_profile(
profile_id: String, profile_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str()); let result = crate::apply_logging_profile(&state, profile_id.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -210,7 +245,7 @@ fn save_logging_document(
candidate: crate::LoggingDocumentCandidateDto, candidate: crate::LoggingDocumentCandidateDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> {
let result = crate::logging_editor::save(&state, candidate); let result = crate::save_logging_document(&state, candidate);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -222,7 +257,7 @@ fn emit_logging_test(
request: crate::LoggingTestRequestDto, request: crate::LoggingTestRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> {
let result = crate::logging_test::emit(&state, request); let result = crate::emit_logging_test(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-app-config-desk/src/tw_main.rs // file: crates/ksp-app-config-desk/src/tw_main.rs
// version: 1 // version: 4
//! Tauri-window helpers for the Config Desk main window. //! Tauri-window helpers for the Config Desk main window.
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
pub(crate) const WINDOW_LABEL_MAIN: &str = "main"; pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required main window or returns a typed error.
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_MAIN); let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -18,8 +20,9 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> { /// Shows main window.
let window = require_window(app); pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = crate::require_main_window(app);
let window = match window { let window = match window {
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),

View File

@@ -1,15 +1,18 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs // file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 2 // 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 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";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH); let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -20,7 +23,12 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> { /// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH { if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err( return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window") ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
@@ -28,12 +36,7 @@ pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri
); );
} }
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();
@@ -42,49 +45,60 @@ pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri
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);
@@ -108,18 +122,45 @@ pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri
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",
"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": {
@@ -45,6 +51,16 @@
"icon": [ "icon": [
"icons/favicon.png", "icons/favicon.png",
"icons/favicon.ico" "icons/favicon.ico"
] ],
"resources": {
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
}
} }
} }

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: 7
//! Desktop build/shell contract audits for Config Desk. //! Desktop build/shell contract audits for Config Desk.
@@ -26,13 +26,52 @@ fn read_json(path: &std::path::Path) -> serde_json::Value {
}; };
} }
fn parse_semver_core(value: &str) -> std::option::Option<(u64, u64, u64)> {
let core = match value.split_once('-') {
std::option::Option::Some((core, _)) => core,
std::option::Option::None => value,
};
let mut parts = core.split('.');
let major = match parts.next().and_then(|part| return part.parse::<u64>().ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let minor = match parts.next().and_then(|part| return part.parse::<u64>().ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let patch = match parts.next().and_then(|part| return part.parse::<u64>().ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
if parts.next().is_some() {
return std::option::Option::None;
}
return std::option::Option::Some((major, minor, patch));
}
fn assert_packaged_version_floor(value: std::option::Option<&str>, field: &str) {
assert!(value.is_some(), "{field} packaged version must exist");
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let parsed = parse_semver_core(value);
assert!(parsed.is_some(), "{field} packaged version must be SemVer-like");
if let std::option::Option::Some(parsed) = parsed {
assert!(parsed >= (0, 2, 6), "{field} packaged version must be >= 0.2.6");
}
}
#[test] #[test]
fn tauri_and_frontend_build_contracts_remain_explicit() { 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 +86,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 +111,54 @@ 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 app_style = read_text(root.join("frontend/sass/_app.scss").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("row g-0 h-100 flex-nowrap"));
assert!(app_style.contains("flex-wrap: nowrap"));
assert!(app_style.contains("min-width: 0"));
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("width: 100%"));
assert!(splash_style.contains("user-select: none"));
assert!(splash_ts.contains("add_debug"));
assert!(splash_ts.contains("add_message"));
}
#[test]
fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writable_root() {
let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path());
let tauri_version = tauri.pointer("/version").and_then(serde_json::Value::as_str);
assert_packaged_version_floor(tauri_version, "tauri.conf.json");
let package = read_json(root.join("package.json").as_path());
let package_version = package.pointer("/version").and_then(serde_json::Value::as_str);
assert_packaged_version_floor(package_version, "package.json");
assert_eq!(tauri_version, package_version, "desktop package metadata versions must remain synchronized");
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Config resources map must exist");
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 8);
assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
assert_eq!(
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/schemas/std.wallet.schema.json"),
);
}
let tauri_source = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri_source.contains("ksp_config_lib::prepare_packaged_runtime"));
assert!(tauri_source.contains("tauri::utils::platform::resource_dir"));
assert!(tauri_source.contains("std::env::set_current_dir(layout.runtime_root())"));
}

View File

@@ -1,31 +1,29 @@
// file: crates/ksp-app-config-desk/unit_tests/documents.rs // file: crates/ksp-app-config-desk/unit_tests/documents.rs
// version: 1 // version: 2
use super::*;
#[test] #[test]
fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() { fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() {
let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json"); let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json");
assert_eq!(classify_error(&json), STAGE_JSON); assert_eq!(super::classify_error(&json), super::STAGE_JSON);
let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema"); let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema");
assert_eq!(classify_error(&schema), STAGE_SCHEMA); assert_eq!(super::classify_error(&schema), super::STAGE_SCHEMA);
let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic"); let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic");
assert_eq!(classify_error(&semantic), STAGE_SEMANTIC); assert_eq!(super::classify_error(&semantic), super::STAGE_SEMANTIC);
let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective"); let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective");
assert_eq!(classify_error(&effective), STAGE_EFFECTIVE); assert_eq!(super::classify_error(&effective), super::STAGE_EFFECTIVE);
} }
#[test] #[test]
fn schema_source_read_errors_are_classified_as_schema_failures() { fn schema_source_read_errors_are_classified_as_schema_failures() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging");
assert_eq!(classify_error(&error), STAGE_SCHEMA); assert_eq!(super::classify_error(&error), super::STAGE_SCHEMA);
} }
#[test] #[test]
fn document_error_projection_does_not_expose_arbitrary_error_context() { fn document_error_projection_does_not_expose_arbitrary_error_context() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary");
let projection = ConfigDocumentErrorDto::from_error(&error); let projection = crate::ConfigDocumentErrorDto::from_error(&error);
assert_eq!(projection.diagnostic_stage, STAGE_JSON); assert_eq!(projection.diagnostic_stage, super::STAGE_JSON);
assert_eq!(projection.error.domain, "config"); assert_eq!(projection.error.domain, "config");
assert_eq!(projection.error.code, "json_syntax_invalid"); assert_eq!(projection.error.code, "json_syntax_invalid");
assert_eq!(projection.error.message, "invalid source"); assert_eq!(projection.error.message, "invalid source");

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-app-config-desk/unit_tests/dto_common.rs // file: crates/ksp-app-config-desk/unit_tests/dto_common.rs
// version: 1 // version: 2
#[test] #[test]
fn command_error_projection_excludes_context_values() { fn command_error_projection_excludes_context_values() {
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported"); let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
let dto = super::CommandErrorDto::from_error(&error); let dto = crate::CommandErrorDto::from_error(&error);
assert_eq!(dto.domain, "test"); assert_eq!(dto.domain, "test");
assert_eq!(dto.code, "failure"); assert_eq!(dto.code, "failure");
assert_eq!(dto.message, "safe message"); assert_eq!(dto.message, "safe message");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/environment.rs // file: crates/ksp-app-config-desk/unit_tests/environment.rs
// version: 2 // version: 4
#[test] #[test]
fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() { fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs // file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
// version: 4 // version: 7
#[test] #[test]
fn committed_logging_document_maps_complete_read_only_editor_contract() { fn committed_logging_document_maps_complete_read_only_editor_contract() {
@@ -66,7 +66,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
let document = super::document_from_management(&management); let document = super::document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}"); assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let std::result::Result::Ok(document) = document { if let std::result::Result::Ok(document) = document {
let candidate = super::LoggingDocumentCandidateDto { let candidate = crate::LoggingDocumentCandidateDto {
logs_directory: document.logs_directory.clone(), logs_directory: document.logs_directory.clone(),
default_profile: document.default_profile.clone(), default_profile: document.default_profile.clone(),
profiles: document.profiles.clone(), profiles: document.profiles.clone(),
@@ -89,42 +89,42 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
#[test] #[test]
fn profile_candidate_preserves_multi_file_and_target_filter_shape() { fn profile_candidate_preserves_multi_file_and_target_filter_shape() {
let profile = super::LoggingProfileDto { let profile = crate::LoggingProfileDto {
profile_id: "clone_test".to_owned(), profile_id: "clone_test".to_owned(),
default_filter: "info".to_owned(), default_filter: "info".to_owned(),
span_events: "full".to_owned(), span_events: "full".to_owned(),
console: super::LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: true, enabled: true,
output: "stdout".to_owned(), output: "stdout".to_owned(),
ansi: false, ansi: false,
format: "human".to_owned(), format: "human".to_owned(),
filter: super::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
files: std::vec![ files: std::vec![
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.one".to_owned(), output_id: "file.one".to_owned(),
enabled: true, enabled: true,
path: "one.log".to_owned(), path: "one.log".to_owned(),
rotation: "daily".to_owned(), rotation: "daily".to_owned(),
format: "human".to_owned(), format: "human".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.two".to_owned(), output_id: "file.two".to_owned(),
enabled: false, enabled: false,
path: "two.jsonl".to_owned(), path: "two.jsonl".to_owned(),
rotation: "hourly".to_owned(), rotation: "hourly".to_owned(),
format: "json".to_owned(), format: "json".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { filter: crate::LoggingOutputFilterDto {
level: "error".to_owned(), level: "error".to_owned(),
targets: std::vec!["ksp-config-lib".to_owned()], targets: std::vec!["ksp-config-lib".to_owned()],
domains: std::vec!["config".to_owned()], domains: std::vec!["config".to_owned()],
}, },
}, },
], ],
target_filters: std::vec![super::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }], target_filters: std::vec![crate::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
}; };
let config = super::config_profile(profile); let config = super::config_profile(profile);
assert_eq!(config.profile_id(), "clone_test"); assert_eq!(config.profile_id(), "clone_test");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs // file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn launch_identity_is_safe_and_contains_application_and_process_identity() { fn launch_identity_is_safe_and_contains_application_and_process_identity() {
let identity = super::launch_identity(); let identity = crate::launch_identity();
assert!(identity.is_ok()); assert!(identity.is_ok());
if let std::result::Result::Ok(identity) = identity { if let std::result::Result::Ok(identity) = identity {
assert_eq!(identity.application_id(), crate::TRACING_TARGET); assert_eq!(identity.application_id(), crate::TRACING_TARGET);
@@ -21,6 +21,6 @@ fn runtime_rotation_labels_are_stable() {
#[test] #[test]
fn runtime_count_projection_is_non_lossy_for_normal_values() { fn runtime_count_projection_is_non_lossy_for_normal_values() {
assert_eq!(super::count_to_u64(0), 0); assert_eq!(crate::count_to_u64(0), 0);
assert_eq!(super::count_to_u64(42), 42); assert_eq!(crate::count_to_u64(42), 42);
} }

View File

@@ -1,25 +1,23 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_test.rs // file: crates/ksp-app-config-desk/unit_tests/logging_test.rs
// version: 1 // version: 2
use super::{LoggingTestLevel, LoggingTestTarget};
#[test] #[test]
fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() { fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() {
assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(LoggingTestLevel::Trace))); assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(super::LoggingTestLevel::Trace)));
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(LoggingTestLevel::Debug))); assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::LoggingTestLevel::Debug)));
assert!(matches!(super::parse_level("info"), std::result::Result::Ok(LoggingTestLevel::Info))); assert!(matches!(super::parse_level("info"), std::result::Result::Ok(super::LoggingTestLevel::Info)));
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(LoggingTestLevel::Warn))); assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::LoggingTestLevel::Warn)));
assert!(matches!(super::parse_level("error"), std::result::Result::Ok(LoggingTestLevel::Error))); assert!(matches!(super::parse_level("error"), std::result::Result::Ok(super::LoggingTestLevel::Error)));
assert!(matches!(super::parse_level("all"), std::result::Result::Ok(LoggingTestLevel::All))); assert!(matches!(super::parse_level("all"), std::result::Result::Ok(super::LoggingTestLevel::All)));
assert!(super::parse_level("fatal").is_err()); assert!(super::parse_level("fatal").is_err());
} }
#[test] #[test]
fn controlled_logging_test_targets_are_static_ksp_targets() { fn controlled_logging_test_targets_are_static_ksp_targets() {
assert!(matches!(super::parse_target("app"), std::result::Result::Ok(LoggingTestTarget::App))); assert!(matches!(super::parse_target("app"), std::result::Result::Ok(super::LoggingTestTarget::App)));
assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(LoggingTestTarget::Dedicated))); assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(super::LoggingTestTarget::Dedicated)));
assert_eq!(super::target_value(LoggingTestTarget::App), crate::TRACING_TARGET); assert_eq!(super::target_value(super::LoggingTestTarget::App), crate::TRACING_TARGET);
assert_eq!(super::target_value(LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST); assert_eq!(super::target_value(super::LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
assert!(super::parse_target("external").is_err()); assert!(super::parse_target("external").is_err());
} }

View File

@@ -1,20 +1,32 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs // file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 2 // version: 6
#[test] #[test]
fn profile_inventory_exposes_logging_default_and_available_profiles() { fn profile_inventory_exposes_registered_profile_documents() {
let management = fixture_management(); let management = fixture_management();
assert!(management.is_ok(), "fixture management should construct: {management:?}"); assert!(management.is_ok(), "fixture management should construct: {management:?}");
if let std::result::Result::Ok(management) = management { if let std::result::Result::Ok(management) = management {
let inventory = super::inventory_from_management(&management); let inventory = super::inventory_from_management(&management);
assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}"); assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}");
if let std::result::Result::Ok(inventory) = inventory { if let std::result::Result::Ok(inventory) = inventory {
assert_eq!(inventory.len(), 1); assert!(inventory.iter().any(|document| -> bool {
assert_eq!(inventory[0].file_id, ksp_config_lib::FILE_ID_STD_LOGGING); return document.file_id == ksp_config_lib::FILE_ID_STD_LOGGING;
assert!(!inventory[0].default_profile.is_empty());
assert!(inventory[0].profile_ids.iter().any(|profile_id| {
return profile_id == &inventory[0].default_profile;
})); }));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
}));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
}));
for document in inventory {
assert!(!document.default_profile.is_empty());
assert!(document.profile_ids.iter().any(|profile_id| -> bool {
return profile_id == &document.default_profile;
}));
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/secrets.rs // file: crates/ksp-app-config-desk/unit_tests/secrets.rs
// version: 1 // version: 2
#[test] #[test]
fn reveal_policy_accepts_only_secret_names_and_supported_sources() { fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
@@ -21,7 +21,7 @@ fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
#[test] #[test]
fn reveal_response_type_does_not_require_debug_or_clone_contracts() { fn reveal_response_type_does_not_require_debug_or_clone_contracts() {
let response = super::SecretRevealResponseDto { let response = crate::SecretRevealResponseDto {
variable_name: "KSP_SECRET_TEST_VALUE".to_owned(), variable_name: "KSP_SECRET_TEST_VALUE".to_owned(),
source: "dotenv".to_owned(), source: "dotenv".to_owned(),
value: std::option::Option::Some("secret-canary".to_owned()), value: std::option::Option::Some("secret-canary".to_owned()),

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

@@ -0,0 +1,44 @@
# file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 5
[package]
name = "ksp-app-wallet-desk"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lib]
name = "ksp_app_wallet_desk_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "ksp-app-wallet-desk"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-dialog.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt", "time"] }
ts-rs.workspace = true
zeroize.workspace = true
[dev-dependencies]
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,136 @@
<!-- file: crates/ksp-app-wallet-desk/README.md -->
<!-- version: 3 -->
# `ksp-app-wallet-desk`
`ksp-app-wallet-desk` est l'application desktop spécialisée stable d'administration et de validation des wallets KSP.
La crate est un package Tauri mixte :
```text
package : ksp-app-wallet-desk
lib : ksp_app_wallet_desk_lib
bin : ksp-app-wallet-desk
```
## Responsabilités
Wallet Desk reste une couche d'interface et de composition. Il ne possède ni le wire `.kspwallet`, ni la cryptographie Wallet, ni la résolution Config, ni le transport Solana :
```text
ksp-config-lib -> configuration/composite/.env
ksp-wallet-lib -> format, crypto, VIEW/OWNER, import/export, migration
ksp-onchain-transport-lib -> RPC Solana HTTP
ksp-logging-lib -> logging/tracing applicatif
ksp-app-wallet-desk -> orchestration Tauri + projections sûres
```
Le frontend ne reçoit jamais les keypairs, ciphertexts, passwords Config, paths complets import/export ou handles VIEW/OWNER. Les opérations privilégiées sont exécutées côté Rust.
## `.kspwallet` V1/V2
La surface courante conserve V1 et utilise V2 comme format natif par défaut :
```text
V1 : JSON UTF-8 historique, lecture explicite toujours supportée
V2 : wire binaire KSP, format de création/import par défaut
```
Wallet Desk consomme uniquement les APIs non versionnées de `ksp-wallet-lib` :
- création/import -> format par défaut `V2` ;
- inventory/inspection/open -> détection V1/V2 ;
- mutations OWNER/VIEW -> restent dans le format natif ouvert ;
- ouverture d'un V1 -> aucune migration implicite.
`ksp-wallet-lib` conserve parallèlement les APIs explicites `_v1` / `_v2` pour les consumers qui doivent imposer un format. `DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont des politiques distinctes ; l'apparition future d'un V3 n'impose donc pas de modifier automatiquement le format créé par les APIs génériques.
La migration V1 -> V2 est une opération explicite OWNER-authentifiée possédée par `ksp-wallet-lib`. Wallet Desk ne migre jamais silencieusement un wallet lors de sa sélection, inspection ou ouverture.
## Capacités fonctionnelles
La surface validée comprend :
- inventory root-scoped des `.kspwallet` ;
- create/import Solana CLI JSON et Base58 ;
- sélection et inspection locked ;
- ouverture VIEW et OWNER, manuelle ou via candidats secrets Config ;
- affichage Pubkey/alias/notes uniquement après autorisation ;
- `getBalance` via le Transport HTTP KSP ;
- mutation alias/notes OWNER ;
- rotations OWNER et VIEW ;
- self-rotation VIEW ;
- disable/recreate VIEW fort par OWNER ;
- export OWNER Solana CLI JSON/Base58 no-clobber ;
- wire V2 par défaut tout en conservant la compatibilité V1.
## Frontières desktop
Le frontend utilise Bootstrap, Font Awesome, DataTables/Select, SimpleBar et le bridge Logging KSP. Les capabilities Tauri du guest restent minimales :
```text
core:default
tracing:default
```
Les pickers import/export sont invoqués côté Rust. Aucun accès direct filesystem ou réseau Solana n'est accordé au frontend.
## Développement
Le workspace contient plusieurs applications Tauri. `-c/--config` n'est pas un sélecteur de crate ; le cycle doit être lancé depuis le répertoire applicatif :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
En développement, le launcher Rust normalise ensuite son current working directory vers la racine du workspace avant le bootstrap Config. Les chemins relatifs `config/`, `config/schemas/`, `.env`, `logs/` et `wallets/` restent ainsi cohérents avec le checkout de développement sans déplacer leur ownership hors de `ksp-config-lib`.
Les ports réservés sont :
```text
Vite HTTP : 1432
Vite WS : 1433
```
## Runtime packagé
Une application distribuée ne dépend pas du checkout source ni du répertoire depuis lequel l'utilisateur lance le binaire.
Les documents Config et schemas enregistrés sont embarqués comme resources Tauri. Au démarrage release :
1. Tauri résout son répertoire de resources ;
2. `ksp-config-lib` résout un répertoire de données utilisateur KSP writable via `ProjectDirs` ;
3. les documents Config packagés sont copiés uniquement lorsqu'ils sont absents ;
4. les schemas, possédés par le package courant, sont resynchronisés à chaque lancement ;
5. le process adopte ce répertoire KSP writable comme current working directory avant `AppState::initialize`.
Le `.env` n'est jamais embarqué ni prérempli. Les Config déjà modifiées par l'utilisateur ne sont jamais écrasées silencieusement. Les chemins relatifs de logs et wallets restent ancrés dans ce runtime writable, sauf override explicite par Config.
## Validation
Les validations Rust courantes sont :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
Le parcours fonctionnel utilise exclusivement Tauri :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
Le build production de la release est exécuté seulement après tous les autres gates :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri build)
```
Cette commande reste l'absolue dernière opération de validation lorsqu'une publication Wallet Desk est candidate au tag stable.
Voir également [`USAGE.md`](USAGE.md), [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md), [`../../docs/formats/KSPWALLET_V2.md`](../../docs/formats/KSPWALLET_V2.md) et [`../../docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](../../docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md).

View File

@@ -0,0 +1,140 @@
<!-- file: crates/ksp-app-wallet-desk/USAGE.md -->
<!-- version: 2 -->
# Utilisation de `ksp-app-wallet-desk`
## 1. Lancement de développement
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
Pour isoler les wallets d'un parcours manuel :
```bash
(cd crates/ksp-app-wallet-desk && \
KSP_WALLETS_DIRECTORY=var/wallet-desk-manual cargo tauri dev)
```
Le processus Rust debug recale son current working directory sur la racine du workspace avant le bootstrap Config. `KSP_WALLETS_DIRECTORY`, les autres variables KSP et le `.env` restent résolus exclusivement par `ksp-config-lib`.
## 2. Format natif
Wallet Desk ne sélectionne pas directement une version de wire : il utilise la façade générique de `ksp-wallet-lib`.
```text
nouveau wallet -> V2
import Solana CLI/Base58 -> V2
inspection/ouverture -> auto-détection V1/V2
mutation d'un V1 ouvert -> V1
mutation d'un V2 ouvert -> V2
```
La présence de V2 ne réécrit pas automatiquement les fichiers V1 existants.
## 3. Création / import
Dans **Create / Import** :
- la création génère une nouvelle identité Solana et un `.kspwallet` V2 ;
- l'import accepte les formats de transfert Solana retenus, puis crée un `.kspwallet` V2 ;
- OWNER est toujours requis ; VIEW est optionnel ;
- les passwords sont request-only et ne sont jamais renvoyés par IPC.
L'import natif utilise un picker Rust. Le path source complet et les bytes de keypair ne traversent pas le frontend.
## 4. Inventory et ouverture
L'inventory n'affiche que des projections locked sûres. La sélection ne déverrouille jamais automatiquement un secret Config.
L'ouverture peut ensuite utiliser :
- un password saisi manuellement ;
- un candidat secret Config explicitement sélectionné côté backend.
VIEW et OWNER produisent des handles Rust-only. La Pubkey, l'alias et les notes ne sont projetés qu'après autorisation.
## 5. Administration
OWNER permet notamment :
- modifier l'alias ;
- ajouter/modifier/supprimer des notes ;
- changer le password OWNER ;
- changer, désactiver ou recréer VIEW ;
- exporter la keypair en Solana CLI JSON ou Base58.
VIEW peut effectuer sa self-rotation lorsqu'il est actif. Les opérations destructives/privilégiées utilisent les modals Bootstrap du shell et non `window.confirm`/`alert`.
## 6. Migration V1 -> V2
La migration existe dans `ksp-wallet-lib`, mais n'est jamais un effet secondaire d'une ouverture Wallet Desk.
Les APIs bibliothèque dédiées sont :
```text
migrate_wallet_v1_to_v2
migrate_wallet_file_v1_to_v2
migrate_wallet_file_v1_to_v2_in_place
```
Elles authentifient OWNER, reconstruisent un document cryptographique V2 neuf et conservent l'identité Solana ainsi que les metadata/note IDs. Lorsque VIEW est activé, un password VIEW cible doit être fourni. La copie est no-clobber ; le remplacement in-place est stale-protected.
## 7. Balance Solana
Une session VIEW ou OWNER autorisée peut demander `getBalance` via `ksp-onchain-transport-lib`. Le frontend ne choisit ni Pubkey arbitraire ni URL RPC : ces valeurs sont dérivées de l'état Rust autorisé et de Config.
## 8. Runtime packagé
En build release, les Config/schemas enregistrés sont des resources du bundle Tauri. Avant le bootstrap applicatif, `ksp-config-lib` prépare un répertoire de données utilisateur commun à KSP :
```text
<runtime KSP writable>/
├── config/
│ ├── composite.ksp-app-wallet-desk.json
│ ├── std.logging.json
│ ├── std.transport.json
│ ├── std.wallet.json
│ └── schemas/
├── .env # créé/modifié uniquement par Config si nécessaire
├── logs/ # par défaut si path relatif
└── wallets/ # par défaut si path relatif
```
Le chemin physique exact dépend de la plateforme et est résolu par `ProjectDirs`; l'application ne suppose aucune home directory particulière.
Les Config existantes sont conservées lors d'une mise à jour. Les schemas sont synchronisés depuis le package courant. Le `.env` et les secrets ne sont jamais embarqués dans le bundle.
## 9. Validation de publication
Avant le build :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-wallet-lib
cargo test -p ksp-app-config-desk
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
Puis parcours fonctionnel :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
Le smoke Devnet opt-in, s'il est rejoué, doit l'être avant le build final.
L'absolue dernière opération est :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri build)
```
Aucune commande de validation ne doit être exécutée après ce build avant la décision de publication/tag, afin de conserver la preuve d'ordre du gate.

View File

@@ -0,0 +1,12 @@
// file: crates/ksp-app-wallet-desk/build.rs
// version: 1
//! Build script for the KSP wallet desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for KSP Wallet Desk",
"windows": [
"splash",
"main"
],
"permissions": [
"core:default",
"tracing:default"
]
}

View File

@@ -0,0 +1,25 @@
<!-- file: crates/ksp-app-wallet-desk/frontend/fonts/README.md -->
<!-- version: 1 -->
# Fonts du splash Wallet Desk
Wallet Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et conserve la police locale :
```text
DOS_Amazigh.ttf
font-family: Dos Amazigh
```
Destination :
```text
crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf
```
Empreinte SHA-256 de l'asset repris du gabarit :
```text
a01ff4b5a0d699db7cefcf5336bf0e379ff6fbf0d096087571a13c8782f9aa02
```
`splash.scss` déclare `Dos Amazigh` avec `@font-face` et l'applique au titre `#app-name`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

View File

@@ -0,0 +1,502 @@
<!-- file: crates/ksp-app-wallet-desk/frontend/main.html -->
<!-- version: 5 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Wallet Desk — Dashboard</title>
</head>
<body>
<header class="app-header">
<nav class="navbar h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 flex-nowrap">
<div class="navbar-brand d-flex align-items-center me-4 flex-nowrap">
<img alt="KSP" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-semibold text-primary text-nowrap">Wallet Desk</span>
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span>
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Dashboard</span>
</div>
<div class="ms-auto d-flex align-items-center gap-2 flex-nowrap">
<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>
</nav>
</header>
<main class="app-main container-fluid p-0">
<div class="row g-0 h-100 flex-nowrap">
<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 Wallet Desk">
<button class="nav-link text-start active" type="button" data-view="dashboard"><i class="fa-solid fa-gauge-high me-2" aria-hidden="true"></i>Dashboard</button>
<button class="nav-link text-start" type="button" data-view="wallets"><i class="fa-solid fa-wallet me-2" aria-hidden="true"></i>Wallets</button>
<button class="nav-link text-start" type="button" data-view="create-import"><i class="fa-solid fa-circle-plus me-2" aria-hidden="true"></i>Create / Import</button>
<button class="nav-link text-start" type="button" data-view="details"><i class="fa-solid fa-id-card me-2" aria-hidden="true"></i>Details</button>
<button class="nav-link text-start" type="button" data-view="security"><i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Security</button>
<button class="nav-link text-start" type="button" data-view="diagnostics"><i class="fa-solid fa-stethoscope me-2" aria-hidden="true"></i>Diagnostics</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-body p-4">
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div>
<span class="badge text-bg-secondary">0.2.6</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
<div class="col-xl-8">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Runtime</div>
<div class="card-body">
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Documents Config enregistrés</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil composite</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Transport</dt><dd id="runtimeTransportProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Cluster Transport</dt><dd id="runtimeTransportClusters" class="col-sm-7"></dd>
<dt class="col-sm-5">Provider Transport</dt><dd id="runtimeTransportProviders" class="col-sm-7"></dd>
<dt class="col-sm-5">Endpoints Transport</dt><dd id="runtimeTransportEndpoints" class="col-sm-7"></dd>
<dt class="col-sm-5">Rôle Transport</dt><dd id="runtimeTransportRole" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Wallet</dt><dd id="runtimeWalletProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Racine Wallet</dt><dd id="runtimeWalletsDirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Sous-répertoire Wallet</dt><dd id="runtimeWalletsSubdirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Répertoire effectif</dt><dd id="runtimeEffectiveWalletsDirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Créé au démarrage</dt><dd id="runtimeWalletDirectoryCreated" class="col-sm-7"></dd>
</dl>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Wallet courant</div>
<div class="card-body text-center">
<i id="currentWalletIcon" class="fa-solid fa-lock fa-2x mb-3 text-body-secondary" aria-hidden="true"></i>
<p id="currentWalletFilename" class="fw-semibold mb-2">Aucun wallet sélectionné.</p>
<dl class="row small text-start mb-0">
<dt class="col-5">État</dt><dd id="currentWalletState" class="col-7"></dd>
<dt class="col-5">Format</dt><dd id="currentWalletFormat" class="col-7"></dd>
<dt class="col-5">VIEW</dt><dd id="currentWalletView" class="col-7"></dd>
<dt class="col-5">Secrets Config</dt><dd id="currentWalletSecretCandidates" class="col-7">0</dd>
<dt class="col-5">Pubkey</dt><dd id="currentWalletPubkey" class="col-7 text-break"></dd>
<dt class="col-5">Alias</dt><dd id="currentWalletAlias" class="col-7 text-break"></dd>
<dt class="col-5">Notes</dt><dd id="currentWalletNotes" class="col-7 text-break"></dd>
<dt class="col-5">Balance SOL</dt><dd id="currentWalletBalanceSol" class="col-7"></dd>
</dl>
</div>
</div>
</div>
</div>
</section>
<section data-view-panel="wallets" hidden>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<span class="fw-semibold">Wallets disponibles</span>
<small class="text-body-secondary"><i class="fa-solid fa-lock me-1" aria-hidden="true"></i>verrouillé · <i class="fa-solid fa-lock-open me-1" aria-hidden="true"></i>ouvert</small>
</div>
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card-body">
<table id="walletInventoryTable" class="table table-striped table-hover align-middle w-100">
<thead><tr><th>État</th><th>Filename</th><th>Format</th><th>VIEW</th><th>Inspection</th></tr></thead>
<tbody id="walletInventoryBody"></tbody>
</table>
</div>
</div>
</section>
<section data-view-panel="create-import" hidden>
<div class="row g-3">
<div class="col-xl-8">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Créer un wallet natif KSP</div>
<div class="card-body">
<form id="createWalletForm" autocomplete="off">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="createWalletFilename">Filename</label>
<input id="createWalletFilename" class="form-control" type="text" value="wallet-01.kspwallet" required spellcheck="false">
<div class="form-text">Un seul filename sous le répertoire Wallet effectif, suffixe exact .kspwallet.</div>
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletAlias">Alias interne optionnel</label>
<input id="createWalletAlias" class="form-control" type="text" autocomplete="off">
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletOwnerPassword">OWNER password</label>
<input id="createWalletOwnerPassword" class="form-control" type="password" autocomplete="new-password" required>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input id="createWalletEnableView" class="form-check-input" type="checkbox">
<label class="form-check-label" for="createWalletEnableView">Créer aussi une capability VIEW</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletViewPassword">VIEW password</label>
<input id="createWalletViewPassword" class="form-control" type="password" autocomplete="new-password" disabled>
</div>
<div class="col-12">
<label class="form-label" for="createWalletInitialNote">Note initiale optionnelle</label>
<textarea id="createWalletInitialNote" class="form-control" rows="3" autocomplete="off"></textarea>
</div>
<div class="col-12 d-flex align-items-center gap-3">
<button id="createWalletSubmit" class="btn btn-primary" type="submit">
<i class="fa-solid fa-circle-plus me-2" aria-hidden="true"></i>Create wallet
</button>
<span id="createWalletStatus" class="text-body-secondary" role="status">Prêt.</span>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="alert alert-info mb-0">
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>
Les passwords sont envoyés uniquement du frontend vers Rust pour l'opération courante. Ils ne sont jamais renvoyés par Rust ni journalisés.
</div>
</div>
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Importer un keypair Solana vers un wallet KSP</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-5">
<label class="form-label" for="importTransferFormat">Format source</label>
<select id="importTransferFormat" class="form-select">
<option value="solana_cli_json">Solana CLI JSON</option>
<option value="solana_keypair_base58">Keypair Base58 complet</option>
</select>
</div>
<div class="col-md-7 d-flex align-items-end">
<button id="inspectImportSource" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-folder-open me-2" aria-hidden="true"></i>Choisir et inspecter la source
</button>
</div>
<div class="col-12">
<div class="alert alert-secondary mb-0">
<div><strong>Source :</strong> <span id="importSourceName"></span></div>
<div><strong>Format :</strong> <span id="importSourceFormat"></span></div>
<div><strong>Pubkey inspectée :</strong> <span id="importSourcePubkey" class="text-break"></span></div>
<div class="small text-body-secondary mt-2">Le chemin et le contenu secret restent exclusivement côté Rust. Le frontend ne reçoit que cette projection publique.</div>
</div>
</div>
</div>
<form id="importWalletForm" autocomplete="off">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="importWalletFilename">Destination .kspwallet</label>
<input id="importWalletFilename" class="form-control" type="text" value="imported-01.kspwallet" required spellcheck="false">
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletAlias">Alias interne optionnel</label>
<input id="importWalletAlias" class="form-control" type="text" autocomplete="off">
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletOwnerPassword">Nouveau OWNER password</label>
<input id="importWalletOwnerPassword" class="form-control" type="password" autocomplete="new-password" required>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input id="importWalletEnableView" class="form-check-input" type="checkbox">
<label class="form-check-label" for="importWalletEnableView">Créer aussi une capability VIEW</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletViewPassword">Nouveau VIEW password</label>
<input id="importWalletViewPassword" class="form-control" type="password" autocomplete="new-password" disabled>
</div>
<div class="col-12">
<label class="form-label" for="importWalletInitialNote">Note initiale optionnelle</label>
<textarea id="importWalletInitialNote" class="form-control" rows="3" autocomplete="off"></textarea>
</div>
<div class="col-12 d-flex align-items-center gap-3">
<button id="importWalletSubmit" class="btn btn-primary" type="submit" disabled>
<i class="fa-solid fa-file-import me-2" aria-hidden="true"></i>Import wallet
</button>
<span id="importWalletStatus" class="text-body-secondary" role="status">Choisir une source.</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<section data-view-panel="details" hidden>
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-semibold">Détails autorisés et balance réseau</span>
<button id="refreshWalletBalance" class="btn btn-outline-primary btn-sm" type="button" disabled>
<i class="fa-solid fa-coins me-2" aria-hidden="true"></i>Refresh balance
</button>
</div>
<div class="card-body">
<dl class="row mb-3">
<dt class="col-md-4">Pubkey</dt><dd id="detailsWalletPubkey" class="col-md-8 text-break"></dd>
<dt class="col-md-4">Alias</dt><dd id="detailsWalletAlias" class="col-md-8 text-break"></dd>
<dt class="col-md-4">Notes</dt><dd id="detailsWalletNotes" class="col-md-8 text-break"></dd>
<dt class="col-md-4">Lamports</dt><dd id="detailsBalanceLamports" class="col-md-8"></dd>
<dt class="col-md-4">SOL</dt><dd id="detailsBalanceSol" class="col-md-8"></dd>
<dt class="col-md-4">RPC slot</dt><dd id="detailsBalanceSlot" class="col-md-8"></dd>
<dt class="col-md-4">RPC api_version</dt><dd id="detailsBalanceApiVersion" class="col-md-8"></dd>
<dt class="col-md-4">Transport</dt><dd id="detailsBalanceTransport" class="col-md-8"></dd>
</dl>
<div id="balanceStatus" class="alert alert-secondary mb-0" role="status">Unlock VIEW ou OWNER pour interroger getBalance.</div>
</div>
</div>
<div class="card shadow-sm mt-3">
<div class="card-header fw-semibold">Administration metadata OWNER</div>
<div class="card-body">
<p class="text-body-secondary">Alias et notes sont protégés dans le wallet. Les contrôles restent désactivés tant quune session OWNER nest pas ouverte.</p>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="ownerWalletAlias">Alias interne</label>
<div class="input-group">
<input id="ownerWalletAlias" class="form-control" type="text" autocomplete="off" data-owner-metadata-control disabled>
<button id="saveOwnerAlias" class="btn btn-outline-primary" type="button" data-owner-metadata-control disabled>Save</button>
<button id="clearOwnerAlias" class="btn btn-outline-secondary" type="button" data-owner-metadata-control disabled>Clear</button>
</div>
<div class="form-text">Limite V1 : 256 octets UTF-8. La validation autoritative reste dans ksp-wallet-lib.</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="ownerNewNote">Nouvelle note</label>
<div class="input-group">
<textarea id="ownerNewNote" class="form-control" rows="2" autocomplete="off" data-owner-metadata-control disabled></textarea>
<button id="addOwnerNote" class="btn btn-outline-primary" type="button" data-owner-metadata-control disabled>Add note</button>
</div>
</div>
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold">Notes autorisées</span>
<small class="text-body-secondary">64 notes maximum en V1</small>
</div>
<div id="ownerNotesList" class="vstack gap-2"><span class="text-body-secondary">Ouvrir OWNER pour administrer les notes.</span></div>
</div>
<div class="col-12">
<div id="ownerMetadataStatus" class="alert alert-secondary mb-0" role="status">Session OWNER requise.</div>
</div>
</div>
</div>
</div>
</section>
<section data-view-panel="security" hidden>
<div class="row g-3">
<div class="col-xl-7">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Unlock manuel du wallet sélectionné</div>
<div class="card-body">
<p class="text-body-secondary">Chaque tentative Argon2 est explicite et peut prendre plusieurs secondes. Le password est envoyé uniquement frontend → Rust puis purgé.</p>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="unlockWalletViewPassword">VIEW password</label>
<div class="input-group">
<input id="unlockWalletViewPassword" class="form-control" type="password" autocomplete="current-password">
<button id="unlockViewManual" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="unlockWalletOwnerPassword">OWNER password</label>
<div class="input-group">
<input id="unlockWalletOwnerPassword" class="form-control" type="password" autocomplete="current-password">
<button id="unlockOwnerManual" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Secrets configurés</div>
<div class="card-body">
<p class="mb-2">Candidats effectifs : <strong id="unlockSecretCandidateCount">0</strong></p>
<p class="small text-body-secondary">Les noms, suffixes et valeurs des secrets Wallet configurés restent exclusivement côté Rust/Config. Cette action tente les candidats uniquement sur demande.</p>
<div class="d-grid gap-2">
<button id="unlockViewConfigured" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW avec secret configuré</button>
<button id="unlockOwnerConfigured" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER avec secret configuré</button>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Rotation credentials OWNER / VIEW</div>
<div class="card-body">
<p class="small text-body-secondary">OWNER peut administrer OWNER et VIEW sans ancien password VIEW. Une session VIEW peut self-rotate son propre password VIEW. Les rotations modifient le fichier Wallet sans changer automatiquement les secrets Config.</p>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="rotateOwnerPassword">Nouveau OWNER password</label>
<input id="rotateOwnerPassword" class="form-control mb-2" type="password" autocomplete="new-password" data-owner-rotation-control disabled>
<label class="form-label" for="rotateOwnerPasswordConfirm">Confirmer OWNER password</label>
<div class="input-group">
<input id="rotateOwnerPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-owner-rotation-control disabled>
<button id="rotateOwnerPasswordSubmit" class="btn btn-outline-danger" type="button" data-owner-rotation-control disabled>Rotate OWNER</button>
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="rotateViewPassword">Nouveau VIEW password</label>
<input id="rotateViewPassword" class="form-control mb-2" type="password" autocomplete="new-password" data-view-rotation-control disabled>
<label class="form-label" for="rotateViewPasswordConfirm">Confirmer VIEW password</label>
<div class="input-group">
<input id="rotateViewPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-view-rotation-control disabled>
<button id="rotateViewPasswordSubmit" class="btn btn-outline-primary" type="button" data-view-rotation-control disabled>Rotate VIEW</button>
</div>
</div>
</div>
<div id="rotationStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session VIEW ou OWNER requise pour une rotation VIEW ; OWNER requis pour une rotation OWNER.</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-warning">
<div class="card-header fw-semibold">Administration forte VIEW — OWNER uniquement</div>
<div class="card-body">
<p class="small text-body-secondary">Ces opérations changent lautorité VIEW courante, pas seulement son password. Disable retire VIEW et rekey les metadata ; Recreate génère une nouvelle metadata key, un nouveau slot VIEW et un nouveau password. La keypair Solana, lOWNER, lalias et les notes restent inchangés.</p>
<div class="row g-3 align-items-end">
<div class="col-lg-4">
<button id="disableWalletView" class="btn btn-outline-danger w-100" type="button" disabled>Strong disable VIEW</button>
</div>
<div class="col-lg-4">
<label class="form-label" for="recreateViewPassword">Nouveau VIEW password</label>
<input id="recreateViewPassword" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
</div>
<div class="col-lg-4">
<label class="form-label" for="recreateViewPasswordConfirm">Confirmer VIEW password</label>
<div class="input-group">
<input id="recreateViewPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
<button id="recreateWalletView" class="btn btn-outline-warning" type="button" data-view-security-recreate-control disabled>Strong recreate</button>
</div>
</div>
</div>
<div id="viewSecurityStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise pour administrer fortement VIEW.</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-danger-subtle">
<div class="card-header fw-semibold">Export keypair Solana — OWNER uniquement</div>
<div class="card-body">
<p class="small text-body-secondary">Lexport produit un fichier secret non chiffré contenant la keypair Solana complète. Le contenu et le chemin complet restent exclusivement côté Rust ; le frontend choisit seulement le format puis confirme lopération.</p>
<div class="row g-3 align-items-end">
<div class="col-lg-6">
<label class="form-label" for="walletExportFormat">Format dexport</label>
<select id="walletExportFormat" class="form-select" data-owner-export-control disabled>
<option value="solana_cli_json">Solana CLI JSON</option>
<option value="solana_keypair_base58">Keypair Base58 complet</option>
</select>
</div>
<div class="col-lg-6 d-grid">
<button id="exportWalletKeypair" class="btn btn-outline-danger" type="button" data-owner-export-control disabled>Export secret keypair file</button>
</div>
</div>
<div id="walletExportStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise. Aucun export texte ou clipboard nest disponible.</div>
</div>
</div>
</div>
<div class="col-12">
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
</div>
</div>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="card shadow-sm"><div class="card-header fw-semibold">Diagnostics Transport sûrs</div><div class="card-body"><dl class="row mb-0">
<dt class="col-md-4">Profil</dt><dd id="diagnosticsTransportProfile" class="col-md-8"></dd>
<dt class="col-md-4">Clusters</dt><dd id="diagnosticsTransportClusters" class="col-md-8"></dd>
<dt class="col-md-4">Providers</dt><dd id="diagnosticsTransportProviders" class="col-md-8"></dd>
<dt class="col-md-4">Endpoints disponibles</dt><dd id="diagnosticsTransportEndpoints" class="col-md-8"></dd>
<dt class="col-md-4">Rôle balance</dt><dd id="diagnosticsTransportRole" class="col-md-8"></dd>
</dl></div></div>
</section>
</div>
</div>
</div>
</section>
</div>
</main>
<div class="modal fade" id="deleteWalletNoteModal" tabindex="-1" aria-labelledby="deleteWalletNoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="deleteWalletNoteModalLabel">Supprimer la note protégée ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Cette mutation OWNER est définitive pour la note sélectionnée. Le contenu protégé nest pas recopié dans la confirmation.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmDeleteWalletNote" type="button" class="btn btn-danger">Delete note</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="disableWalletViewModal" tabindex="-1" aria-labelledby="disableWalletViewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="disableWalletViewModalLabel">Strong disable VIEW ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Le slot VIEW courant sera supprimé et les metadata seront rekeyées. Le password VIEW actuel ne pourra plus ouvrir létat courant. Les copies historiques du fichier restent hors de la garantie de révocation.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmDisableWalletView" type="button" class="btn btn-danger">Disable VIEW</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="recreateWalletViewModal" tabindex="-1" aria-labelledby="recreateWalletViewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="recreateWalletViewModalLabel">Strong recreate VIEW ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Une nouvelle metadata key et un nouveau slot VIEW seront créés. Tout credential VIEW courant sera remplacé. Le nouveau password nest jamais recopié dans cette confirmation.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmRecreateWalletView" type="button" class="btn btn-warning">Recreate VIEW</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="exportWalletKeypairModal" tabindex="-1" aria-labelledby="exportWalletKeypairModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="exportWalletKeypairModalLabel">Exporter la keypair Solana ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Cette opération OWNER crée un fichier secret non chiffré contenant les 64 octets de la keypair Solana. Le fichier doit être protégé comme une clé privée. Un fichier existant ne sera jamais écrasé par Wallet.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmExportWalletKeypair" type="button" class="btn btn-danger">Choose destination and export</button>
</div>
</div>
</div>
</div>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,111 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/_app.scss
// version: 2
$app-header-height: 72px;
$app-footer-height: 42px;
$app-sidebar-width: 250px;
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
overflow: hidden;
background: $gray-100;
}
.app-header {
position: fixed;
inset: 0 0 auto;
height: $app-header-height;
z-index: 1020;
border-bottom: 2px solid rgba($primary, 0.3);
}
.app-footer {
position: fixed;
inset: auto 0 0;
height: $app-footer-height;
z-index: 1020;
border-top: 2px solid rgba($primary, 0.3);
}
.app-main {
position: relative;
height: calc(100vh - $app-header-height - $app-footer-height);
margin-top: $app-header-height;
margin-bottom: $app-footer-height;
overflow: hidden;
}
.app-main > .row {
flex-wrap: nowrap;
min-width: 0;
}
.app-logo {
display: block;
width: auto;
height: 42px;
}
.app-scrollable {
height: 100%;
max-height: 100%;
}
.app-shell-card {
max-width: 1320px;
}
.app-runtime-list dd,
#walletInventoryTable td:nth-child(2),
#walletInventoryTable td:nth-child(3) {
font-family: var(--bs-font-monospace);
}
.app-sidebar {
width: $app-sidebar-width;
min-width: $app-sidebar-width;
max-width: $app-sidebar-width;
}
.app-sidebar-scroll,
.app-content {
height: 100%;
max-height: 100%;
}
.app-content {
min-width: 0;
}
.app-sidebar .nav-link {
white-space: nowrap;
}
.app-placeholder {
min-height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem;
border: 1px dashed var(--bs-border-color);
border-radius: var(--bs-border-radius-lg);
color: var(--bs-secondary-color);
background: var(--bs-body-bg);
text-align: center;
}
.app-placeholder i {
font-size: 2.5rem;
}
#walletInventoryTable tbody tr {
cursor: pointer;
}

View File

@@ -0,0 +1,160 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
// version: 1
// Pulse 5.3.8
// Bootswatch
// Variables
// Buttons
.btn {
&:focus,
&:active,
&:active:focus,
&.active:focus {
outline: none;
}
&-secondary {
color: $gray-900;
background-color: $white;
border-color: #ccc;
&:hover {
color: $gray-900;
background-color: $gray-300;
border-color: $gray-500;
}
&.disabled {
color: tint-color($gray-900, 5%);
background-color: $white;
border-color: tint-color(#ccc, 5%);
}
}
&-warning {
color: $white;
}
&-primary:focus {
box-shadow: 0 0 5px tint-color($primary, 10%);
}
&-secondary:focus {
box-shadow: 0 0 5px $gray-400;
}
&-success:focus {
box-shadow: 0 0 5px tint-color($success, 10%);
}
&-info:focus {
box-shadow: 0 0 5px tint-color($info, 10%);
}
&-warning:focus {
box-shadow: 0 0 5px tint-color($warning, 10%);
}
&-danger:focus {
box-shadow: 0 0 5px tint-color($danger, 10%);
}
&.disabled:focus {
box-shadow: none;
}
}
// Tables
.table .thead-dark th {
background-color: $secondary;
border-color: $table-border-color;
}
.table-primary,
.table-secondary,
.table-success,
.table-warning,
.table-danger,
.table-info,
.table-light {
--#{$prefix}table-color: #{$body-color};
}
// Forms
.form-control:focus {
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
}
// Navs
.nav-tabs {
.nav-link,
.nav-link.active {
border-width: 0 0 1px;
}
.nav-link:hover,
.nav-link.active,
.nav-link.active:hover,
.nav-link.active:focus {
border-bottom: 1px solid $primary;
}
.nav-item+.nav-item {
margin-left: 0;
}
}
.breadcrumb {
&-item.active {
color: $gray-700;
}
}
// Indicators
.badge {
&.bg-light {
color: $dark;
}
}
// Progress bars
.progress {
height: 8px;
}
// Containers
.list-group {
&-item {
color: rgba(255, 255, 255, .8);
&.active,
&:hover,
&:focus {
color: $white;
}
&.active {
font-weight: 700;
&:hover {
background-color: $list-group-hover-bg;
}
}
&.disabled:hover {
color: $list-group-disabled-color;
}
}
}

View File

@@ -0,0 +1,19 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss
// version: 1
//@use '@fortawesome/fontawesome-free/scss/variables' with (
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
// $font-path: '../webfonts',
//);
@use '@fortawesome/fontawesome-free/scss/variables' with (
// use fonts from @fortawesome/fontawesome-free
$font-path: '@fortawesome/fontawesome-free/webfonts',
);
// load Font Awesome core
@use '@fortawesome/fontawesome-free/scss/fontawesome';
// load and make available Font Awesome helpers (mixins, functions, and variables)
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;

View File

@@ -0,0 +1,248 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss
// version: 1
/* Rtl support */
[data-simplebar] {
position: relative;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: flex-start;
}
.simplebar-wrapper {
overflow: hidden;
width: inherit;
height: inherit;
max-width: inherit;
max-height: inherit;
}
.simplebar-mask {
direction: inherit;
position: absolute;
overflow: hidden;
padding: 0;
margin: 0;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: auto !important;
height: auto !important;
// z-index: 0;
inset: 0;
}
.simplebar-offset {
direction: inherit !important;
box-sizing: inherit !important;
resize: none !important;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0;
margin: 0;
-webkit-overflow-scrolling: touch;
inset: 0;
}
.simplebar-content-wrapper {
direction: inherit;
box-sizing: border-box !important;
position: relative;
display: block;
height: 100%;
width: auto;
max-width: 100%;
max-height: 100%;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.simplebar-hide-scrollbar {
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
position: fixed;
left: 0;
visibility: hidden;
overflow-y: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}
.simplebar-content {
&:before {
content: ' ';
display: table;
}
&:after {
content: ' ';
display: table;
}
}
.simplebar-placeholder {
max-height: 100%;
max-width: 100%;
width: 100%;
pointer-events: none;
}
.simplebar-height-auto-observer-wrapper {
box-sizing: inherit !important;
height: 100%;
width: 100%;
max-width: 1px;
position: relative;
float: left;
max-height: 1px;
overflow: hidden;
// z-index: -1;
padding: 0;
margin: 0;
pointer-events: none;
flex-grow: inherit;
flex-shrink: 0;
flex-basis: 0;
}
.simplebar-height-auto-observer {
box-sizing: inherit;
display: block;
opacity: 0;
position: absolute;
top: 0;
left: 0;
height: 1000%;
width: 1000%;
min-height: 1px;
min-width: 1px;
overflow: hidden;
pointer-events: none;
// z-index: -1;
}
.simplebar-track {
// z-index: 1;
position: absolute;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
}
[data-simplebar].simplebar-dragging {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.simplebar-content {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.simplebar-track {
pointer-events: all;
}
}
.simplebar-scrollbar {
position: absolute;
left: 0;
right: 0;
min-height: 10px;
&:before {
position: absolute;
content: '';
background: black;
border-radius: 7px;
left: 2px;
right: 2px;
opacity: 0;
transition: opacity 0.2s 0.5s linear;
top: 2px;
bottom: 2px;
}
}
.simplebar-scrollbar.simplebar-visible {
&:before {
opacity: 0.5;
transition-delay: 0s;
transition-duration: 0s;
}
}
.simplebar-track.simplebar-vertical {
top: 0;
width: 11px;
}
.simplebar-track.simplebar-horizontal {
left: 0;
height: 11px;
.simplebar-scrollbar {
right: auto;
left: 0;
top: 0;
bottom: 0;
min-height: 0;
min-width: 10px;
width: auto;
}
}
[data-simplebar-direction='rtl'] {
.simplebar-track.simplebar-vertical {
right: auto;
left: 0;
}
}
.simplebar-dummy-scrollbar-size {
direction: rtl;
position: fixed;
opacity: 0;
visibility: hidden;
height: 500px;
width: 500px;
overflow-y: hidden;
overflow-x: scroll;
-ms-overflow-style: scrollbar !important;
>div {
width: 200%;
height: 200%;
margin: 10px 0;
}
}
.simplebar-hover {
cursor: pointer;
}

View File

@@ -0,0 +1,95 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/_variables.scss
// version: 1
// Pulse 5.3.8
// Bootswatch
$theme: "pulse" !default;
//
// Color system
//
$white: #fff !default;
$gray-100: #fafafa !default;
$gray-200: #f9f8fc !default;
$gray-300: #ededed !default;
$gray-400: #cbc8d0 !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #444 !default;
$gray-800: #343a40 !default;
$gray-900: #17141f !default;
$black: #000 !default;
$blue: #007bff !default;
$indigo: #6610f2 !default;
$purple: #593196 !default;
$pink: #e83e8c !default;
$red: #fc3939 !default;
$orange: #fd7e14 !default;
$yellow: #efa31d !default;
$green: #13b955 !default;
$teal: #20c997 !default;
$cyan: #009cdc !default;
$primary: $purple !default;
$secondary: #a991d4 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-200 !default;
$dark: $gray-900 !default;
$min-contrast-ratio: 2.1 !default;
// Options
$enable-rounded: false !default;
// Body
$body-color: $gray-700 !default;
// Links
$link-hover-color: $primary !default;
// Tables
$table-color: initial !default;
$table-border-color: rgba(0, 0, 0, .05) !default;
// Forms
$input-focus-border-color: $primary !default;
// Dropdowns
$dropdown-link-hover-color: $white !default;
$dropdown-link-hover-bg: $primary !default;
// Navs
$nav-tabs-border-color: $gray-300 !default;
$nav-tabs-link-hover-border-color: $primary !default;
// Navbar
$navbar-padding-y: 1.2rem !default;
// Progress bars
$progress-bg: $gray-300 !default;
$progress-bar-bg: $primary !default;
// List group
$list-group-bg: $gray-900 !default;
$list-group-border-color: transparent !default;
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
$list-group-active-color: $white !default;
$list-group-active-bg: $list-group-bg !default;
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;

View File

@@ -0,0 +1,10 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/main.scss
// version: 1
@import "bootstrap/scss/functions";
@import "variables";
@import "fontawesome";
@import "simplebar";
@import "bootstrap/scss/bootstrap";
@import "bootswatch";
@import "app";

View File

@@ -0,0 +1,137 @@
// file: crates/ksp-app-wallet-desk/frontend/sass/splash.scss
// version: 3
@font-face {
font-family: "Dos Amazigh";
src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
font-family: Arial, sans-serif;
}
#splash-container {
position: relative;
width: 960px;
max-width: 100%;
height: 637px;
max-height: 100%;
overflow: hidden;
opacity: 0;
will-change: opacity;
}
#splash-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
object-fit: contain;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
#app-name {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
transform: translate(-50%, -50%);
color: #fff;
font-family: "Dos Amazigh", sans-serif;
font-size: 76px;
font-weight: bold;
text-align: center;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
#debug-info {
position: absolute;
top: 0;
left: 0;
z-index: 3;
width: 100%;
max-height: 30%;
overflow-y: auto;
padding: 8px;
color: #fff;
background: rgba(0, 0, 0, 0.18);
font-family: monospace;
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

@@ -0,0 +1,23 @@
<!-- file: crates/ksp-app-wallet-desk/frontend/splash.html -->
<!-- version: 1 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="stylesheet" href="sass/splash.scss">
<title>KSP Wallet Desk — Chargement</title>
</head>
<body>
<div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Wallet Desk">
<div id="app-name">Wallet Desk</div>
<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>

View File

@@ -0,0 +1,110 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_wallet_desk/frontend_logging/FrontendLogPayloadDto.ts";
export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
export type FrontendLogTargetId = "frontend" | "main" | "splash";
type ConsoleMethod = (...items: unknown[]) => void;
const originalConsole = {
trace: console.trace.bind(console),
debug: console.debug.bind(console),
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
function stringifyItem(item: unknown): string {
if (item instanceof Error) {
return item.stack ?? item.message;
}
if (typeof item === "string") {
return item;
}
try {
const serialized = JSON.stringify(item);
return serialized ?? String(item);
} catch {
return String(item);
}
}
function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
const payload: FrontendLogPayloadDto = {
level,
targetId,
message,
};
await invoke("emit_frontend_log", { payload });
}
function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
return level === "trace"
? originalConsole.trace(message)
: level === "debug"
? originalConsole.debug(message)
: level === "info"
? originalConsole.info(message)
: level === "warn"
? originalConsole.warn(message)
: originalConsole.error(message);
}
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
writeOriginalConsole(level, message);
await sendFrontendLog(level, targetId, message);
}
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.trace(message);
void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.debug(message);
void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.info(message);
void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.warn(message);
void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.error(message);
void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
return (...items: unknown[]) => {
original(...items);
void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
};
}
export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
console.log = buildConsoleBridge("info", targetId, originalConsole.log);
console.info = buildConsoleBridge("info", targetId, originalConsole.info);
console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
console.error = buildConsoleBridge("error", targetId, originalConsole.error);
}

View File

@@ -0,0 +1,17 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/invoke.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
export async function invokeKsp<T>(targetId: FrontendLogTargetId, command: string, args?: Record<string, unknown>): Promise<T> {
frontendDebug(targetId, "Frontend IPC command requested", { command });
try {
const result = await invoke<T>(command, args);
frontendTrace(targetId, "Frontend IPC command completed", { command });
return result;
} catch (caughtError) {
frontendError(targetId, "Frontend IPC command failed", { command });
throw caughtError;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/splash.ts
// version: 2
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { SplashOrderDto } from "./bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts";
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
installFrontendConsoleBridge("splash");
let activeOpacityFrame: number | null = null;
function easeInOut(value: number): number {
return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
}
function normalizeDurationMs(value: number | null): number {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
}
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
if (activeOpacityFrame !== null) {
cancelAnimationFrame(activeOpacityFrame);
activeOpacityFrame = null;
}
element.style.opacity = fromOpacity.toString();
element.style.willChange = "opacity";
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
await new Promise<void>(resolve => {
const startedAt = performance.now();
const opacityDelta = toOpacity - fromOpacity;
const updateOpacity = (currentTime: number): void => {
const elapsedMs = currentTime - startedAt;
const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1);
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
if (rawProgress >= 1) {
element.style.opacity = toOpacity.toString();
element.style.willChange = "auto";
activeOpacityFrame = null;
resolve();
return;
}
activeOpacityFrame = requestAnimationFrame(updateOpacity);
};
activeOpacityFrame = requestAnimationFrame(updateOpacity);
});
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
}
function scrollToLatest(element: HTMLElement): void {
element.scrollTop = element.scrollHeight;
}
function addMessage(message: string, status: string | null): void {
const container = document.querySelector<HTMLElement>("#messages-container");
if (!container) {
return;
}
const line = document.createElement("div");
const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
line.className = `splash-message ${safeStatus}`;
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> {
frontendTrace("splash", "Splash order received", { action: order.action });
const container = document.querySelector<HTMLElement>("#splash-container");
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) {
return;
}
if (order.action === "fade_in") {
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
return;
}
if (order.action === "fade_out") {
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
}
}
async function initializeSplash(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("splash", "Wallet Desk splash frontend loaded", { windowLabel });
const container = document.querySelector<HTMLElement>("#splash-container");
if (container) {
container.style.opacity = "0";
container.style.willChange = "opacity";
frontendTrace("splash", "Splash container prepared for managed fade-in");
}
await listen<SplashOrderDto>("ksp-splash-order", event => {
void handleSplashOrder(event.payload);
});
frontendDebug("splash", "Splash lifecycle listener installed");
try {
await invokeKsp<void>("splash", "splash_frontend_ready");
} catch {
addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
if (container) {
container.style.opacity = "1";
container.style.willChange = "auto";
}
frontendError("splash", "Splash readiness command failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeSplash();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,28 @@
{
"name": "ksp-app-wallet-desk",
"private": true,
"version": "0.2.6",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
"@fortawesome/fontawesome-free": "^7.3",
"@tauri-apps/api": "^2.11",
"bootstrap": "^5.3",
"datatables.net-bs5": "^3.0",
"datatables.net-select-bs5": "^4.0",
"resize-observer-polyfill": "^1.5",
"simplebar": "^6.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11",
"@types/bootstrap": "^5.2",
"@types/node": "^26.1",
"sass-embedded": "^1.102",
"typescript": "^7.0",
"vite": "^8.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
// file: crates/ksp-app-wallet-desk/src/bootstrap.rs
// version: 2
//! Config composite and Logging bootstrap for Wallet Desk.
/// Crate-internal Logging startup state shared by the application state.
pub(crate) struct LoggingStartup {
/// Guard that keeps the installed Logging runtime alive.
pub(crate) guard: ksp_logging_lib::LoggingGuard,
/// Active managed profile when Config resolved one successfully.
pub(crate) active_profile_id: std::option::Option<String>,
/// Whether a transient in-memory fallback was installed.
pub(crate) fallback_active: bool,
/// Safe startup diagnostic retained for the shell.
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
enum LoggingStartupPlan {
Managed {
active_profile_id: String,
settings: ksp_logging_lib::LoggingSettings,
},
Fallback {
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
},
}
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
}
/// Initializes the Logging runtime from the Wallet Desk composite, with the same bounded fallback policy as Config Desk.
pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let plan = resolve_logging_startup(management);
return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
match guard {
std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, active_profile = active_profile_id.as_str(), "initialized Wallet Desk logging from composite-managed configuration");
std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::Some(active_profile_id),
fallback_active: false,
startup_diagnostic: std::option::Option::None,
})
},
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
}
},
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
},
};
}
/// Loads the concrete Wallet Desk Config composite using its registered logical `file_id` and autonomous default profile.
pub(crate) fn load_wallet_desk_composite(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return management.engine().load_resolved_composite(&file_id, std::option::Option::None);
}
/// Returns one required standard profile from the Wallet Desk composite after validating the referenced logical `file_id`.
pub(crate) fn required_composite_component_profile(
composite: &ksp_config_lib::ResolvedConfigComposite,
component_id: &str,
expected_file_id: &str,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigProfile> {
let component = composite.component(component_id);
let component = match component {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Wallet Desk composite is missing a required component")
.with_context("composite_file_id", composite.file_id().as_str())
.with_context("composite_profile_id", composite.profile_id())
.with_context("component_id", component_id),
);
},
};
if component.resolved().file_id().as_str() != expected_file_id {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Wallet Desk composite component references an unexpected Config document")
.with_context("composite_file_id", composite.file_id().as_str())
.with_context("composite_profile_id", composite.profile_id())
.with_context("component_id", component_id)
.with_context("expected_file_id", expected_file_id)
.with_context("actual_file_id", component.resolved().file_id().as_str()),
);
}
return std::result::Result::Ok(component.resolved().clone());
}
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
let logging_profile = match logging_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
if let std::result::Result::Err(error) = transport_profile {
return fallback_startup_plan(error);
}
let wallet_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_WALLET, ksp_config_lib::FILE_ID_STD_WALLET);
if let std::result::Result::Err(error) = wallet_profile {
return fallback_startup_plan(error);
}
let resolved = management.engine().resolve_logging_config_profile(&logging_profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
}
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
}
fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
}
fn initialize_planned_fallback_logging(
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard {
std::result::Result::Ok(value) => value,
std::result::Result::Err(fallback_error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Wallet Desk fallback Logging runtime")
.with_context("initial_error_domain", initial_error.code().domain())
.with_context("initial_error_code", initial_error.code().code())
.with_source(fallback_error),
);
},
};
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, error_domain = diagnostic.domain.as_str(), error_code = diagnostic.code.as_str(), "managed Logging configuration is unavailable; using transient in-memory fallback");
return std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::None,
fallback_active: true,
startup_diagnostic: std::option::Option::Some(diagnostic),
});
}

View File

@@ -0,0 +1,43 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 8
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
/// Composite-local identifier for the Logging standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Composite-local identifier for the Wallet standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_WALLET: &str = "wallet";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by the Wallet Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
/// Structured domain used by Wallet Desk HTTP Transport and balance operations.
pub(crate) const TRACING_DOMAIN_TRANSPORT: &str = "wallet.transport";
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
/// Structured domain used by native-save-picker OWNER transfer export operations.
pub(crate) const TRACING_DOMAIN_WALLET_EXPORT: &str = "wallet.export";
/// Structured domain used by native-picker Wallet transfer import operations.
pub(crate) const TRACING_DOMAIN_WALLET_IMPORT: &str = "wallet.import";
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
/// Structured domain used by Config-owned Wallet password candidate operations.
pub(crate) const TRACING_DOMAIN_WALLET_SECRET: &str = "wallet.secret";
/// Structured domain used by durable Wallet session, creation and unlock operations.
pub(crate) const TRACING_DOMAIN_WALLET_SESSION: &str = "wallet.session";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Wallet Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-wallet-desk";
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
/// Owning target for main-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
/// Native Wallet filename suffix accepted by the Wallet Desk inventory.
pub(crate) const WALLET_FILE_SUFFIX: &str = ".kspwallet";

View File

@@ -0,0 +1,80 @@
// file: crates/ksp-app-wallet-desk/src/dto_common.rs
// version: 4
//! Common Tauri DTOs shared by Wallet Desk shell commands.
use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts")]
pub(crate) struct CommandErrorDto {
/// Stable KSP error domain.
pub(crate) domain: String,
/// Stable KSP error code within the domain.
pub(crate) code: String,
/// Human-readable error message without arbitrary context fields.
pub(crate) message: String,
}
impl CommandErrorDto {
/// Builds a bounded safe projection from a KSP error.
#[must_use]
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
return Self {
domain: error.code().domain().to_owned(),
code: error.code().code().to_owned(),
message: error.message().to_owned(),
};
}
}
/// Safe application/runtime snapshot exposed to the Wallet Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")]
pub(crate) struct RuntimeStatusDto {
/// Cargo application version.
pub(crate) application_version: String,
/// Wallet Desk composite profile selected during bootstrap.
pub(crate) active_composite_profile: String,
/// Active configured Logging profile, or `None` while the transient fallback runtime is active.
pub(crate) active_logging_profile: std::option::Option<String>,
/// Standard Transport profile selected by the active composite.
pub(crate) active_transport_profile: String,
/// Standard Wallet profile selected by the active composite.
pub(crate) active_wallet_profile: String,
/// Number of logical Config resources registered by the current application runtime.
pub(crate) config_document_count: u32,
/// Effective Wallet directory after applying the optional profile subdirectory.
pub(crate) effective_wallets_directory: String,
/// Whether bootstrap created any missing component of the effective Wallet profile directory.
pub(crate) effective_wallets_directory_created_on_startup: bool,
/// Whether Wallet Desk had to install its transient in-memory Logging fallback.
pub(crate) fallback_logging_active: bool,
/// Whether bootstrap created the configured global Wallet root.
pub(crate) root_wallets_directory_created_on_startup: bool,
/// Current implementation phase exposed by the Wallet Desk shell.
pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
/// Number of Transport endpoints currently eligible for normal routing.
pub(crate) transport_available_endpoint_count: u32,
/// Safe cluster labels configured by the selected Transport profile.
pub(crate) transport_clusters: std::vec::Vec<String>,
/// Total number of configured Transport endpoints.
pub(crate) transport_endpoint_count: u32,
/// Safe provider labels configured by the selected Transport profile.
pub(crate) transport_providers: std::vec::Vec<String>,
/// Logical Transport role used by Wallet Desk balance reads.
pub(crate) transport_role: String,
/// Resolved global Wallet root configured by `cfg.std.wallet`.
pub(crate) wallets_directory: String,
/// Optional profile-relative Wallet subdirectory selected by the active composite.
pub(crate) wallets_subdirectory: std::option::Option<String>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_common.rs"]
mod tests;

View File

@@ -0,0 +1,54 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 8
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
/// Shared Wallet Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid");
/// Shared Wallet Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_lock_failed");
/// The Wallet Desk Config composite does not expose one of its required standard components with the expected `file_id`.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_target_invalid");
/// Wallet Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "logging_bootstrap_failed");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_operation_failed");
/// A Wallet balance request requires an authorized VIEW or OWNER session.
pub(crate) const ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_authorization_required");
/// The configured Wallet root or effective profile directory resolves to an unsupported filesystem object.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_invalid");
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed");
/// A native save-picker Wallet export destination is not a usable local filesystem path.
pub(crate) const ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_export_destination_invalid");
/// A native-picker Wallet transfer source is not a supported regular file or cannot be staged safely.
pub(crate) const ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_import_source_invalid");
/// Wallet import was requested without one previously inspected native-picker source.
pub(crate) const ERROR_CODE_WALLET_IMPORT_SOURCE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_import_source_missing");
/// Wallet Desk cannot enumerate or inspect its effective Wallet inventory directory.
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
/// No effective Config-owned Wallet password candidate is available for the explicit configured-secret unlock action.
pub(crate) const ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_secret_candidates_missing");
/// A requested Wallet inventory identifier is unsafe or does not resolve to an eligible regular `.kspwallet` file.
pub(crate) const ERROR_CODE_WALLET_SELECTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_selection_invalid");
/// A Wallet session lifecycle operation is incompatible with the current state.
pub(crate) const ERROR_CODE_WALLET_SESSION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_session_invalid");
/// The selected Wallet does not expose an enabled VIEW capability.
pub(crate) const ERROR_CODE_WALLET_VIEW_DISABLED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_view_disabled");

View File

@@ -0,0 +1,145 @@
// file: crates/ksp-app-wallet-desk/src/frontend_logging.rs
// version: 1
//! KSP-owned bridge for technical log events emitted by Wallet Desk frontend scripts.
use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Wallet Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/frontend_logging/FrontendLogPayloadDto.ts")]
pub(crate) struct FrontendLogPayloadDto {
/// Lowercase KSP log level.
pub(crate) level: String,
/// Whitelisted logical frontend target identifier.
pub(crate) target_id: String,
/// Rendered technical frontend message.
pub(crate) message: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FrontendLogLevel {
Debug,
Error,
Info,
Trace,
Warn,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FrontendLogTarget {
Frontend,
Main,
Splash,
}
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
let level = parse_level(payload.level.as_str());
let level = match level {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let target = parse_target(payload.target_id.as_str());
let target = match target {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
emit_validated_frontend_log(level, target, payload.message.as_str());
return std::result::Result::Ok(());
}
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
return match level.trim().to_ascii_lowercase().as_str() {
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
};
}
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
return match target_id.trim().to_ascii_lowercase().as_str() {
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
_ => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
},
};
}
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
return match target {
FrontendLogTarget::Frontend => emit_frontend_target(level, message),
FrontendLogTarget::Main => emit_main_target(level, message),
FrontendLogTarget::Splash => emit_splash_target(level, message),
};
}
fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Debug => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
},
FrontendLogLevel::Error => {
ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
},
FrontendLogLevel::Info => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
},
FrontendLogLevel::Trace => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
},
FrontendLogLevel::Warn => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
},
};
}
fn emit_main_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Debug => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
},
FrontendLogLevel::Error => {
ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
},
FrontendLogLevel::Info => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
},
FrontendLogLevel::Trace => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
},
FrontendLogLevel::Warn => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND_MAIN, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "main", "{message}")
},
};
}
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Debug => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
},
FrontendLogLevel::Error => {
ksp_logging_lib::error!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
},
FrontendLogLevel::Info => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
},
FrontendLogLevel::Trace => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
},
FrontendLogLevel::Warn => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET_FRONTEND_SPLASH, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "splash", "{message}")
},
};
}
#[cfg(test)]
#[path = "../unit_tests/frontend_logging.rs"]
mod tests;

View File

@@ -0,0 +1,241 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 13
//! Tauri desktop application shell for KSP Wallet management and inspection.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod app_state;
mod bootstrap;
mod constants;
mod dto_common;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod splash;
mod tauri;
mod transport_runtime;
mod tw_main;
mod tw_splash;
mod wallet_balance;
mod wallet_config;
mod wallet_export;
mod wallet_import;
mod wallet_inventory;
mod wallet_metadata;
mod wallet_secrets;
mod wallet_security;
mod wallet_session;
mod wallet_transfer;
/// Runs the KSP wallet desktop application.
pub use self::tauri::run;
/// Shared Wallet Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) use self::bootstrap::config_management;
/// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
pub(crate) use self::bootstrap::initialize_logging;
/// Loads the concrete Wallet Desk Config composite.
pub(crate) use self::bootstrap::load_wallet_desk_composite;
/// Resolves and validates one required standard-document component from the Wallet Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the Logging standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Composite-local identifier for the Wallet standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_WALLET;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by the Wallet Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Wallet Desk HTTP Transport and balance operations.
pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT;
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
/// Structured domain used by native-save-picker OWNER transfer export operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_EXPORT;
/// Structured domain used by native-picker Wallet transfer import operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_IMPORT;
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
/// Structured domain used by Config-owned Wallet password candidate operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SECRET;
/// Structured domain used by durable Wallet session, creation and unlock operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SESSION;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Wallet Desk.
pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Native Wallet filename suffix accepted by inventory operations.
pub(crate) use self::constants::WALLET_FILE_SUFFIX;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
pub(crate) use self::dto_common::RuntimeStatusDto;
/// Shared Wallet Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Wallet Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// The Wallet Desk Config composite is missing or misroutes one required standard component.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Wallet Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Wallet balance access requires an authorized VIEW or OWNER session.
pub(crate) use self::errors::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED;
/// The configured Wallet root or effective profile directory resolves to an unsupported filesystem object.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
/// Native save-picker Wallet export destination is invalid.
pub(crate) use self::errors::ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID;
/// Native-picker transfer source is invalid or cannot be staged.
pub(crate) use self::errors::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID;
/// Wallet import has no staged native-picker source.
pub(crate) use self::errors::ERROR_CODE_WALLET_IMPORT_SOURCE_MISSING;
/// Wallet inventory filesystem operation failed.
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// No effective Config-owned Wallet password candidate is available.
pub(crate) use self::errors::ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING;
/// Wallet selection identifier is invalid or no longer eligible.
pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID;
/// Wallet session lifecycle operation is incompatible with the current state.
pub(crate) use self::errors::ERROR_CODE_WALLET_SESSION_INVALID;
/// Selected Wallet does not expose an enabled VIEW capability.
pub(crate) use self::errors::ERROR_CODE_WALLET_VIEW_DISABLED;
/// Log payload sent by Wallet Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Creates the stable runtime identity for this Wallet Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings;
/// Safe and executable Transport runtime retained by the application state.
pub(crate) use self::transport_runtime::TransportRuntime;
/// Resolves the composite-selected Transport profile and builds the executable HTTP pool.
pub(crate) use self::transport_runtime::initialize_transport;
/// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_main::require_main_window;
/// Shows and focuses the main Wallet Desk window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) use self::tw_splash::splash_frontend_ready_service;
/// Safe balance snapshot for the currently authorized Wallet.
pub(crate) use self::wallet_balance::WalletBalanceDto;
/// Formats exact lamports as a decimal SOL string without floating point.
pub(crate) use self::wallet_balance::format_lamports_as_sol;
/// Resolved Wallet Config and directory preparation status captured during application bootstrap.
pub(crate) use self::wallet_config::WalletConfigStartup;
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
pub(crate) use self::wallet_config::initialize_wallet_config;
/// OWNER transfer export request carrying only the format selected by the frontend.
pub(crate) use self::wallet_export::WalletExportRequestDto;
/// Safe OWNER transfer export result without path or key material.
pub(crate) use self::wallet_export::WalletExportResultDto;
/// Builds the deterministic native save-picker filename for the current OWNER Wallet.
pub(crate) use self::wallet_export::wallet_export_default_filename;
/// Extracts a safe basename from the Rust-only native save-picker destination.
pub(crate) use self::wallet_export::wallet_export_destination_name;
/// Rust-only staged import source containing bounded zeroizing transfer bytes.
pub(crate) use self::wallet_import::PendingWalletImport;
/// Import request whose new native Wallet credentials move frontend -> Rust only.
pub(crate) use self::wallet_import::WalletImportRequestDto;
/// Native picker request containing only the expected transfer format.
pub(crate) use self::wallet_import::WalletImportSourceRequestDto;
/// Safe inspection projection for one staged external transfer source.
pub(crate) use self::wallet_import::WalletTransferInspectionDto;
/// Loads and validates one native-picker import source into bounded zeroizing Rust memory.
pub(crate) use self::wallet_import::stage_wallet_import_source;
/// Safe locked Wallet projection returned after selection.
pub(crate) use self::wallet_inventory::LockedWalletDto;
/// Locked inspection outcome exposed by Wallet inventory rows in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_inventory::WalletInspectionStatusDto;
/// Safe Wallet inventory row projection.
pub(crate) use self::wallet_inventory::WalletInventoryEntryDto;
/// Visual lock/error state exposed by Wallet inventory rows in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_inventory::WalletInventoryStateDto;
/// Request DTO used for one root-scoped Wallet selection.
pub(crate) use self::wallet_inventory::WalletSelectionRequestDto;
/// Enumerates native Wallet files under the effective Config-managed root.
pub(crate) use self::wallet_inventory::list_wallet_inventory;
/// Builds a validated destination path for one new root-scoped Wallet.
pub(crate) use self::wallet_inventory::new_wallet_destination;
/// Re-resolves one locked Wallet into Rust-only path state plus safe DTO.
pub(crate) use self::wallet_inventory::resolve_locked_wallet;
/// Re-inspects one root-scoped Wallet selection in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_inventory::select_locked_wallet;
/// OWNER alias update request carrying protected metadata only frontend -> Rust.
pub(crate) use self::wallet_metadata::WalletAliasUpdateRequestDto;
/// OWNER note append request carrying protected metadata only frontend -> Rust.
pub(crate) use self::wallet_metadata::WalletNoteAddRequestDto;
/// OWNER note delete request selecting one authorized stable note identifier.
pub(crate) use self::wallet_metadata::WalletNoteDeleteRequestDto;
/// OWNER note update request carrying protected metadata only frontend -> Rust.
pub(crate) use self::wallet_metadata::WalletNoteUpdateRequestDto;
/// Discovers Config-owned Wallet password candidates without exposing their names or values.
pub(crate) use self::wallet_secrets::discover_wallet_secret_candidates;
/// Returns the configured Wallet password candidate count without exposing their names or values.
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
/// New credential request moved frontend -> Rust only for one explicit VIEW/OWNER-authorized rotation.
pub(crate) use self::wallet_security::WalletPasswordRotationRequestDto;
/// Safe VIEW security state returned after OWNER-authorized strong disable/recreate operations.
pub(crate) use self::wallet_security::WalletViewSecurityStatusDto;
/// Builds the safe strong VIEW administration status from the resulting OWNER projection.
pub(crate) use self::wallet_security::view_security_status_from_authorized;
/// Authorized OWNER projection exposed only after successful creation.
pub(crate) use self::wallet_session::WalletAuthorizedDto;
/// Create request whose password strings move frontend -> Rust only.
pub(crate) use self::wallet_session::WalletCreateRequestDto;
/// Durable backend Wallet session retaining full paths and handles only in Rust.
pub(crate) use self::wallet_session::WalletSession;
/// Minimal safe projection of the durable Wallet session.
pub(crate) use self::wallet_session::WalletSessionDto;
/// Safe state code for the durable Wallet session in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_session::WalletSessionStateDto;
/// Capability targeted by one explicit unlock operation.
pub(crate) use self::wallet_session::WalletUnlockCapability;
/// Manual unlock request whose password moves frontend -> Rust only.
pub(crate) use self::wallet_session::WalletUnlockRequestDto;
/// Builds an authorized OWNER DTO from a Rust-only Wallet handle.
pub(crate) use self::wallet_session::owner_projection;
/// Builds an authorized VIEW DTO from a Rust-only Wallet handle.
pub(crate) use self::wallet_session::view_projection;
/// Shared import/export transfer format selected by Wallet Desk.
pub(crate) use self::wallet_transfer::WalletTransferFormatDto;

View File

@@ -0,0 +1,10 @@
// file: crates/ksp-app-wallet-desk/src/logging_runtime.rs
// version: 1
//! Wallet Desk Logging runtime identity helpers.
/// Creates the stable runtime identity for this Wallet Desk process launch.
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
}

View File

@@ -0,0 +1,58 @@
// file: crates/ksp-app-wallet-desk/src/main.rs
// version: 1
//! Binary entry point for the KSP wallet desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use fs2::FileExt; // rust-rules: trait-import
fn main() -> std::process::ExitCode {
let working_directory = configure_runtime_working_directory();
if let std::result::Result::Err(error) = working_directory {
eprintln!("cannot configure Wallet Desk runtime working directory: {error}");
return std::process::ExitCode::FAILURE;
}
let mut lock_path = std::env::temp_dir();
lock_path.push("com_sasedev_ksp_app_wallet_desk.lock");
let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
std::result::Result::Ok(file) => file,
std::result::Result::Err(error) => {
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
return std::process::ExitCode::FAILURE;
},
};
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
if error.kind() == std::io::ErrorKind::WouldBlock {
eprintln!("another ksp-app-wallet-desk instance is already running");
return std::process::ExitCode::FAILURE;
}
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
return std::process::ExitCode::FAILURE;
}
let _lock_file = lock_file;
let arguments = std::env::args_os().collect::<std::vec::Vec<std::ffi::OsString>>();
let run_result = ksp_app_wallet_desk_lib::run(arguments.as_slice());
return match run_result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
eprintln!("application error: {error}");
std::process::ExitCode::FAILURE
},
};
}
fn configure_runtime_working_directory() -> std::io::Result<()> {
#[cfg(debug_assertions)]
{
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
return std::env::set_current_dir(workspace_root);
}
#[cfg(not(debug_assertions))]
{
return std::result::Result::Ok(());
}
}

View File

@@ -0,0 +1,214 @@
// file: crates/ksp-app-wallet-desk/src/splash.rs
// version: 2
//! Common splash settings and frontend event contracts for Wallet Desk.
use ts_rs::TS; // rust-rules: trait-import
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
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 MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SplashSettings {
fade_in_ms: u32,
minimum_ms: u64,
fade_out_ms: u32,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
}
impl SplashSettings {
/// Resolves splash timings through the Config-owned environment snapshot.
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let 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 = match minimum {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
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::Err(error) => return std::result::Result::Err(error),
};
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
let minimum_ms = match minimum_ms {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
let fade_out_ms = match fade_out_ms {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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.
#[must_use]
pub(crate) const fn fallback() -> Self {
return Self {
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
};
}
/// 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]
pub(crate) const fn minimum_ms(self) -> u64 {
return self.minimum_ms;
}
/// Returns the configured fade-out duration.
#[must_use]
pub(crate) const fn fade_out_ms(self) -> u32 {
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.
#[must_use]
pub(crate) const fn minimum_source(self) -> &'static str {
return environment_source_code(self.minimum_source);
}
/// Returns the safe provenance code for the fade-out duration.
#[must_use]
pub(crate) const fn fade_out_source(self) -> &'static str {
return environment_source_code(self.fade_out_source);
}
/// Returns the expected additive backend lifecycle from readiness through fade-out.
#[must_use]
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
}
}
/// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/splash/SplashOrderDto.ts")]
pub(crate) struct SplashOrderDto {
/// Splash action name.
pub(crate) action: String,
/// Optional status text.
pub(crate) message: std::option::Option<String>,
/// Optional message severity used by the bottom message feed.
pub(crate) status: std::option::Option<String>,
/// Optional animation duration in milliseconds.
pub(crate) duration_ms: std::option::Option<u32>,
}
impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use]
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),
status: status.map(std::string::ToString::to_string),
duration_ms,
};
}
}
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
let parsed = value.parse::<u32>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
#[cfg(test)]
#[path = "../unit_tests/splash.rs"]
mod tests;

View File

@@ -0,0 +1,470 @@
// file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 11
//! Tauri runtime assembly for the KSP wallet desktop application.
use tauri_plugin_dialog::DialogExt; // rust-rules: trait-import
/// Runs the Wallet Desk application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
let context = tauri::generate_context!();
let runtime_layout = configure_packaged_runtime(&context);
if let std::result::Result::Err(error) = runtime_layout {
return std::result::Result::Err(error);
}
let app_state = crate::AppState::initialize(arguments);
let app_state = match app_state {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut builder = tauri::Builder::default();
builder = configure_state(builder, app_state);
builder = configure_plugins(builder);
builder = configure_commands(builder);
builder = configure_setup(builder);
let run_result = builder.run(context);
return match run_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Wallet Desk Tauri runtime")
.with_context("tauri_error", error.to_string()),
),
};
}
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
if cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
let resource_root = match resource_root {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
.with_context("tauri_error", error.to_string()),
);
},
};
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
let layout = match layout {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let working_directory = std::env::set_current_dir(layout.runtime_root());
return match working_directory {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
.with_source(error),
),
};
}
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
return builder.manage(app_state);
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tauri_plugin_dialog::init()).plugin(tracing_plugin);
}
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![
add_wallet_note,
clear_import_source,
create_wallet,
delete_wallet_note,
deselect_wallet,
disable_wallet_view,
emit_frontend_log,
export_wallet_transfer,
get_runtime_status,
import_wallet,
inspect_import_source,
list_wallets,
lock_wallet,
recreate_wallet_view,
refresh_wallet_balance,
refresh_wallets,
rotate_owner_password,
rotate_view_password,
select_wallet,
splash_frontend_ready,
unlock_wallet_owner_configured,
unlock_wallet_owner_manual,
unlock_wallet_view_configured,
unlock_wallet_view_manual,
update_wallet_alias,
update_wallet_note,
]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| {
let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error));
}
let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error));
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Wallet Desk Tauri shell setup completed");
return std::result::Result::Ok(());
});
}
#[tauri::command]
async fn add_wallet_note(
request: crate::WalletNoteAddRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.add_wallet_note(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn clear_import_source(state: tauri::State<'_, crate::AppState>) -> std::result::Result<(), crate::CommandErrorDto> {
let result = state.clear_import_source();
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn create_wallet(
request: crate::WalletCreateRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.create_wallet(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn delete_wallet_note(
request: crate::WalletNoteDeleteRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.delete_wallet_note(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn deselect_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletSessionDto, crate::CommandErrorDto> {
let result = state.deselect_wallet();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn disable_wallet_view(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletViewSecurityStatusDto, crate::CommandErrorDto> {
let result = state.disable_wallet_view().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn export_wallet_transfer(
app: tauri::AppHandle,
request: crate::WalletExportRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::option::Option<crate::WalletExportResultDto>, crate::CommandErrorDto> {
let format = request.format;
let default_filename = state.wallet_export_picker_filename(format);
let default_filename = match default_filename {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
let extensions = [format.default_extension()];
let selected = app
.dialog()
.file()
.set_title("Export Solana keypair — secret material")
.set_file_name(default_filename)
.add_filter("Solana keypair transfer", &extensions)
.blocking_save_file();
let selected = match selected {
std::option::Option::Some(value) => value,
std::option::Option::None => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_EXPORT, transfer_format = format.code(), "Wallet OWNER transfer export native save picker cancelled");
return std::result::Result::Ok(std::option::Option::None);
},
};
let path = selected.into_path();
let path = match path {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let error = ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID,
"Selected Wallet transfer export destination is not a local filesystem path",
);
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
},
};
let result = state.export_wallet_transfer(path, request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RuntimeStatusDto, crate::CommandErrorDto> {
let result = state.runtime_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn import_wallet(
request: crate::WalletImportRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.import_wallet(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn inspect_import_source(
app: tauri::AppHandle,
request: crate::WalletImportSourceRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::option::Option<crate::WalletTransferInspectionDto>, crate::CommandErrorDto> {
let cleared = state.clear_import_source();
if let std::result::Result::Err(error) = cleared {
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
}
let format = request.format;
let extensions = [format.default_extension()];
let selected = app
.dialog()
.file()
.set_title("Select Solana keypair transfer source")
.add_filter("Solana keypair transfer", &extensions)
.blocking_pick_file();
let selected = match selected {
std::option::Option::Some(value) => value,
std::option::Option::None => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, transfer_format = format.code(), "Wallet import native picker cancelled");
return std::result::Result::Ok(std::option::Option::None);
},
};
let path = selected.into_path();
let path = match path {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let error =
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Selected Wallet transfer source is not a local filesystem path");
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
},
};
let result = state.stage_import_source(path, format).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn recreate_wallet_view(
request: crate::WalletPasswordRotationRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletViewSecurityStatusDto, crate::CommandErrorDto> {
let result = state.recreate_wallet_view(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn refresh_wallet_balance(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletBalanceDto, crate::CommandErrorDto> {
let result = state.refresh_wallet_balance().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::list_wallet_inventory(root.as_path()).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn lock_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
let result = state.lock_wallet().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn refresh_wallets(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
let deselected = state.deselect_wallet();
if let std::result::Result::Err(error) = deselected {
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
}
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::list_wallet_inventory(root.as_path()).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn rotate_owner_password(
request: crate::WalletPasswordRotationRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.rotate_owner_password(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn rotate_view_password(
request: crate::WalletPasswordRotationRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.rotate_view_password(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn select_wallet(
request: crate::WalletSelectionRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
let result = state.select_wallet(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::splash_frontend_ready_service(app, window, &state).await;
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn unlock_wallet_owner_configured(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_owner_configured().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn unlock_wallet_owner_manual(
request: crate::WalletUnlockRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_owner_manual(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn unlock_wallet_view_configured(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_view_configured().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn unlock_wallet_view_manual(
request: crate::WalletUnlockRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_view_manual(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn update_wallet_alias(
request: crate::WalletAliasUpdateRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.update_wallet_alias(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn update_wallet_note(
request: crate::WalletNoteUpdateRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.update_wallet_note(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}

View File

@@ -0,0 +1,97 @@
// file: crates/ksp-app-wallet-desk/src/transport_runtime.rs
// version: 1
//! Composite-selected HTTP Transport runtime owned by Wallet Desk.
/// Safe and executable Transport runtime retained by the application state.
pub(crate) struct TransportRuntime {
pool: ksp_onchain_transport_lib::HttpTransportPool,
profile_id: String,
role: ksp_onchain_transport_lib::HttpRoleName,
}
impl TransportRuntime {
/// Returns the shareable HTTP Transport pool.
#[must_use]
pub(crate) const fn pool(&self) -> &ksp_onchain_transport_lib::HttpTransportPool {
return &self.pool;
}
/// Returns the composite-selected Transport profile identifier.
#[must_use]
pub(crate) fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the logical role used by Wallet Desk read-only RPC calls.
#[must_use]
pub(crate) const fn role(&self) -> &ksp_onchain_transport_lib::HttpRoleName {
return &self.role;
}
/// Returns safe configured cluster labels without endpoint URLs.
#[must_use]
pub(crate) fn clusters(&self) -> std::vec::Vec<String> {
let mut values = self.pool.snapshot().endpoints().iter().map(|endpoint| return endpoint.cluster().to_owned()).collect::<std::vec::Vec<_>>();
values.sort();
values.dedup();
return values;
}
/// Returns safe configured provider labels without endpoint URLs.
#[must_use]
pub(crate) fn providers(&self) -> std::vec::Vec<String> {
let mut values = self.pool.snapshot().endpoints().iter().map(|endpoint| return endpoint.provider().to_owned()).collect::<std::vec::Vec<_>>();
values.sort();
values.dedup();
return values;
}
}
/// Resolves the composite-selected Transport profile and builds the executable HTTP pool.
pub(crate) fn initialize_transport(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<TransportRuntime> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().resolve_transport_config_profile(&profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile_id = resolved.profile_id().to_owned();
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method("getBalance");
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Wallet Desk Transport registry does not expose getBalance",
));
},
};
let selection = pool.select_for_method(&role, descriptor);
if let std::result::Result::Err(error) = selection {
return std::result::Result::Err(error);
}
let snapshot = pool.snapshot();
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, transport_profile = profile_id.as_str(), transport_role = role.as_str(), endpoint_count = snapshot.endpoint_count(), available_endpoint_count = snapshot.available_endpoint_count(), "initialized Wallet Desk HTTP Transport from composite-managed configuration");
return std::result::Result::Ok(TransportRuntime { pool, profile_id, role });
}

View File

@@ -0,0 +1,48 @@
// file: crates/ksp-app-wallet-desk/src/tw_main.rs
// version: 1
//! Tauri-window helpers for the Wallet Desk main window.
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal main window label.
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
/// Resolves the required main window or returns a typed error.
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Wallet Desk main window is missing")
.with_context("window_label", WINDOW_LABEL_MAIN),
),
};
}
/// Shows and focuses the main Wallet Desk window.
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = crate::require_main_window(app);
let window = match window {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let shown = window.show();
if let std::result::Result::Err(error) = shown {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Wallet Desk main window")
.with_context("window_label", WINDOW_LABEL_MAIN)
.with_source(error),
);
}
let focused = window.set_focus();
if let std::result::Result::Err(error) = focused {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Wallet Desk main window")
.with_context("window_label", WINDOW_LABEL_MAIN)
.with_source(error),
);
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_MAIN, "main window shown and focused");
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,173 @@
// file: crates/ksp-app-wallet-desk/src/tw_splash.rs
// version: 2
//! Tauri-window lifecycle for the Wallet Desk splash window.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal splash window label.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
/// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Wallet Desk splash window is missing")
.with_context("window_label", WINDOW_LABEL_SPLASH),
),
};
}
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
.with_context("window_label", invoking_window.label()),
);
}
if !state.begin_splash_sequence() {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
return std::result::Result::Ok(());
}
let settings = state.splash_settings();
let lifecycle_started = std::time::Instant::now();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
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"
);
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);
}
let emitted = emit_message(&invoking_window, "Initialisation de Wallet Desk...", "info");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
if let std::result::Result::Err(error) = emitted {
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);
if let std::result::Result::Err(error) = show_main {
return std::result::Result::Err(error);
}
let destroyed = invoking_window.destroy();
if let std::result::Result::Err(error) = destroyed {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Wallet Desk splash window")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_source(error),
);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash window destroyed after main activation"
);
return std::result::Result::Ok(());
}
fn emit_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<()> {
let action = order.action.clone();
let emitted = window.emit(SPLASH_EVENT_NAME, order);
return match emitted {
std::result::Result::Ok(()) => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Wallet Desk splash order")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_context("action", action)
.with_source(error),
),
};
}

View File

@@ -0,0 +1,39 @@
// file: crates/ksp-app-wallet-desk/src/wallet_balance.rs
// version: 1
//! Safe Wallet balance projection produced by the typed HTTP Transport path.
use ts_rs::TS; // rust-rules: trait-import
/// Safe balance snapshot for the currently authorized Wallet.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts")]
pub(crate) struct WalletBalanceDto {
/// Optional Solana RPC API version reported by the selected node.
pub(crate) api_version: std::option::Option<String>,
/// Exact decimal account balance in lamports, encoded as text to avoid JavaScript integer precision loss.
pub(crate) lamports: String,
/// RPC context slot associated with this balance.
pub(crate) slot: u64,
/// Exact decimal SOL presentation derived from lamports without floating point.
pub(crate) sol: String,
/// Composite-selected Transport profile used for the RPC.
pub(crate) transport_profile: String,
/// Logical Transport role used for the RPC.
pub(crate) transport_role: String,
/// Root-scoped Wallet identifier whose authorized Pubkey was queried.
pub(crate) wallet_id: String,
}
/// Formats exact lamports as a decimal SOL string with nine fractional digits.
#[must_use]
pub(crate) fn format_lamports_as_sol(lamports: u64) -> String {
let whole = lamports / 1_000_000_000;
let fractional = lamports % 1_000_000_000;
return format!("{whole}.{fractional:09}");
}
#[cfg(test)]
#[path = "../unit_tests/wallet_balance.rs"]
mod tests;

View File

@@ -0,0 +1,179 @@
// file: crates/ksp-app-wallet-desk/src/wallet_config.rs
// version: 2
//! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation.
/// Resolved Wallet Config and directory preparation status captured during application bootstrap.
pub(crate) struct WalletConfigStartup {
composite_profile_id: String,
effective_directory_created_on_startup: bool,
resolved: ksp_config_lib::ResolvedWalletConfig,
root_directory_created_on_startup: bool,
}
impl WalletConfigStartup {
/// Returns the selected Wallet Desk composite profile.
pub(crate) fn composite_profile_id(&self) -> &str {
return self.composite_profile_id.as_str();
}
/// Reports whether bootstrap created at least one missing component of the effective Wallet profile directory.
pub(crate) const fn effective_directory_created_on_startup(&self) -> bool {
return self.effective_directory_created_on_startup;
}
/// Returns the resolved standard Wallet configuration.
pub(crate) const fn resolved(&self) -> &ksp_config_lib::ResolvedWalletConfig {
return &self.resolved;
}
/// Reports whether bootstrap created the configured global Wallet root.
pub(crate) const fn root_directory_created_on_startup(&self) -> bool {
return self.root_directory_created_on_startup;
}
}
/// Resolves the Wallet component selected by the Wallet Desk composite and prepares its global/effective directory tree.
pub(crate) fn initialize_wallet_config(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<WalletConfigStartup> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
if let std::result::Result::Err(error) = logging_profile {
return std::result::Result::Err(error);
}
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
if let std::result::Result::Err(error) = transport_profile {
return std::result::Result::Err(error);
}
let wallet_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_WALLET, ksp_config_lib::FILE_ID_STD_WALLET);
let wallet_profile = match wallet_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().resolve_wallet_config_profile(&wallet_profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preparation = prepare_wallet_directories(&resolved);
let preparation = match preparation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root_path = resolved.wallets_directory().to_string_lossy().into_owned();
let effective_path = resolved.effective_wallets_directory().to_string_lossy().into_owned();
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, composite_profile = composite.profile_id(), wallet_profile = resolved.profile_id(), root_path = root_path.as_str(), effective_path = effective_path.as_str(), root_created = preparation.root_created, effective_created = preparation.effective_created, "Wallet Desk Wallet directories are ready");
return std::result::Result::Ok(WalletConfigStartup {
composite_profile_id: composite.profile_id().to_owned(),
effective_directory_created_on_startup: preparation.effective_created,
resolved,
root_directory_created_on_startup: preparation.root_created,
});
}
struct WalletDirectoryPreparation {
effective_created: bool,
root_created: bool,
}
fn prepare_wallet_directories(resolved: &ksp_config_lib::ResolvedWalletConfig) -> ksp_core_lib::Result<WalletDirectoryPreparation> {
let root_created = ensure_global_wallet_root(resolved.wallets_directory());
let root_created = match root_created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_created = match resolved.wallets_subdirectory() {
std::option::Option::Some(subdirectory) => {
let created = ensure_profile_wallet_directory(resolved.wallets_directory(), subdirectory);
match created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
},
std::option::Option::None => root_created,
};
return std::result::Result::Ok(WalletDirectoryPreparation { effective_created, root_created });
}
fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
let metadata = std::fs::symlink_metadata(path);
return match metadata {
std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => directory_invalid(path, "configured Wallet root cannot be a symbolic link"),
std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false),
std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let creation = std::fs::create_dir_all(path);
match creation {
std::result::Result::Ok(()) => std::result::Result::Ok(true),
std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be created", error),
}
},
std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be inspected", error),
};
}
fn ensure_profile_wallet_directory(root: &std::path::Path, subdirectory: &std::path::Path) -> ksp_core_lib::Result<bool> {
let mut current = root.to_path_buf();
let mut created = false;
for component in subdirectory.components() {
let normal = match component {
std::path::Component::Normal(value) => value,
_ => return directory_invalid(subdirectory, "Wallet profile subdirectory contains an unsafe path component"),
};
current.push(normal);
let metadata = std::fs::symlink_metadata(current.as_path());
match metadata {
std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => {
return directory_invalid(current.as_path(), "Wallet profile directory path traverses a symbolic link");
},
std::result::Result::Ok(metadata) if metadata.is_dir() => {},
std::result::Result::Ok(_) => return directory_invalid(current.as_path(), "Wallet profile directory path contains a non-directory entry"),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let creation = std::fs::create_dir(current.as_path());
if let std::result::Result::Err(error) = creation {
return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be created", error);
}
created = true;
},
std::result::Result::Err(error) => {
return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be inspected", error);
},
}
}
return std::result::Result::Ok(created);
}
fn directory_invalid<T>(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Result<T> {
let path_text = path.to_string_lossy().into_owned();
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, "Wallet directory preparation rejected an invalid filesystem object");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_INVALID, "Configured Wallet directory is invalid")
.with_context("path", path_text)
.with_context("reason", reason),
);
}
fn directory_prepare_error<T>(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Result<T> {
let path_text = path.to_string_lossy().into_owned();
let source_kind = std::format!("{:?}", source.kind());
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, source_kind = source_kind.as_str(), "Wallet directory preparation failed");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED, "Configured Wallet directory cannot be prepared")
.with_context("path", path_text)
.with_context("reason", reason)
.with_source(source),
);
}
#[cfg(test)]
#[path = "../unit_tests/wallet_config.rs"]
mod tests;

View File

@@ -0,0 +1,52 @@
// file: crates/ksp-app-wallet-desk/src/wallet_export.rs
// version: 1
//! OWNER-only native save-picker Wallet transfer export contracts.
use ts_rs::TS; // rust-rules: trait-import
/// OWNER export request containing only the explicit transfer format.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_export/WalletExportRequestDto.ts")]
pub(crate) struct WalletExportRequestDto {
/// Secret-transfer encoding selected before the Rust-owned save picker opens.
pub(crate) format: crate::WalletTransferFormatDto,
}
/// Safe result returned after an OWNER keypair export completes.
#[derive(serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_export/WalletExportResultDto.ts")]
pub(crate) struct WalletExportResultDto {
/// Basename only for the newly created export file; the parent path remains Rust-only.
pub(crate) destination_name: String,
/// Explicit transfer format used by the export.
pub(crate) format: crate::WalletTransferFormatDto,
/// Root-scoped native Wallet identifier that authorized the export.
pub(crate) wallet_id: String,
}
/// Builds a deterministic native-save default filename without exposing Wallet metadata or Pubkey.
#[must_use]
pub(crate) fn wallet_export_default_filename(wallet_id: &str, format: crate::WalletTransferFormatDto) -> String {
let stem = wallet_id.strip_suffix(crate::WALLET_FILE_SUFFIX).unwrap_or(wallet_id);
let stem = if stem.is_empty() { "wallet" } else { stem };
return format!("{stem}-solana-keypair.{}", format.default_extension());
}
/// Extracts one safe basename from a native save-picker destination.
pub(crate) fn wallet_export_destination_name(destination: &std::path::Path) -> ksp_core_lib::Result<String> {
let name = destination.file_name().and_then(std::ffi::OsStr::to_str);
return match name {
std::option::Option::Some(value) if !value.is_empty() => std::result::Result::Ok(value.to_owned()),
_ => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID,
"Selected Wallet export destination has no usable local filename",
)),
};
}
#[cfg(test)]
#[path = "../unit_tests/wallet_export.rs"]
mod tests;

View File

@@ -0,0 +1,146 @@
// file: crates/ksp-app-wallet-desk/src/wallet_import.rs
// version: 3
//! Native-picker transfer import staging with bounded secret material kept only in Rust.
use tokio::io::AsyncReadExt; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: trait-import
/// Request that chooses only the transfer format; the source path comes from the native Rust picker.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts")]
pub(crate) struct WalletImportSourceRequestDto {
/// Transfer encoding expected for the selected external file.
pub(crate) format: crate::WalletTransferFormatDto,
}
/// Safe projection returned after validating one native-picker import source.
#[derive(Clone, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts")]
pub(crate) struct WalletTransferInspectionDto {
/// Transfer format used to validate the secret source.
pub(crate) format: crate::WalletTransferFormatDto,
/// Public identity derived from the imported keypair.
pub(crate) pubkey: String,
/// Basename only; no parent path is ever projected.
pub(crate) source_name: String,
}
/// Import request whose new native Wallet credentials move frontend -> Rust only.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts")]
pub(crate) struct WalletImportRequestDto {
/// Optional protected internal alias stored in the new native Wallet.
pub(crate) alias: std::option::Option<String>,
/// Root-scoped destination filename under the effective Wallet directory.
pub(crate) filename: String,
/// Optional first protected note stored in the imported Wallet.
pub(crate) initial_note: std::option::Option<String>,
/// New OWNER password transported only frontend -> Rust.
pub(crate) owner_password: String,
/// Optional new VIEW password; `None` keeps VIEW disabled.
pub(crate) view_password: std::option::Option<String>,
}
/// Rust-only staged transfer source. Secret bytes are bounded and zeroized on drop.
pub(crate) struct PendingWalletImport {
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
format: crate::WalletTransferFormatDto,
inspection: WalletTransferInspectionDto,
}
impl PendingWalletImport {
/// Returns the safe inspection projection without exposing source bytes or path.
#[must_use]
pub(crate) fn safe_projection(&self) -> WalletTransferInspectionDto {
return self.inspection.clone();
}
/// Returns the bounded secret transfer bytes for one immediate Wallet import operation.
#[must_use]
pub(crate) fn source_bytes(&self) -> &[u8] {
return self.bytes.as_slice();
}
/// Returns the stable Wallet transfer format corresponding to the staged source.
#[must_use]
pub(crate) const fn wallet_format(&self) -> ksp_wallet_lib::WalletTransferFormat {
return self.format.wallet_format();
}
}
/// Reads, bounds and validates one native-picker transfer source while retaining no external path.
pub(crate) async fn stage_wallet_import_source(
source: std::path::PathBuf,
format: crate::WalletTransferFormatDto,
) -> ksp_core_lib::Result<PendingWalletImport> {
let metadata = tokio::fs::symlink_metadata(source.as_path()).await;
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot inspect the selected Wallet transfer source")
.with_source(error),
);
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID,
"Selected Wallet transfer source must be one regular non-symlink file",
));
}
let source_name = source.file_name().and_then(std::ffi::OsStr::to_str);
let source_name = match source_name {
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
_ => "selected-transfer".to_owned(),
};
let file = tokio::fs::File::open(source.as_path()).await;
let file = match file {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot open the selected Wallet transfer source").with_source(error),
);
},
};
let maximum_source_bytes = format.maximum_source_bytes();
let read_limit = maximum_source_bytes.saturating_add(1);
let read_limit = u64::try_from(read_limit);
let read_limit = match read_limit {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Wallet transfer source bound exceeds platform range").with_source(error),
);
},
};
let mut reader = file.take(read_limit);
let mut bytes = zeroize::Zeroizing::new(std::vec::Vec::with_capacity(maximum_source_bytes.saturating_add(1)));
let read_result = reader.read_to_end(&mut bytes).await;
if let std::result::Result::Err(error) = read_result {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot read the selected Wallet transfer source").with_source(error),
);
}
if bytes.len() > maximum_source_bytes {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID,
"Selected Wallet transfer source exceeds the supported format bound",
));
}
let inspection = ksp_wallet_lib::inspect_wallet_transfer(bytes.as_slice(), format.wallet_format());
let inspection = match inspection {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dto = WalletTransferInspectionDto { format, pubkey: inspection.pubkey().to_string(), source_name };
return std::result::Result::Ok(PendingWalletImport { bytes, format, inspection: dto });
}
#[cfg(test)]
#[path = "../unit_tests/wallet_import.rs"]
mod tests;

View File

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

Some files were not shown because too many files have changed in this diff Show More