Compare commits
299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cc94ebb46 | |||
| 8e8f1f0b4a | |||
| 78e015413b | |||
| 2f0eb316f5 | |||
| 55325ffd32 | |||
| ad02b6f13c | |||
| 47b12e14e6 | |||
| c43d4d7c38 | |||
| b76baf6835 | |||
| c83e3261d0 | |||
| 28ca5bdac5 | |||
| afd4c770f9 | |||
| ecc5607fd4 | |||
| 11e4cde77f | |||
| 62ed72f2b5 | |||
| 1a07574bae | |||
| 29a888d1dc | |||
| 3d307be645 | |||
| 7dfe892467 | |||
| 2cf00e9426 | |||
| 4eba9be48c | |||
| bebfc63b75 | |||
| 0319794e59 | |||
| bcf2f16c05 | |||
| 27d4cb1f36 | |||
| c1f61a380f | |||
| 5e5ed6d1ab | |||
| 31461cf9be | |||
| 8c8b7ee986 | |||
| 5bfe6f0820 | |||
| 8c4e835fdd | |||
| 695d1080bd | |||
| e774a065b4 | |||
| 29f27ae109 | |||
| 0ee28eeb95 | |||
| 900444bff5 | |||
| 99477d0d2b | |||
| 9148920f42 | |||
| e42688794a | |||
| c73e4c0aec | |||
| e701267135 | |||
| 4f71dbc62d | |||
| ca6af3d904 | |||
| 233aaf9bd8 | |||
| d3bb026345 | |||
| d363991f78 | |||
| f0493df09e | |||
| fe46cbe3bd | |||
| f1e365ec97 | |||
| 2042482592 | |||
| 7d0fe8ac5f | |||
| a00e485ebb | |||
| 9d351dd1c3 | |||
| 5ac2197b6e | |||
| c5a70a2eff | |||
| e7c3379f44 | |||
| afc50c1e48 | |||
| 5a165296d9 | |||
| 0d01017c31 | |||
| d1d683d101 | |||
| f7e4ea09bc | |||
| 9a7eda1aa6 | |||
| db1f8d4a53 | |||
| 9852613426 | |||
| 81eb295a09 | |||
| b5a1293953 | |||
| 251f9f9ad8 | |||
| 98708df6e0 | |||
| 2db3c4be78 | |||
| 69294a153f | |||
| 2fdff17651 | |||
| 87d7314bf4 | |||
| 5aeff5ca14 | |||
| 98093d859b | |||
| 4021d08541 | |||
| 5c97a772be | |||
| 60afb51451 | |||
| f4413ebbb0 | |||
| be2a06bc80 | |||
| 2dbddfe367 | |||
| 126e8abfcd | |||
| 3fd540a9db | |||
| 4f92fb03f1 | |||
| f42fa8f4f1 | |||
| a8e980b225 | |||
| afaa9c0239 | |||
| 7d456b41dc | |||
| 0edeed1c48 | |||
| be5e3464ee | |||
| 1967e845b0 | |||
| af807afff5 | |||
| 7f567bc1bb | |||
| 09aacc36ab | |||
| 15b129ad04 | |||
| 6ddf2b4995 | |||
| dcabceb52f | |||
| 51bcf870d3 | |||
| c6c5793c61 | |||
| d98ee736be | |||
| 39dbd86f1e | |||
| c938b2409e | |||
| 5b30bb9948 | |||
| 813a45385a | |||
| 21ed5f88da | |||
| 3198cc5666 | |||
| 4568d94a62 | |||
| d32d8de3a5 | |||
| a9fb6a7ac8 | |||
| 433e69272a | |||
| affa18a6e9 | |||
| 05aec88178 | |||
| b32de48c0b | |||
| 15e3402e1c | |||
| dca0a4b809 | |||
| 20320dba7b | |||
| e4bbc78a41 | |||
| feb9befb35 | |||
| a038194679 | |||
| 3172cda241 | |||
| 61fba107ef | |||
| 835de48cb7 | |||
| 21de590aea | |||
| 3081bd1085 | |||
| bdcab1e1a7 | |||
| d70c3a1672 | |||
| 7cdf5e80c9 | |||
| d8bfd7cd2e | |||
| 9c0d4fc197 | |||
| 7c12ec886b | |||
| 7eb6dec809 | |||
| 68f4384c5b | |||
| 9cc140fb84 | |||
| 3b64d1e0ec | |||
| 56b9ce6abc | |||
| 1c8d69778b | |||
| 8e739b9e55 | |||
| 53dbb5bccd | |||
| 92224e5ac6 | |||
| cbb4e7b0de | |||
| 38fd62c256 | |||
| f2a3ec62aa | |||
| b3363073c4 | |||
| c94a54f3e3 | |||
| 0871b85df9 | |||
| 315e7e67e5 | |||
| df95f2f558 | |||
| f0865d5137 | |||
| 4d77b607ea | |||
| 307711f873 | |||
| 5aa7b45840 | |||
| 3c5786f273 | |||
| 3b4d355537 | |||
| 628b4f12f2 | |||
| 66deaf8245 | |||
| 0e256a8ecf | |||
| 9eb0e19d81 | |||
| 74686892e9 | |||
| 1391858972 | |||
| 98bf88e431 | |||
| f0f444bc86 | |||
| d17161234a | |||
| 93199d1856 | |||
| 6fefc64e75 | |||
| 4c540d67a7 | |||
| b67fa89f44 | |||
| 435126f67a | |||
| 8721e54b18 | |||
| 6e3a0fa034 | |||
| 34637848eb | |||
| 778ea58ee1 | |||
| b0461f15ec | |||
| d5df0fe9af | |||
| ab29dc51bb | |||
| b6908cb573 | |||
| b64a799c85 | |||
| cf4b28df2b | |||
| 9e8fd53291 | |||
| 3c9c1d1349 | |||
| 79fee574d9 | |||
| fca9abb11a | |||
| ba8f42f9b1 | |||
| 362123f357 | |||
| 5c0be421da | |||
| 738b340cc7 | |||
| 92a2c4fff3 | |||
| 946d88322b | |||
| d370027c02 | |||
| bd561cad47 | |||
| 6907e4bbb8 | |||
| c07db925b1 | |||
| 263464ddd7 | |||
| 4b350b4161 | |||
| ecbd6b746d | |||
| 8ba554306a | |||
| 6dcd278459 | |||
| 4dcdbfaa55 | |||
| 42326e7f02 | |||
| 2b7afab663 | |||
| 7d03825ec6 | |||
| 4db4087b8a | |||
| 1a9edd14e1 | |||
| 787df2861f | |||
| 60b346c552 | |||
| f11f0921b0 | |||
| 4c9086e2f0 | |||
| a207cf7161 | |||
| 63e711629f | |||
| 7aa38b0daf | |||
| 2132dfd884 | |||
| 76bfce17c1 | |||
| 6eb4d71043 | |||
| 7eb0381932 | |||
| 3fa4ab251a | |||
| 0e72914237 | |||
| f9f9ac79b6 | |||
| 1a57cdeef0 | |||
| 927b9e4709 | |||
| b7d9e7fd1f | |||
| 136ea6d76e | |||
| 8c65096f4c | |||
| cb142b1cb8 | |||
| 08c8046262 | |||
| c0b131bf6f | |||
| 5bf9651038 | |||
| e91bf36e9e | |||
| 1125ade4c1 | |||
| 518cc9257f | |||
| 46afe10423 | |||
| 25edcc231e | |||
| 6094d33120 | |||
| dcd23abb92 | |||
| 36b98e0abc | |||
| 3c069347b7 | |||
| a0a5595186 | |||
| 6c1172efc3 | |||
| d5536b0778 | |||
| c6794af05b | |||
| bcda6db11f | |||
| 51b5114ae2 | |||
| c97a8739ab | |||
| c2ccef3849 | |||
| 58134c81d1 | |||
| 5b3967327a | |||
| 804b92b957 | |||
| e6772c109d | |||
| 7e84522787 | |||
| 37461ec248 | |||
| 4252a21140 | |||
| 7a889dd466 | |||
| d3cc7c93f0 | |||
| 561b6678ed | |||
| e500024c3a | |||
| 033800f897 | |||
| cf8d79567d | |||
| cb19742cf7 | |||
| c03a3eaf5d | |||
| f7acac3fc8 | |||
| 039b93cfb3 | |||
| c9a0f7fd37 | |||
| 704110d509 | |||
| fb32ed03f8 | |||
| 4439589f0d | |||
| 1e1bc0d421 | |||
| bd26d4f636 | |||
| 2a7f3e8f40 | |||
| 1d37f04d63 | |||
| 1ccf11bcc1 | |||
| d06284fca2 | |||
| fff69ead9c | |||
| c93a833bac | |||
| 05e0e96083 | |||
| b57f796187 | |||
| ecfb9500eb | |||
| e14ce1c36f | |||
| 83cb861e54 | |||
| 1d1bc6a4d6 | |||
| c4636ac8b9 | |||
| f15448ff6e | |||
| bec1f2ec08 | |||
| f625ee5979 | |||
| e68f073505 | |||
| bffb4f9a31 | |||
| 9059a2dc45 | |||
| 6c3ecf1f18 | |||
| 79b67f8eae | |||
| e0a7ac0bf8 | |||
| 598474438b | |||
| f5d98c4e69 | |||
| 14bcbf2cfb | |||
| ac1b1033c4 | |||
| bc71fba289 | |||
| 9c1568ee1c | |||
| c1cea6e813 | |||
| ed978179d8 | |||
| babe7d9f2b | |||
| d3fc0c6d69 | |||
| 0cff0406ab | |||
| d98d152f08 | |||
| 624202c363 |
76
.env.example
76
.env.example
@@ -1,12 +1,80 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 2
|
# version: 10
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
# PublicNode Mainnet personal token used as secret x-token metadata by the committed Mainnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real value only in the process environment or local .env; do not assume that a Testnet token is authorized on Mainnet.
|
||||||
|
# KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
|
# PublicNode Testnet personal token used as secret x-token metadata by the committed Testnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real value only in the process environment or local .env; do not assume that a Mainnet token is authorized on Testnet.
|
||||||
|
# KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
|
# OrbitFlare Solana Free License Key used as secret x-token metadata by the committed Devnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real ORBIT-* value only in the process environment or local .env; do not substitute the Customer API X-ORBIT-KEY.
|
||||||
|
# KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
|
# Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json.
|
||||||
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
|
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
||||||
|
|
||||||
|
# Birdeye Standard API key used by the optional `all_free` profile in config/std.offchain_transport.json.
|
||||||
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
|
# KSP_SECRET_BIRDEYE_API_KEY=replace-me
|
||||||
|
|
||||||
|
# CoinGecko Demo API key used by the optional `all_free` Off-chain Transport profile.
|
||||||
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
|
# KSP_SECRET_COINGECKO_DEMO_API_KEY=replace-me
|
||||||
|
|
||||||
|
# CoinMarketCap Basic API key used by the optional `all_free` Off-chain Transport profile.
|
||||||
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
|
# KSP_SECRET_COINMARKETCAP_API_KEY=replace-me
|
||||||
|
|
||||||
|
# Public Solana pair address used by DexScreener for explicit SOL/USD pair lookup in the optional `all_free` profile.
|
||||||
|
# This value is intentionally public and must identify the exact pair chosen by the operator.
|
||||||
|
# KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS=replace-with-solana-pair-pubkey
|
||||||
|
|
||||||
|
# Jupiter Free API key used by the optional `all_free` Off-chain Transport profile.
|
||||||
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
|
# KSP_SECRET_JUPITER_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
1
.gitignore
vendored
@@ -36,6 +36,7 @@ yarn.lock
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
# Operating system files
|
# Operating system files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
102
CHANGELOG.md
102
CHANGELOG.md
@@ -1,10 +1,110 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 19 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
|
## 0.3.1 — Store API RAW foundation — 2026-08-29
|
||||||
|
|
||||||
|
`0.3.1` introduit `ksp-store-api` comme contrat backend-agnostic de persistence N1 RAW, sans runtime Store ni backend physique. La release stabilise deux familles réellement convergentes : `RawTransaction` avec payload canonique opaque/versionné, identité réseau+signature et observations d’acquisition séparées, puis `RawAccountState` avec bytes complets, identité réseau+pubkey+slot+hash et observations pouvant conserver les enrichissements Yellowstone sans les confondre avec l’état canonique. `TransactionStatusObservation` reste reporté faute de convergence sémantique suffisante entre snapshot HTTP, transition WebSocket et update Yellowstone ; `logsSubscribe`, slot/root/slotsUpdates et vote restent event-only candidats, `RawBlock` reste une idée conditionnelle et Yellowstone `Entry` reste rejeté de la taxonomie active.
|
||||||
|
|
||||||
|
La façade publique conserve un modèle objet sans SQL ni rows backend, des primitives de provenance/hash/timestamps bornées, des queries cursorisées sans plafond métier KSP arbitraire, des outcomes d’idempotence/conflit, dix capabilities fines object-safe et un lifecycle logique de rétention transactionnelle `Full -> Compacted -> Archived -> Purged`. Le tombstone minimal empêche le rebackfill normal après purge tandis que `ForceRehydrate` reste une intention distincte ; une course de compare-and-transition est représentée par `ExpectedStateMismatch` plutôt que par un overwrite silencieux. Le backlog, le batch-size, la priorité, la policy de processing, la compression/archive physique, les notifications runtime et les couches STRUCTURAL/DECODED/DOMAIN restent hors Store API. Le graphe normal final de `ksp-store-api` reste strictement limité à `ksp-core-lib`; aucun PostgreSQL, Tokio, serde, Config, Transport, Program, Logging ou backend concret n’entre dans la crate.
|
||||||
|
|
||||||
|
Les canaris de clôture verrouillent 60 exports crate-root, 10 capabilities, l’inventaire exact des modules RAW, l’implémentabilité par un backend externe, les bornes adversariales, la redaction des `Debug`, la frontière Interface/Store et l’absence de surface N2/N3/N4. Le gate technique de référence a été exécuté après `cargo clean` et passe audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés des crates, `cargo test --workspace`, builds Tauri des trois Desk et graphes Cargo ; les gates documentaires suivants restent également verts. Le redécoupage final prépare trois releases Store/PostgreSQL où `ksp-store-lib` et `ksp-store-postgres-lib` avancent toujours ensemble : `0.3.2` pour la fondation runtime/backend, `0.3.3` pour la vertical slice `RawTransaction`, puis `0.3.4` pour `RawAccountState` et la complétude RAW. `prompts/021-V0_3_2_START_PROMPT.md` ouvre donc uniquement la fondation conjointe Store/PostgreSQL, avec `tokio-postgres` comme driver retenu mais pooling, TLS, migrations et Config à réauditer avant implémentation lourde.
|
||||||
|
|
||||||
|
## 0.2.14 — Program API foundation — 2026-08-28
|
||||||
|
|
||||||
|
`0.2.14` introduit `ksp-program-api` comme première API publique extensible du domaine Program, volontairement limitée au décodage d’instructions et indépendante des runtimes supérieurs. La façade réexporte les contrats Core/Interface nécessaires puis possède `ProgramInstructionRecognition` (`NoMatch`, `ProgramMatch`, `ExactMatch`), `ProgramInstructionDecodeOutcome<Decoded>` (`Decoded`, `Unsupported`) et le trait `ProgramInstructionDecoder: Send + Sync`. L’output `Decoded` reste possédé par l’implémentation et ne reçoit aucun bound implicite `Debug`, `Clone`, `Send` ou `Sync`; les erreurs réelles restent dans le `Result` Core. Les Program IDs sont des `Pubkey` opaques : une implémentation externe peut prendre en charge un programme absent du registry Core sans enum centrale fermée, `Any`, JSON, descriptor global ni registry runtime.
|
||||||
|
|
||||||
|
Le graphe normal final reste strictement `ksp-program-api -> ksp-core-lib + ksp-interface-lib`, Interface dépendant elle-même de Core. Aucun `ksp-program-lib`, codec, serde, logging, réseau, filesystem, environnement, Store, Materializer, Wallet, Config ou Tauri n’entre dans cette foundation. Les canaris public API, implémentation externe, dependency firewall, release completeness et hardening couvrent notamment l’inventaire exact de dix exports crate-root, les trois modules de production, un Program Pubkey non enregistré, l’input Interface maximal de 255 accounts / 10 240 bytes, l’absence d’echo automatique d’un payload hostile et l’absence de claim `dyn` hétérogène. `pre.005-fix.001` corrige uniquement un faux positif cross-crate du scanner Logging provoqué par le motif de test recherché, sans changement fonctionnel. Les gates `pre.006` et `pre.007` passent ensuite audits Rust/Markdown, `cargo check`, Clippy, les 18 tests Program API, ownership Logging et le workspace complet avant la réconciliation documentaire finale.
|
||||||
|
|
||||||
|
`prompts/020-V0_3_1_START_PROMPT.md` ouvre `0.3.1 — Store RAW foundation` exclusivement depuis le tag stable `v0.2.14`. Le gate `pre.001` exige également l’archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` afin d’auditer l’ancien `ks-store`, ses migrations PostgreSQL et ses contrats N1/N2/N3 sous une matrice `REPRENDRE / REDESSINER / REPORTER / REJETER`. L’archive reste une source historique uniquement : `0.3.1` doit créer `ksp-store-api` et `ksp-store-lib` avec PostgreSQL de référence et persistence **RAW seulement**, sans aspirer les contrats CORE/DECODE/SPECIALIZED, les jobs/workers, l’Interface `0.3.2` ni la configuration produit.
|
||||||
|
|
||||||
|
## 0.2.13 — Interface / wire foundation — 2026-08-28
|
||||||
|
|
||||||
|
`0.2.13` introduit `ksp-interface-lib` comme première façade wire officielle KSP, volontairement passive et Program-facing. La surface stable réexporte le `Pubkey` canonique de Core, ajoute `ProgramAccountMeta` et `ProgramInstruction` à champs privés avec accessors explicites, conserve l’ordre et les doublons des account metas, accepte les Program Pubkeys opaques et borne l’admission à **255 account metas** et **10 240 octets** de data. Les deux bornes sont des limites d’admission Interface et ne prétendent pas garantir à elles seules le fit d’une transaction Solana top-level. Les erreurs réutilisent le contrat Core `Error/Result` avec uniquement `field`, `actual_len` et `maximum_len`, tandis que le `Debug` de l’instruction n’expose que `program_id`, `account_count` et `data_len`.
|
||||||
|
|
||||||
|
La dependency direction finale reste strictement `ksp-interface-lib -> ksp-core-lib -> solana-pubkey`. Aucun `serde`, codec générique (`borsh`, `wincode`, `bincode`), `solana-instruction`, runtime réseau ou logging n’est ajouté à cette foundation. Les canaris public API, consumer externe, dependency firewall et release completeness verrouillent les six exports crate-root, les quatre modules de production, l’absence de narrowing/codec caché, le transfert des `Vec` admis sans réallocation interne et la non-répétition de matériel hostile dans les erreurs. `pre.005-fix.001` corrige uniquement un faux positif du scanner Logging provoqué par le littéral de test `tracing::`, sans changement de production ni de dépendance. Les gates `pre.006` et `pre.007` passent ensuite audits Rust/Markdown, `cargo check`, Clippy, tests Interface, ownership Logging, workspace complet et graphes Cargo avant la réconciliation documentaire finale.
|
||||||
|
|
||||||
|
`prompts/019-V0_2_14_START_PROMPT.md` ouvre `0.2.14 — Program API foundation` exclusivement depuis le tag stable `v0.2.13`. Le gate `pre.001` de cette prochaine release exige aussi l’archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` afin d’auditer les anciens contrats decoder/executor sous une matrice `REPRENDRE / REDESSINER / REPORTER / REJETER`, sans faire de kbot3 une autorité architecturale ni recréer son monolithe `ks-lib`.
|
||||||
|
|
||||||
|
## 0.2.12 — SOL Prices Desk + projection prix Wallet — 2026-08-27
|
||||||
|
|
||||||
|
`0.2.12` ajoute `ksp-app-solprices-desk`, troisième Desk Tauri spécialisé de KSP, comme HID strictement provider-neutral au-dessus de `ksp-offchain-transport-lib`. Son composite dédié sélectionne `std.logging` et `std.offchain_transport`; la vue Prices expose l’inventaire des huit providers SOL/USD, disponibilité, sémantique, auth, prix exact sous forme de chaîne, timestamps provider/KSP distincts et retry deadline sans inventer de fraîcheur. Les refresh manuel individuel, sélectionné et global passent exclusivement par `MarketPriceService`; les batchs sont bornés à `1..=64`, les états `in_flight` restent backend-owned et aucun polling, scheduler, fallback, moyenne ou prix canonique n’est introduit dans le Desk. Le frontend conserve l’absence de réseau/persistence navigateur et journalise les interactions sans prix, credential, URL, header ou payload provider.
|
||||||
|
|
||||||
|
La même release étend `ksp-app-wallet-desk` de façon volontairement minimale. Le refresh de balance lance `getBalance` et le refresh SOL/USD générique, puis calcule côté Rust une moyenne arithmétique uniquement à partir des observations réussies du refresh courant et l’équivalent USD exact du solde, sans `f64`/`f32`. Le prix reste auxiliaire : une indisponibilité Off-chain n’invalide jamais une balance Solana réussie et produit simplement `N.A.`. Le Wallet ne connaît aucun provider concret, n’expose aucune commande Market Price dédiée au frontend et n’ajoute aucune valorisation persistée ou logique de consensus.
|
||||||
|
|
||||||
|
Le hardening final ajoute une garde IPC hostile avant parsing pour les sélections de plus de 64 providers, un smoke live de composition `SOL Prices composite -> Config -> MarketPriceService -> refresh_all`, les canaris de frontières et les builds Tauri Linux. `pre.009-fix.001` corrige en outre la politique de version desktop : la version KSP/bundle est Cargo-owned, `tauri.conf.json` omet désormais `version` et les `package.json` privés ne sont plus forcés à suivre chaque bump workspace. Le gate opérateur passe audits Rust/Markdown, `cargo check`, Clippy, tests ciblés, `cargo test --workspace`, smoke live, puis builds `.deb`/`.rpm`/`.AppImage` pour SOL Prices, Wallet et Config Desk.
|
||||||
|
|
||||||
|
La clôture documentaire réconcilie README/USAGE, architectures, plan et validation, puis prépare `prompts/018-V0_2_13_START_PROMPT.md`. Ce prompt ouvre `0.2.13 — Interface foundation` exclusivement depuis le tag stable `v0.2.12`. Une reconnaissance préalable de l’archive historique `khadhroony-bot3` fournit des idées de contrats wire et d’extension, mais `0.2.13-pre.001` doit les réauditer contre l’architecture KSP actuelle avant toute implémentation : `ksp-interface-lib` porte les données/wires publics, `ksp-program-api` reste réservé au comportement extensible de `0.2.14`, et les implémentations officielles `ksp-program-lib` arrivent plus tard.
|
||||||
|
|
||||||
|
## 0.2.11 — Off-chain price transport — 2026-08-26
|
||||||
|
|
||||||
|
`0.2.11` crée `ksp-offchain-transport-lib` avec une première surface volontairement bornée à **SOL/USD** et huit adapters REST V1 : CoinGecko, CoinMarketCap, CoinPaprika, Kraken, Coinbase Exchange, Jupiter Price V3, Birdeye et DexScreener. Les adapters utilisent `reqwest` directement, sans SDK provider, avec origines HTTPS possédées par la crate, DTOs wire privés et prix normalisés par `MarketPriceDecimal` sans passage canonique par `f64`. `MarketPriceObservation` conserve paire, sémantique, provenance et timestamps réellement disponibles afin de ne pas confondre prix agrégé, dernier trade d'exchange, heuristique Solana, spot provider et prix DEX pair. DexScreener exige une paire Solana explicite et ne réalise aucune discovery ou sélection automatique de pool.
|
||||||
|
|
||||||
|
La crate possède désormais le registry provider-neutral, les descriptors/capabilities, les limites et quotas informatifs, les états `MarketPriceProviderAvailability`, le cooldown et le service générique `MarketPriceService`. Les consumers disposent de `registry`, `refresh`, `refresh_many` et `refresh_all`; le batch V1 reste séquentiel et déterministe, ne dort jamais pour attendre un rate limit et n'introduit ni fallback, ni consensus, ni moyenne KSP. Les erreurs provider sont normalisées en états génériques et les credentials/endpoints/raw bodies restent absents des projections et diagnostics publics.
|
||||||
|
|
||||||
|
Config ajoute `std.offchain_transport` V1, son schema, les profils `public_keyless` et `all_free`, ainsi que `ResolvedOffchainTransportConfig` qui construit le service sans dépendance inverse. Les credentials CoinGecko/CoinMarketCap/Jupiter/Birdeye restent sous `KSP_SECRET_*`; la paire DexScreener utilise une provenance `KSP_PUBLIC_*`. Les deux applications desktop existantes embarquent les nouvelles resources Config sans encore consommer fonctionnellement le prix. Les canaris de sécurité verrouillent notamment l'absence d'URL/rate-limit provider configurable au-dessus de Config, de lecture directe d'environnement dans Off-chain Transport et de fuite de clés par `Debug`.
|
||||||
|
|
||||||
|
Le gate technique final a passé les audits Rust/Markdown, `cargo check`, Clippy, les tests ciblés Config/Off-chain, le workspace complet et les graphes Cargo. Le premier smoke live keyless a isolé une erreur de query CoinMarketCap (`ids=5426` au lieu de `id=5426`); `pre.011-fix.001` la corrige pour les modes Keyless et Basic, puis le re-smoke passe **7/7** providers keyless. Le smoke keyed Birdeye/CoinGecko Demo/CoinMarketCap Basic/Jupiter Free reste explicitement `SKIP opérateur`. `prompts/017-V0_2_12_START_PROMPT.md` ouvre ensuite `0.2.12 — SOL Prices Desk + intégration prix Wallet Desk` uniquement depuis le tag stable `v0.2.11`, avec audit/sizing desktop strict avant toute implémentation UI lourde.
|
||||||
|
|
||||||
|
## 0.2.10 — OrbitFlare Yellowstone gRPC — 2026-08-25
|
||||||
|
|
||||||
|
`0.2.10` ajoute OrbitFlare comme provider Yellowstone gRPC sur le moteur N1 et le standard N2 acquis dans `0.2.9`, sans modifier le moteur gRPC ni créer de façade provider inutile. L'audit et le smoke live ont établi le contrat réel du service Solana Free Devnet : endpoint `http://devnet.rpc.orbitflare.com:10000`, authentification par metadata gRPC secrète `x-token` portant la License Key OrbitFlare, et émission du Ping Yellowstone standard. Config Transport V3 représente ce provider avec `provider = orbitflare`, `cluster = devnet`, `protocol = solana_yellowstone` et `secret_metadata`, tandis que `.env.example` inventorie `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` sans valeur réelle.
|
||||||
|
|
||||||
|
La première tentative live sans metadata a volontairement échoué avec `Unauthenticated`, ce qui a conduit à `pre.002-fix.001` après audit des sources OrbitFlare actuelles. Le smoke corrigé reçoit la License Key uniquement par stdin opérateur, ouvre `Subscribe`, observe un Slot non nul et un `SubscribeUpdate::Ping`, puis ferme la session proprement. Le même smoke est repassé au gate technique final `pre.003`. La clôture conserve donc N1/N2 inchangés, n'ajoute ni heartbeat OrbitFlare, ni SDK provider, ni Config V4, et confirme que la réponse automatique au Ping standard existante suffit.
|
||||||
|
|
||||||
|
Le gate final conserve l'ensemble du workspace vert : audits Rust/Markdown, `cargo check`, Clippy, tests Config et Transport, `cargo test --workspace`, canaris de dépendances et graphes Cargo. Helius LaserStream gRPC, audité comme largement wire-compatible Yellowstone mais non raisonnablement live-testable sans abonnement payant, est reporté dans les TODO provider sans numéro de release. La séquence active passe donc à `0.2.11 — off-chain price transport`; `prompts/016-V0_2_11_START_PROMPT.md` l'ouvre exclusivement depuis le tag stable `v0.2.10` avec audit actuel des sources de prix, de leurs sémantiques et de leur testabilité avant toute implémentation lourde.
|
||||||
|
|
||||||
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.9 — Yellowstone gRPC standard/provider-neutral — 2026-08-24
|
||||||
|
|
||||||
|
`0.2.9` ajoute à `ksp-onchain-transport-lib` un moteur Yellowstone gRPC partagé construit directement sur Tonic/Protobuf, avec `yellowstone-grpc-proto` comme contrat wire upstream et sans `yellowstone-grpc-client` dans le runtime KSP. La surface Solana standard reste provider-neutral et couvre les sept unary retenues (`SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid`, `GetVersion`), `Subscribe` bidi, les familles accounts/slots/transactions/transaction status/blocks/block meta/entry, les neuf variantes d'update standard retenues, les filtres/common fields actuels, le backpressure, les limites de messages, le half-close/shutdown, ainsi qu'un reconnect/replay prudent depuis le dernier slot observé. Aucune garantie exactly-once, lossless ou d'ordre global gapless n'est introduite ; gaps et duplicates restent observables. `SubscribeDeshred` reste explicitement hors du standard KSP de cette release.
|
||||||
|
|
||||||
|
Config passe à `std.transport` V3, backward-readable avec V1 HTTP et V2 HTTP+WebSocket, et sépare les axes `provider` / `protocol` ainsi que `metadata` / `secret_metadata` sans créer de dépendance Transport -> Config/env. PublicNode constitue la première intégration provider : profils Yellowstone Mainnet et Testnet sur `https://solana-yellowstone-grpc.publicnode.com:443` et `https://solana-testnet-yellowstone-grpc.publicnode.com:443`, avec authentification `x-token` résolue par Config depuis des variables `KSP_SECRET_*`. Le smoke Transport pur reçoit les credentials uniquement par stdin opérateur, n'en journalise aucun et valide en live `Subscribe -> Slot` sur les deux réseaux ; le même personal token a été utilisé avec succès pour Mainnet et Testnet, tout en conservant deux entrées Config distinctes par flexibilité opérationnelle.
|
||||||
|
|
||||||
|
La clôture conserve les surfaces antérieures HTTP (**52 current typed + 14 historiques**), WebSocket Solana standard (**9 familles / 18 opérations**) et Helius LaserStream WebSocket. Le gate final comprend **383/383** tests unit Transport, **49/49** public API, **43/43** release completeness, **4/4** doctests, `cargo test --workspace`, les canaris de dépendances, le smoke PublicNode live **2/2 PASS** et l'inspection des graphes Cargo (`yellowstone-grpc-proto 12.6.0`, `tonic` / `tonic-prost 0.14.6`, `prost` / `prost-types 0.14.4`). `prompts/015-V0_2_10_START_PROMPT.md` ouvre ensuite `0.2.10 — OrbitFlare Yellowstone gRPC` uniquement depuis le tag stable `v0.2.9`, avec audit provider actuel, threat model et sizing en `pre.001` avant toute extension provider lourde.
|
||||||
|
|
||||||
|
## 0.2.8 — Helius LaserStream WebSocket — 2026-08-23
|
||||||
|
|
||||||
|
`0.2.8` étend `ksp-onchain-transport-lib` avec une façade `HeliusLaserStreamWsSession` dédiée qui réutilise le même `WsSession` physique/actor que le WebSocket Solana standard, sans second client, socket, registry ou scheduler. La surface stable Helius réutilise les sept familles standard actuellement retenues (`account`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates`) et ajoute l’extension typée `transactionSubscribe` / `transactionUnsubscribe`; `block` et `vote` restent absents de la façade Helius et `slotsUpdates` conserve son statut unstable. Le heartbeat provider est possédé par l’actor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`.
|
||||||
|
|
||||||
|
La release ajoute le mapping Config V2 `helius_laserstream`, les profils Helius mainnet/devnet et le secret `KSP_SECRET_HELIUS_API_KEY` avec provenance/redaction segmentaire, sans dépendance inverse Transport -> Config ni lecture directe de l’environnement par Transport. Les canaris couvrent erreurs RPC provider, payload oversized, mismatch de notification, reconnect/remap/unsubscribe races, backpressure isolé et diagnostics sans payload brut. La compliance finale conserve simultanément **52 méthodes HTTP courantes + 14 historiques**, **9 familles / 18 opérations WebSocket Solana standard**, et la surface Helius `7 standard + transaction`. Le smoke Helius live cross-crates est volontairement reporté vers une future surface d’intégration/orchestration afin de préserver l’ownership Config du secret. Les graphes Cargo finaux n’introduisent aucun SDK Helius/gRPC ni nouvelle duplication bloquante. `prompts/014-V0_2_9_START_PROMPT.md` ouvre ensuite `0.2.9 — Yellowstone gRPC standard/provider-neutral` uniquement depuis le tag stable `v0.2.8`, avec audit service/proto/crates/licences/MSRV/features et sizing strict en `pre.001` avant toute implémentation lourde.
|
||||||
|
|
||||||
|
## 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 l’inventaire 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 l’audit 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 l’ouverture mémoire/fichier, la persistence no-clobber, la signature Solana OWNER sans getter secret, l’administration 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.
|
||||||
|
|||||||
37
Cargo.toml
37
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 95
|
# version: 331
|
||||||
|
|
||||||
[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-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.0"
|
version = "0.3.1"
|
||||||
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,37 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
argon2 = { version = "^0.5", default-features = false }
|
||||||
|
base64 = { version = "^0.23" }
|
||||||
|
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||||
|
chrono = { version = "^0.4", default-features = false }
|
||||||
|
directories = { version = "^6.0" }
|
||||||
|
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||||
fs2 = { version = "^0.4" }
|
fs2 = { version = "^0.4" }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
futures-util = { version = "^0.3", default-features = false }
|
||||||
|
getrandom = { version = "^0.4", default-features = false }
|
||||||
|
http = { version = "^1.5", default-features = false }
|
||||||
|
jsonschema = { version = "^0.51", default-features = false }
|
||||||
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
jsonschema = { version = "^0.49", 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-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
|
||||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time"] }
|
|
||||||
chrono = { version = "^0.4", default-features = false, features = ["std", "now"] }
|
|
||||||
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" }
|
||||||
|
tempfile = { version = "^3.27" }
|
||||||
|
tracing = { version = "^0.1", default-features = false }
|
||||||
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
|
tokio = { version = "^1.53", default-features = false }
|
||||||
|
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||||
|
tonic = { version = "^0.14", default-features = false }
|
||||||
|
tonic-prost = { version = "^0.14", default-features = false }
|
||||||
ts-rs = { version = "^12.0" }
|
ts-rs = { version = "^12.0" }
|
||||||
|
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
|
||||||
|
zeroize = { version = "^1.9" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
missing_docs = "warn"
|
missing_docs = "warn"
|
||||||
|
|||||||
101
ROADMAP.md
101
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 24 -->
|
<!-- version: 94 -->
|
||||||
|
|
||||||
# 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,35 @@ 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 l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||||
- [ ] `0.2.6` — 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.
|
- [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur l’actor WebSocket partagé, sept familles standard réutilisées (`account/logs/program/root/signature/slot/slotsUpdates`) + `transactionSubscribe`/`transactionUnsubscribe`, `block/vote` absents, heartbeat Ping 60 s Helius-only, Config V2/secrets redacted, lifecycle adversarial, compliance HTTP 52+14 / Standard WS 18/18 et graphes Cargo finaux validés ; prompt `0.2.9` prêt.
|
||||||
- [ ] `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.
|
- [X] `0.2.9` — Yellowstone gRPC standard/provider-neutral stable : moteur Tonic/Protobuf KSP partagé, sept unary standard retenues, `Subscribe` bidi et neuf variantes d’update, lifecycle/backpressure/reconnect/replay bornés sans promesse lossless, Config Transport V3 backward V1/V2 avec provider/protocol séparés, profils PublicNode Mainnet/Testnet authentifiés par `x-token`, smoke live `Subscribe -> Slot` 2/2 PASS et graphes Cargo finaux inspectés ; `SubscribeDeshred` reste hors scope.
|
||||||
- [ ] `0.2.10` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
- [X] `0.2.10` — OrbitFlare Yellowstone gRPC stable : profil Config V3 Devnet, License Key injectée comme metadata secrète `x-token`, smoke live `Subscribe -> Slot + Ping` validé deux fois, sans modification du moteur N1/N2 ni heartbeat provider.
|
||||||
|
- [X] `0.2.11` — Off-chain price transport stable : `ksp-offchain-transport-lib` expose SOL/USD via huit adapters REST `reqwest` sans SDK provider, décimal exact, sémantiques/provenance explicites, registry/availability/rate limits et refresh single/many/all génériques ; Config `std.offchain_transport` construit le service sans dépendance inverse, DexScreener reste lié à une paire explicite sans discovery, aucun consensus/fallback automatique n’est introduit, et le smoke live keyless final passe 7/7 après correction CoinMarketCap V2.
|
||||||
|
- [X] `0.2.12` — SOL Prices Desk + projection prix Wallet stables : HID provider-neutral avec refresh row/selected/all et batch `1..=64`, observations/timestamps exacts sans polling/consensus, puis refresh balance Wallet enrichi d’une moyenne SOL/USD consumer-owned et d’un équivalent USD exact best-effort ; smoke live de composition, workspace complet et bundles Tauri Linux validés.
|
||||||
|
- [X] `0.2.13` — Interface / wire foundation stable : `ksp-interface-lib` expose `Pubkey`, `ProgramAccountMeta` et `ProgramInstruction` passifs, admission bornée à 255 account metas / 10 240 bytes, erreurs et `Debug` sans payload hostile, façade crate-root et consumer externe canaris ; graphe strict `Interface -> Core`, sans serde/codec générique, `solana-instruction`, réseau ni logging runtime.
|
||||||
|
- [X] `0.2.14` — Program API foundation stable : `ksp-program-api` expose une surface instruction-only ouverte avec `ProgramInstructionRecognition`, `ProgramInstructionDecodeOutcome<Decoded>` et `ProgramInstructionDecoder`; implémentation externe avec Program Pubkey opaque validée, 10 exports crate-root / 3 modules de production verrouillés, graphe strict Core + Interface, sans registry/runtime/serde/codec/Store/Materializer/Execution. Le prompt `0.3.1` prépare Store RAW-only.
|
||||||
|
|
||||||
|
### TODO/IDEAS — providers Yellowstone non planifiés
|
||||||
|
|
||||||
|
- [ ] **TODO** — Helius LaserStream gRPC : réauditer lorsque l'accès live gRPC est raisonnablement disponible ; conserver N1/N2 Yellowstone inchangés, vérifier auth/endpoints/Subscribe/Ping/replay/from_slot/erreurs provider et traiter les preprocessed transactions comme extension Helius séparée.
|
||||||
|
- [ ] **TODO** — eRPC : réauditer accès, auth/IP policy, capabilities et produits complémentaires avant toute décision d’implémentation.
|
||||||
|
- [ ] **TODO** — Triton : réauditer la frontière Yellowstone upstream / extensions Triton, notamment Deshred et futures extensions.
|
||||||
|
- [ ] **TODO** — Alchemy : réauditer auth, replay, limites et capabilities Yellowstone avant toute intégration.
|
||||||
|
- [ ] **TODO** — QuickNode : réauditer auth, compression, `from_slot`, filtres et limites de plan.
|
||||||
|
- [ ] **TODO** — Chainstack : réauditer networks, auth, add-on et capabilities Yellowstone.
|
||||||
|
- [ ] **IDEAS** — Tatum, Shyft, Solinfra, NodeFlare et autres providers : conserver comme candidats exploratoires sans numéro de release ni engagement d’implémentation.
|
||||||
|
|
||||||
### Règles Transport pour toute la série
|
### Règles Transport pour toute la série
|
||||||
|
|
||||||
@@ -68,33 +80,52 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
|
|
||||||
## Architecture de données — progression canonique
|
## Architecture de données — progression canonique
|
||||||
|
|
||||||
La chaîne durable cible est :
|
La progression durable cible est :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
- **RAW** et **CORE** ne nécessitent aucun décodage Program.
|
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
||||||
- À la fin de chaque couche horizontale RAW/CORE, ajouter les jobs/workers/apps nécessaires pour la rendre réellement exploitable avant d'ouvrir la couche suivante.
|
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en N1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
||||||
- À partir de **DECODE**, avancer verticalement groupe par groupe : wire -> decode -> matérialisation -> projection spécialisée si utile -> préparation d'exécution -> policy -> execution -> scénarios Devnet.
|
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
||||||
|
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
||||||
|
|
||||||
## 0.3.x — RAW / acquisition persistée
|
## 0.3.x — RAW / acquisition persistée
|
||||||
|
|
||||||
- [ ] `0.3.1` — Introduire `ksp-store-api` + `ksp-store-lib` avec PostgreSQL de référence et **modèles/persistence RAW uniquement**.
|
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||||
- [ ] `0.3.2` — Étendre `ksp-interface-lib` avec les wires génériques nécessaires aux acquisitions et à la future normalisation CORE.
|
- [ ] `0.3.2` — Introduire ensemble `ksp-store-lib` et `ksp-store-postgres-lib` pour la **fondation runtime/backend PostgreSQL uniquement** : façade Store, feature `postgres` par défaut, dispatch des backends compilés, Config `std.store`/secrets, connexion/pool/TLS à réauditer, bootstrap/migrations privés et health/readiness seulement si un contrat portable est réellement justifié. Aucun schéma `RawTransaction`/`RawAccountState` n’est ajouté dans cette slice.
|
||||||
- [ ] `0.3.3` — Introduire `ksp-job-api` et un job de backfill historique concret.
|
- [ ] `0.3.3` — Étendre le même couple `ksp-store-lib` + `ksp-store-postgres-lib` avec la vertical slice PostgreSQL `RawTransaction` complète : persistence/observation atomiques, get/list cursorisé, idempotence/conflit, rétention/tombstone/force-rehydrate, concurrence et rollback validés sur PostgreSQL réel.
|
||||||
- [ ] `0.3.4` — Introduire une application spécialisée de backfill/inspection RAW.
|
- [ ] `0.3.4` — Étendre le même couple avec `RawAccountState` + observation, puis fermer la complétude/conformance RAW cross-family, les indexes/migrations physiques nécessaires et le hardening PostgreSQL final.
|
||||||
- [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils d'exploitation réellement nécessaires avant de passer à CORE.
|
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/events réellement partagés par les premiers consumers d’acquisition, sans dupliquer les modèles persistants de `ksp-store-api`.
|
||||||
|
- [ ] `0.3.6` — Introduire `ksp-job-api` et un premier job de backfill historique concret consommant `ksp-store-lib`, avec policy/batch-size/progression possédés par le job et non par Store.
|
||||||
|
- [ ] `0.3.7` — Introduire une application spécialisée de backfill/inspection RAW.
|
||||||
|
- [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils d’exploitation réellement nécessaires avant de passer à la couche de normalisation générique suivante.
|
||||||
|
|
||||||
## Série CORE suivante
|
### TODO/IDEAS — taxonomie N1, processing et rétention
|
||||||
|
|
||||||
- [ ] Définir la persistence CORE canonique Solana générique.
|
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||||
- [ ] Implémenter `RAW -> CORE` sans decoder Program : blocs, slots, signatures, transactions/messages, comptes, instructions/CPI brutes, logs/meta et relations structurelles.
|
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique reste réservée à `0.3.4`.
|
||||||
- [ ] Ajouter replay/backfill RAW -> CORE.
|
- [ ] **TODO** — `TransactionStatusObservation` : réauditer `signatureSubscribe`, `getSignatureStatuses`, Yellowstone TransactionStatus et extensions provider lorsqu’un consumer réel apparaît ; ne pas fusionner snapshot, transition et update dans un modèle Option-soup.
|
||||||
- [ ] Ajouter worker/service CORE.
|
- [ ] **TODO** — logs realtime : conserver `logMessages` dans `RawTransaction` jusqu’à la décomposition STRUCTURAL ; traiter `logsSubscribe` comme event-only candidat et décider son contrat passif dans `ksp-interface-lib`, sans table Store par défaut. Le format canonique d’un wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé qu’avec un publisher/consumer réel.
|
||||||
- [ ] Ajouter l'application de contrôle/inspection CORE utile.
|
- [ ] **TODO** — slot/root/slotsUpdates et vote : ne créer un modèle passif commun que si un consumer realtime réel et une sémantique cross-ledger/provider justifient le contrat ; aucune persistence Store par défaut.
|
||||||
|
- [ ] **IDEA** — `RawBlock` : ne rouvrir que si une information block-level non reconstructible devient nécessaire ; `getBlock` doit d’abord être traité comme source de `RawTransaction`, pas comme invitation à recopier le ledger en blocs.
|
||||||
|
- [ ] **REJET ACTUEL** — Yellowstone `Entry` : trop bas niveau et aucune destination replay/decomposition/event métier justifiant un modèle KSP n’est identifiée.
|
||||||
|
- [ ] **TODO** — processing ledger : reprendre l’idée kbot2/kbot3 `stage + processor identity/version + input identity/hash + terminal status`, sans faire d’un `processed: bool` la preuve durable unique ; prévoir force replay/version upgrades lorsque les processors seront ouverts.
|
||||||
|
- [X] lifecycle RAW logique — `RawRetentionState`, tombstone minimal, normal-skip et force-rehydrate sont stabilisés en `0.3.1` pour `RawTransaction`.
|
||||||
|
- [ ] **TODO** — rétention physique : définir plus tard compression/archive backend, critères d’éligibilité fondés sur les preuves de processing et maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être purgé.
|
||||||
|
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
||||||
|
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de N2/N3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||||
|
|
||||||
## Séries DECODE/SPECIALIZED/EXECUTION — progression verticale
|
## Série STRUCTURAL suivante
|
||||||
|
|
||||||
|
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
||||||
|
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
||||||
|
- [ ] Vérifier avant extension si d'autres familles N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
||||||
|
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
||||||
|
- [ ] Ajouter worker/service STRUCTURAL et l'application de contrôle/inspection utile.
|
||||||
|
|
||||||
|
## Séries DECODED/DOMAIN/EXECUTION — progression verticale
|
||||||
|
|
||||||
### Priorité 1 — Solana Core Programs
|
### Priorité 1 — Solana Core Programs
|
||||||
|
|
||||||
@@ -107,7 +138,7 @@ RAW -> CORE -> DECODE -> SPECIALIZED
|
|||||||
- [ ] SPL Token.
|
- [ ] SPL Token.
|
||||||
- [ ] Associated Token Account.
|
- [ ] Associated Token Account.
|
||||||
- [ ] Token-2022 et extensions pertinentes.
|
- [ ] Token-2022 et extensions pertinentes.
|
||||||
- [ ] Pour chaque famille : decode -> materialize -> specialized -> prepare -> policy -> execute -> scenarios.
|
- [ ] Pour chaque famille : decode -> materialize -> domain projection -> prepare -> policy -> execute -> scenarios.
|
||||||
|
|
||||||
### Priorité 3 — Metadata token
|
### Priorité 3 — Metadata token
|
||||||
|
|
||||||
@@ -131,7 +162,7 @@ RAW -> CORE -> DECODE -> SPECIALIZED
|
|||||||
|
|
||||||
- [ ] Après les premiers groupes Meteora/Raydium/Pump/Orca, introduire une petite `ksp-app-market-desk` spécialisée.
|
- [ ] Après les premiers groupes Meteora/Raydium/Pump/Orca, introduire une petite `ksp-app-market-desk` spécialisée.
|
||||||
- [ ] Visualiser tokens, pools/markets, liquidité, swaps/trades, prix, volumes, OHLC/candles et activité live/récente lorsque disponible.
|
- [ ] Visualiser tokens, pools/markets, liquidité, swaps/trades, prix, volumes, OHLC/candles et activité live/récente lorsque disponible.
|
||||||
- [ ] Lire les projections SPECIALIZED KSP ; ne pas reconstruire la logique protocolaire dans l'UI.
|
- [ ] Lire les projections DOMAIN KSP ; ne pas reconstruire la logique protocolaire dans l'UI.
|
||||||
|
|
||||||
### Routing
|
### Routing
|
||||||
|
|
||||||
|
|||||||
51
config/composite.ksp-app-solprices-desk.json
Normal file
51
config/composite.ksp-app-solprices-desk.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "public_keyless",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "public_keyless",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "console_info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "all_free",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "console_info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "all_free"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "tests",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "console_info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
config/composite.ksp-app-wallet-desk.json
Normal file
81
config/composite.ksp-app-wallet-desk.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "transport",
|
||||||
|
"file_id": "cfg.std.transport",
|
||||||
|
"profile_id": "devnet_public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "wallet",
|
||||||
|
"file_id": "cfg.std.wallet",
|
||||||
|
"profile_id": "tests"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
config/examples/composite.ksp-app-wallet-desk.example.json
Normal file
81
config/examples/composite.ksp-app-wallet-desk.example.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "offchain_transport",
|
||||||
|
"file_id": "cfg.std.offchain_transport",
|
||||||
|
"profile_id": "public_keyless"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "transport",
|
||||||
|
"file_id": "cfg.std.transport",
|
||||||
|
"profile_id": "devnet_public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "wallet",
|
||||||
|
"file_id": "cfg.std.wallet",
|
||||||
|
"profile_id": "tests"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -61,4 +61,4 @@
|
|||||||
"target_filters": []
|
"target_filters": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
43
config/examples/std.offchain_transport.example.json
Normal file
43
config/examples/std.offchain_transport.example.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "custom_free",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "custom_free",
|
||||||
|
"market_price": {
|
||||||
|
"birdeye": {
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": "${KSP_SECRET_BIRDEYE_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "demo",
|
||||||
|
"api_key": "${KSP_SECRET_COINGECKO_DEMO_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "basic",
|
||||||
|
"api_key": "${KSP_SECRET_COINMARKETCAP_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"enabled": true,
|
||||||
|
"sol_usd_pair_address": "${KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS}"
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "free",
|
||||||
|
"api_key": "${KSP_SECRET_JUPITER_API_KEY}"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
148
config/examples/std.transport.example.json
Normal file
148
config/examples/std.transport.example.json
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"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_helius_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "helius_laserstream",
|
||||||
|
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
||||||
|
"session": {
|
||||||
|
"notification_queue_capacity": 512,
|
||||||
|
"max_active_subscriptions": 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "devnet_helius",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "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": "devnet_helius_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"kind": "helius_laserstream",
|
||||||
|
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
config/examples/std.wallet.example.json
Normal file
18
config/examples/std.wallet.example.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -263,4 +263,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
366
config/schemas/std.offchain_transport.schema.json
Normal file
366
config/schemas/std.offchain_transport.schema.json
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "urn:ksp:schema:std.offchain_transport:v1",
|
||||||
|
"title": "KSP standard Off-chain Transport configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"format_version",
|
||||||
|
"default_profile",
|
||||||
|
"profiles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"const": 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._-]*$"
|
||||||
|
},
|
||||||
|
"enabledOnly": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"enum": [
|
||||||
|
"demo",
|
||||||
|
"keyless"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"const": "keyless"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"not": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"const": true
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"const": "demo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"enum": [
|
||||||
|
"basic",
|
||||||
|
"keyless"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"const": "keyless"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"not": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"const": true
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"const": "basic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"enum": [
|
||||||
|
"free",
|
||||||
|
"keyless"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"const": "keyless"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"not": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"const": true
|
||||||
|
},
|
||||||
|
"access_mode": {
|
||||||
|
"const": "free"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"access_mode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"birdeye": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"const": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"api_key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sol_usd_pair_address": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"const": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"sol_usd_pair_address"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marketPrice": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"birdeye",
|
||||||
|
"coinbase_exchange",
|
||||||
|
"coingecko",
|
||||||
|
"coinmarketcap",
|
||||||
|
"coinpaprika",
|
||||||
|
"dexscreener",
|
||||||
|
"jupiter",
|
||||||
|
"kraken"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"birdeye": {
|
||||||
|
"$ref": "#/$defs/birdeye"
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"$ref": "#/$defs/enabledOnly"
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"$ref": "#/$defs/coingecko"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"$ref": "#/$defs/coinmarketcap"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"$ref": "#/$defs/enabledOnly"
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"$ref": "#/$defs/dexscreener"
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"$ref": "#/$defs/jupiter"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"$ref": "#/$defs/enabledOnly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"profile_id",
|
||||||
|
"market_price"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"profile_id": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"market_price": {
|
||||||
|
"$ref": "#/$defs/marketPrice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
732
config/schemas/std.transport.schema.json
Normal file
732
config/schemas/std.transport.schema.json
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "urn:ksp:schema:std.transport:v3",
|
||||||
|
"title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/documentV1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/documentV2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/documentV3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$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",
|
||||||
|
"helius_laserstream"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcPositiveMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 300000
|
||||||
|
},
|
||||||
|
"grpcCapacity": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65536
|
||||||
|
},
|
||||||
|
"grpcMessageSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 536870912
|
||||||
|
},
|
||||||
|
"grpcReconnect": {
|
||||||
|
"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/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"max_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcReconnectOverride": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"properties": {
|
||||||
|
"max_retries": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"initial_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"max_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcSession": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"connect_timeout_ms",
|
||||||
|
"unary_timeout_ms",
|
||||||
|
"close_timeout_ms",
|
||||||
|
"reconnect",
|
||||||
|
"request_channel_capacity",
|
||||||
|
"update_channel_capacity",
|
||||||
|
"max_inbound_message_size_bytes",
|
||||||
|
"max_outbound_message_size_bytes"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"connect_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"unary_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"close_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"reconnect": {
|
||||||
|
"$ref": "#/$defs/grpcReconnect"
|
||||||
|
},
|
||||||
|
"request_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"update_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"max_inbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
},
|
||||||
|
"max_outbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcSessionOverride": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"properties": {
|
||||||
|
"connect_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"unary_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"close_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"reconnect": {
|
||||||
|
"$ref": "#/$defs/grpcReconnectOverride"
|
||||||
|
},
|
||||||
|
"request_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"update_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"max_inbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
},
|
||||||
|
"max_outbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"key",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128,
|
||||||
|
"pattern": "^[0-9a-z_.-]+$"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 8192
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcEndpoint": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"enabled",
|
||||||
|
"provider",
|
||||||
|
"cluster",
|
||||||
|
"protocol",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"const": "solana_yellowstone"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 8192
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 64,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcMetadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"secret_metadata": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 64,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcMetadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"$ref": "#/$defs/grpcSessionOverride"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profileV3": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpc_endpoints": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 128,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcEndpoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentV3": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"format_version",
|
||||||
|
"retry",
|
||||||
|
"ws_defaults",
|
||||||
|
"grpc_defaults",
|
||||||
|
"default_profile",
|
||||||
|
"profiles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"const": 3
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"$ref": "#/$defs/retry"
|
||||||
|
},
|
||||||
|
"ws_defaults": {
|
||||||
|
"$ref": "#/$defs/wsSession"
|
||||||
|
},
|
||||||
|
"grpc_defaults": {
|
||||||
|
"$ref": "#/$defs/grpcSession"
|
||||||
|
},
|
||||||
|
"default_profile": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/profileV3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
config/schemas/std.wallet.schema.json
Normal file
54
config/schemas/std.wallet.schema.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
75
config/std.offchain_transport.json
Normal file
75
config/std.offchain_transport.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "public_keyless",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "public_keyless",
|
||||||
|
"market_price": {
|
||||||
|
"birdeye": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "all_free",
|
||||||
|
"market_price": {
|
||||||
|
"birdeye": {
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": "${KSP_SECRET_BIRDEYE_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "demo",
|
||||||
|
"api_key": "${KSP_SECRET_COINGECKO_DEMO_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "basic",
|
||||||
|
"api_key": "${KSP_SECRET_COINMARKETCAP_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"enabled": true,
|
||||||
|
"sol_usd_pair_address": "${KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS}"
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "free",
|
||||||
|
"api_key": "${KSP_SECRET_JUPITER_API_KEY}"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
296
config/std.transport.json
Normal file
296
config/std.transport.json
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
{
|
||||||
|
"format_version": 3,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"grpc_defaults": {
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"unary_timeout_ms": 10000,
|
||||||
|
"close_timeout_ms": 5000,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 5,
|
||||||
|
"initial_backoff_ms": 250,
|
||||||
|
"max_backoff_ms": 5000
|
||||||
|
},
|
||||||
|
"request_channel_capacity": 128,
|
||||||
|
"update_channel_capacity": 256,
|
||||||
|
"max_inbound_message_size_bytes": 67108864,
|
||||||
|
"max_outbound_message_size_bytes": 67108864
|
||||||
|
},
|
||||||
|
"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": "orbitflare_devnet",
|
||||||
|
"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}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "orbitflare_solana_devnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "orbitflare",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "http://devnet.rpc.orbitflare.com:10000",
|
||||||
|
"metadata": [],
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "publicnode_mainnet",
|
||||||
|
"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}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_mainnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "publicnode_testnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_testnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"url": "https://api.testnet.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_testnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "wss://api.testnet.solana.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_testnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
config/std.wallet.json
Normal file
18
config/std.wallet.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-app-config-desk/Cargo.toml
|
# file: crates/ksp-app-config-desk/Cargo.toml
|
||||||
# version: 7
|
# version: 9
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-app-config-desk"
|
name = "ksp-app-config-desk"
|
||||||
@@ -22,16 +22,16 @@ path = "src/main.rs"
|
|||||||
tauri-build.workspace = true
|
tauri-build.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { workspace = true, features = ["std", "now"] }
|
||||||
fs2.workspace = true
|
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
|
tokio = { workspace = true, features = ["time"] }
|
||||||
tokio.workspace = true
|
|
||||||
ts-rs.workspace = true
|
ts-rs.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -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 n’est 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 n’est lancé par l’opé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 l’application 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 l’application 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 d’un 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 d’un 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.
|
||||||
|
|||||||
@@ -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 n’est lancé directement par l’opé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é n’est 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é n’est nécessaire dans le cycle de validation.
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"tracing:default"
|
"tracing:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -157,4 +157,4 @@
|
|||||||
color: $list-group-disabled-color;
|
color: $list-group-disabled-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||||
|
|||||||
@@ -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] {
|
||||||
@@ -245,4 +245,4 @@
|
|||||||
|
|
||||||
.simplebar-hover {
|
.simplebar-hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -92,4 +92,4 @@ $list-group-border-color: transparent !default;
|
|||||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||||
$list-group-active-color: $white !default;
|
$list-group-active-color: $white !default;
|
||||||
$list-group-active-bg: $list-group-bg !default;
|
$list-group-active-bg: $list-group-bg !default;
|
||||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<!-- file: crates/ksp-app-config-desk/frontend/splash.html -->
|
<!-- file: crates/ksp-app-config-desk/frontend/splash.html -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 3 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link rel="icon" href="imgs/logo.png">
|
||||||
<link rel="stylesheet" href="sass/splash.scss">
|
<link rel="stylesheet" href="sass/splash.scss">
|
||||||
<title>KSP Config Desk — Chargement</title>
|
<title>KSP Config Desk — Chargement</title>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$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",
|
|
||||||
"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 +50,19 @@
|
|||||||
"icon": [
|
"icon": [
|
||||||
"icons/favicon.png",
|
"icons/favicon.png",
|
||||||
"icons/favicon.ico"
|
"icons/favicon.ico"
|
||||||
]
|
],
|
||||||
|
"resources": {
|
||||||
|
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||||
|
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||||
|
"../../config/std.logging.json": "config/std.logging.json",
|
||||||
|
"../../config/std.offchain_transport.json": "config/std.offchain_transport.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.offchain_transport.schema.json": "config/schemas/std.offchain_transport.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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: 10
|
||||||
|
|
||||||
//! Desktop build/shell contract audits for Config Desk.
|
//! Desktop build/shell contract audits for Config Desk.
|
||||||
|
|
||||||
@@ -31,8 +31,10 @@ fn tauri_and_frontend_build_contracts_remain_explicit() {
|
|||||||
let root = app_root();
|
let root = app_root();
|
||||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||||
assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1430"));
|
assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1430"));
|
||||||
assert_eq!(tauri.pointer("/build/beforeDevCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
|
assert_eq!(tauri.pointer("/build/beforeDevCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
|
||||||
assert_eq!(tauri.pointer("/build/beforeBuildCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
|
assert_eq!(tauri.pointer("/build/beforeDevCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
|
||||||
|
assert_eq!(tauri.pointer("/build/beforeBuildCommand/script").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
|
||||||
|
assert_eq!(tauri.pointer("/build/beforeBuildCommand/cwd").and_then(serde_json::Value::as_str), std::option::Option::Some("."));
|
||||||
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
|
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
|
||||||
assert!(windows.is_some());
|
assert!(windows.is_some());
|
||||||
let windows = match windows {
|
let windows = match windows {
|
||||||
@@ -47,11 +49,13 @@ fn tauri_and_frontend_build_contracts_remain_explicit() {
|
|||||||
if let std::option::Option::Some(main) = main {
|
if let std::option::Option::Some(main) = main {
|
||||||
assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false));
|
assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false));
|
||||||
}
|
}
|
||||||
|
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||||
|
assert!(manifest.contains("name = \"ksp-app-config-desk\""));
|
||||||
let package = read_json(root.join("package.json").as_path());
|
let package = read_json(root.join("package.json").as_path());
|
||||||
let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str);
|
let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str);
|
||||||
let check = package.pointer("/scripts/check").and_then(serde_json::Value::as_str);
|
|
||||||
assert!(build.is_some_and(|value| return value.contains("tsc") && value.contains("vite build")));
|
assert!(build.is_some_and(|value| return value.contains("tsc") && value.contains("vite build")));
|
||||||
assert_eq!(check, std::option::Option::Some("tsc --noEmit"));
|
assert!(package.pointer("/scripts/check").is_none());
|
||||||
|
assert!(package.pointer("/scripts/preview").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -70,3 +74,63 @@ 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());
|
||||||
|
assert!(tauri.pointer("/version").is_none(), "Config Desk must let Tauri derive the bundle version from Cargo");
|
||||||
|
let package = read_json(root.join("package.json").as_path());
|
||||||
|
assert_eq!(package.pointer("/private").and_then(serde_json::Value::as_bool), std::option::Option::Some(true));
|
||||||
|
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(), 11);
|
||||||
|
assert_eq!(
|
||||||
|
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
|
||||||
|
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
|
||||||
|
);
|
||||||
|
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/std.offchain_transport.json").and_then(serde_json::Value::as_str),
|
||||||
|
std::option::Option::Some("config/std.offchain_transport.json"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
|
||||||
|
std::option::Option::Some("config/schemas/std.offchain_transport.schema.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())"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
"frontend",
|
"frontend",
|
||||||
"vite.config.ts"
|
"vite.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||||
// version: 2
|
// version: 7
|
||||||
|
|
||||||
#[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;
|
||||||
|
}));
|
||||||
|
assert!(inventory.iter().any(|document| -> bool {
|
||||||
|
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_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;
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
41
crates/ksp-app-solprices-desk/Cargo.toml
Normal file
41
crates/ksp-app-solprices-desk/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# file: crates/ksp-app-solprices-desk/Cargo.toml
|
||||||
|
# version: 2
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-app-solprices-desk"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ksp_app_solprices_desk_lib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ksp-app-solprices-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-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
tauri.workspace = true
|
||||||
|
tauri-plugin-tracing.workspace = true
|
||||||
|
tokio = { workspace = true, features = ["time"] }
|
||||||
|
ts-rs.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
154
crates/ksp-app-solprices-desk/README.md
Normal file
154
crates/ksp-app-solprices-desk/README.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!-- file: crates/ksp-app-solprices-desk/README.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# `ksp-app-solprices-desk`
|
||||||
|
|
||||||
|
`ksp-app-solprices-desk` est l'application desktop spécialisée de visualisation et de refresh manuel des prix SOL/USD fournis par `ksp-offchain-transport-lib`.
|
||||||
|
|
||||||
|
L'application reste une **HID provider-neutral** : elle consomme le registry, les observations, les disponibilités et les opérations génériques du service Off-chain sans connaître les endpoints, credentials, limites ou wire formats propres aux providers.
|
||||||
|
|
||||||
|
## Package
|
||||||
|
|
||||||
|
```text
|
||||||
|
package : ksp-app-solprices-desk
|
||||||
|
lib : ksp_app_solprices_desk_lib
|
||||||
|
bin : ksp-app-solprices-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Le binaire reste un launcher mince. La bibliothèque applicative possède l'assemblage Tauri, le bootstrap Config/Logging/Off-chain, l'état de présentation en mémoire et les commands Tauri.
|
||||||
|
|
||||||
|
## Responsabilités
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib -> composite, profils, secrets et runtime packagé
|
||||||
|
ksp-offchain-transport-lib -> providers, HTTP, registry, availability, limits et refresh
|
||||||
|
ksp-logging-lib -> logging/tracing applicatif
|
||||||
|
ksp-app-solprices-desk -> orchestration Tauri + projections provider-neutral sûres
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Desk ne possède aucun client HTTP provider et ne réimplémente ni admission, ni cooldown, ni retry provider.
|
||||||
|
|
||||||
|
## Vues
|
||||||
|
|
||||||
|
### Prices
|
||||||
|
|
||||||
|
La vue `Prices` affiche l'inventaire SOL/USD dans l'ordre stable du registry et conserve chaque observation séparée.
|
||||||
|
|
||||||
|
La projection comprend notamment :
|
||||||
|
|
||||||
|
- identité/display provider et paire ;
|
||||||
|
- sémantique et mode d'authentification ;
|
||||||
|
- availability générique ;
|
||||||
|
- prix exact sous forme de `String` ;
|
||||||
|
- timestamp provider lorsqu'il existe ;
|
||||||
|
- timestamp de réception KSP ;
|
||||||
|
- `retry_at` lorsqu'il existe ;
|
||||||
|
- état `Refreshing` local à la présentation.
|
||||||
|
|
||||||
|
Aucune moyenne, aucun fallback automatique et aucun prix canonique ne sont produits par cette application.
|
||||||
|
|
||||||
|
Les actions manuelles sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Refresh
|
||||||
|
Refresh selected
|
||||||
|
Refresh all
|
||||||
|
Select all providers
|
||||||
|
Clear selection
|
||||||
|
```
|
||||||
|
|
||||||
|
Le batch applicatif est borné à `1..=64` IDs opaques avant parsing/dispatch. Les doublons, IDs inconnus et conflits `in_flight` sont rejetés avant mutation partielle.
|
||||||
|
|
||||||
|
### Diagnostics
|
||||||
|
|
||||||
|
La vue `Diagnostics` expose uniquement des informations de runtime sûres : identité/version applicative, profils résolus et compte de providers. Aucun endpoint, header, API key, payload provider ou prix n'est journalisé comme diagnostic brut.
|
||||||
|
|
||||||
|
## Config et profils
|
||||||
|
|
||||||
|
Le composite dédié est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cfg.composite.ksp-app-solprices-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Il compose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logging
|
||||||
|
Off-chain Transport
|
||||||
|
```
|
||||||
|
|
||||||
|
Profils committed :
|
||||||
|
|
||||||
|
```text
|
||||||
|
public_keyless profil par défaut
|
||||||
|
all_free modes gratuits incluant les credentials Config requis
|
||||||
|
tests profil déterministe de composition
|
||||||
|
```
|
||||||
|
|
||||||
|
Le frontend ne sélectionne ni provider concret ni secret. La résolution des credentials reste entièrement possédée par Config.
|
||||||
|
|
||||||
|
## Frontières desktop
|
||||||
|
|
||||||
|
Le frontend utilise uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
core:default
|
||||||
|
tracing:default
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne possède aucun accès direct filesystem, HTTP, shell ou persistence navigateur. Les refresh passent uniquement par les commands Rust centralisées dans `tauri.rs`.
|
||||||
|
|
||||||
|
Il n'existe aucun polling, auto-refresh ou scheduler prix dans le Desk. Les deadlines/cooldowns affichés proviennent de `ksp-offchain-transport-lib`.
|
||||||
|
|
||||||
|
## Ports et build
|
||||||
|
|
||||||
|
Les ports réservés sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Vite HTTP : 1434
|
||||||
|
Vite WS : 1435
|
||||||
|
```
|
||||||
|
|
||||||
|
Le frontend build est isolé sous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
builds/khadhroony-solana-project/ksp-app-solprices-desk/dist
|
||||||
|
```
|
||||||
|
|
||||||
|
La version du binaire et des bundles Tauri provient de la version Cargo résolue. `tauri.conf.json` ne duplique pas cette version ; `package.json` reste un manifeste frontend privé non autoritatif pour la release KSP.
|
||||||
|
|
||||||
|
## Runtime packagé
|
||||||
|
|
||||||
|
Les onze resources Config/Schemas enregistrées sont embarquées dans le bundle. Au démarrage release, `ksp-config-lib` prépare la racine KSP user-writable commune, seed les documents absents, resynchronise les schemas et ne bundle jamais `.env`.
|
||||||
|
|
||||||
|
## Développement et validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-solprices-desk && cargo tauri dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Gate Rust courant :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-app-solprices-desk
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke live de composition opt-in :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Build production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-solprices-desk && cargo tauri build)
|
||||||
|
```
|
||||||
|
|
||||||
|
Voir également [`USAGE.md`](USAGE.md), [`../ksp-offchain-transport-lib/README.md`](../ksp-offchain-transport-lib/README.md) et [`../../docs/validation/015-V0_2_12_SOL_PRICES_DESK.md`](../../docs/validation/015-V0_2_12_SOL_PRICES_DESK.md).
|
||||||
149
crates/ksp-app-solprices-desk/USAGE.md
Normal file
149
crates/ksp-app-solprices-desk/USAGE.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<!-- file: crates/ksp-app-solprices-desk/USAGE.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Utilisation de `ksp-app-solprices-desk`
|
||||||
|
|
||||||
|
## 1. Lancement de développement
|
||||||
|
|
||||||
|
Depuis la racine du workspace :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-solprices-desk && cargo tauri dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le launcher debug normalise son current working directory vers la racine du workspace avant le bootstrap Config.
|
||||||
|
|
||||||
|
Les ports réservés sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Vite HTTP : 1434
|
||||||
|
Vite WS : 1435
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Profil de prix
|
||||||
|
|
||||||
|
Le composite `cfg.composite.ksp-app-solprices-desk` utilise `public_keyless` par défaut.
|
||||||
|
|
||||||
|
Profils committed :
|
||||||
|
|
||||||
|
```text
|
||||||
|
public_keyless providers utilisables sans secret
|
||||||
|
all_free modes gratuits avec credentials Config lorsqu'ils sont requis
|
||||||
|
tests composition de test
|
||||||
|
```
|
||||||
|
|
||||||
|
La sélection d'un autre profil appartient aux mécanismes Config ; l'interface ne demande jamais directement une API key et ne choisit pas d'endpoint provider.
|
||||||
|
|
||||||
|
## 3. Vue Prices
|
||||||
|
|
||||||
|
La vue `Prices` présente une row par provider du registry Off-chain.
|
||||||
|
|
||||||
|
Les informations principales sont :
|
||||||
|
|
||||||
|
- paire SOL/USD ;
|
||||||
|
- sémantique de prix ;
|
||||||
|
- mode d'authentification ;
|
||||||
|
- availability ;
|
||||||
|
- prix exact lorsqu'une observation existe ;
|
||||||
|
- timestamp provider lorsqu'il est réellement fourni ;
|
||||||
|
- timestamp de réception KSP ;
|
||||||
|
- prochaine deadline `retry_at` lorsqu'elle existe.
|
||||||
|
|
||||||
|
`Not supplied` signifie qu'un provider n'a pas fourni de timestamp exploitable. Cette absence n'est jamais remplacée artificiellement par le timestamp KSP.
|
||||||
|
|
||||||
|
## 4. Refresh individuel
|
||||||
|
|
||||||
|
Le bouton `Refresh` d'une row déclenche uniquement le provider correspondant via `MarketPriceService::refresh`.
|
||||||
|
|
||||||
|
Pendant l'appel, la row passe temporairement à `Refreshing`. Le Desk ne garde aucun mutex de présentation pendant l'I/O réseau.
|
||||||
|
|
||||||
|
Si le service ne produit pas de nouvelle observation, la dernière observation réussie peut rester visible tandis que l'availability/retry est actualisée.
|
||||||
|
|
||||||
|
## 5. Refresh sélectionné
|
||||||
|
|
||||||
|
Cocher une ou plusieurs rows puis utiliser :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Refresh selected
|
||||||
|
```
|
||||||
|
|
||||||
|
La sélection est reconstruite dans l'ordre courant du registry. Les doublons, IDs inconnus, conflits `in_flight`, batch vide et batch supérieur à 64 IDs sont rejetés côté Rust avant dispatch partiel.
|
||||||
|
|
||||||
|
Les contrôles :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Select all providers
|
||||||
|
Clear selection
|
||||||
|
```
|
||||||
|
|
||||||
|
ne déclenchent aucun accès réseau par eux-mêmes.
|
||||||
|
|
||||||
|
## 6. Refresh global
|
||||||
|
|
||||||
|
```text
|
||||||
|
Refresh all
|
||||||
|
```
|
||||||
|
|
||||||
|
appelle la surface générique `MarketPriceService::refresh_all` et conserve l'ordre déterministe du service.
|
||||||
|
|
||||||
|
Les providers disabled, en cooldown ou temporairement indisponibles restent classifiés par Off-chain Transport. Le frontend ne contourne jamais ces règles et n'implémente aucun retry local.
|
||||||
|
|
||||||
|
## 7. Lecture des états
|
||||||
|
|
||||||
|
Les états visibles sont provider-neutral, notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ready
|
||||||
|
Cooling down
|
||||||
|
Authentication unavailable
|
||||||
|
Disabled
|
||||||
|
Misconfigured
|
||||||
|
Quota unavailable
|
||||||
|
Temporarily unavailable
|
||||||
|
Refreshing
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résumé en haut de la vue indique les nombres de providers, rows ready, observations disponibles, états nécessitant attention et refresh en cours.
|
||||||
|
|
||||||
|
Aucune moyenne ou fusion de prix n'est calculée dans SOL Prices Desk.
|
||||||
|
|
||||||
|
## 8. Diagnostics et sécurité
|
||||||
|
|
||||||
|
La vue `Diagnostics` expose uniquement les informations sûres de composition/runtime.
|
||||||
|
|
||||||
|
Le frontend :
|
||||||
|
|
||||||
|
- ne fait aucun `fetch`/XHR provider ;
|
||||||
|
- ne possède aucun endpoint ou header provider ;
|
||||||
|
- ne stocke aucun secret dans `localStorage`/`sessionStorage` ;
|
||||||
|
- n'utilise aucun scheduler ou polling prix ;
|
||||||
|
- ne journalise aucun prix ou payload provider brut.
|
||||||
|
|
||||||
|
Les actions utilisateur sont instrumentées via le bridge Logging KSP.
|
||||||
|
|
||||||
|
## 9. Runtime packagé
|
||||||
|
|
||||||
|
En build release, les documents Config et schemas enregistrés sont embarqués comme resources Tauri. `ksp-config-lib` prépare ensuite la racine KSP user-writable commune ; les Config utilisateur existantes sont conservées, les schemas sont resynchronisés et `.env` n'est jamais embarqué.
|
||||||
|
|
||||||
|
La version de bundle est dérivée de Cargo. `package.json` reste un manifeste frontend privé.
|
||||||
|
|
||||||
|
## 10. Validation de publication
|
||||||
|
|
||||||
|
Avant le build :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.12
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-app-solprices-desk
|
||||||
|
cargo test --workspace
|
||||||
|
cargo test -p ksp-app-solprices-desk --test market_price_composition_live_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Le build production est :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-solprices-desk && cargo tauri build)
|
||||||
|
```
|
||||||
12
crates/ksp-app-solprices-desk/build.rs
Normal file
12
crates/ksp-app-solprices-desk/build.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/build.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Build script for the KSP SOL prices desktop application.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
13
crates/ksp-app-solprices-desk/capabilities/default.json
Normal file
13
crates/ksp-app-solprices-desk/capabilities/default.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for KSP SOL Prices Desk",
|
||||||
|
"windows": [
|
||||||
|
"splash",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"tracing:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
crates/ksp-app-solprices-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
crates/ksp-app-solprices-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
25
crates/ksp-app-solprices-desk/frontend/fonts/README.md
Normal file
25
crates/ksp-app-solprices-desk/frontend/fonts/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!-- file: crates/ksp-app-solprices-desk/frontend/fonts/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Fonts du splash SOL Prices Desk
|
||||||
|
|
||||||
|
SOL Prices Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et Wallet Desk et conserve la police locale :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DOS_Amazigh.ttf
|
||||||
|
font-family: Dos Amazigh
|
||||||
|
```
|
||||||
|
|
||||||
|
Destination :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-app-solprices-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`.
|
||||||
BIN
crates/ksp-app-solprices-desk/frontend/imgs/logo.png
Normal file
BIN
crates/ksp-app-solprices-desk/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/ksp-app-solprices-desk/frontend/imgs/splash.png
Normal file
BIN
crates/ksp-app-solprices-desk/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
188
crates/ksp-app-solprices-desk/frontend/main.html
Normal file
188
crates/ksp-app-solprices-desk/frontend/main.html
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
|
||||||
|
<!-- version: 8 -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link rel="icon" href="imgs/logo.png">
|
||||||
|
<link rel="stylesheet" href="sass/main.scss">
|
||||||
|
<title>SOL Prices Desk — Prices</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">SOL Prices 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">Prices</span>
|
||||||
|
</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 SOL Prices Desk">
|
||||||
|
<button class="nav-link text-start active" type="button" data-view="prices">
|
||||||
|
<i class="fa-solid fa-dollar-sign me-2" aria-hidden="true"></i>Prices
|
||||||
|
</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">
|
||||||
|
<section data-view-panel="prices">
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">Prices</h1>
|
||||||
|
<p class="text-body-secondary mb-0">Inventaire provider-neutral SOL/USD. Les observations restent distinctes : aucune moyenne, aucun fallback automatique et aucun polling.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap app-market-toolbar">
|
||||||
|
<span id="selectedProviderCount" class="small text-body-secondary" aria-live="polite">0 selected</span>
|
||||||
|
<button id="clearSelectionButton" class="btn btn-sm btn-outline-secondary" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-xmark me-1" aria-hidden="true"></i>Clear selection
|
||||||
|
</button>
|
||||||
|
<button id="refreshSelectedButton" class="btn btn-sm btn-outline-primary" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-list-check me-1" aria-hidden="true"></i>Refresh selected
|
||||||
|
</button>
|
||||||
|
<button id="refreshAllButton" class="btn btn-sm btn-primary" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pricesSummary" class="row g-2 mb-3" aria-label="Résumé des providers">
|
||||||
|
<div class="col-6 col-lg">
|
||||||
|
<div class="app-market-summary-card">
|
||||||
|
<span class="app-market-summary-label">Providers</span>
|
||||||
|
<strong id="pricesSummaryProviders">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg">
|
||||||
|
<div class="app-market-summary-card">
|
||||||
|
<span class="app-market-summary-label">Ready</span>
|
||||||
|
<strong id="pricesSummaryReady">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg">
|
||||||
|
<div class="app-market-summary-card">
|
||||||
|
<span class="app-market-summary-label">Observed</span>
|
||||||
|
<strong id="pricesSummaryObserved">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg">
|
||||||
|
<div class="app-market-summary-card">
|
||||||
|
<span class="app-market-summary-label">Attention</span>
|
||||||
|
<strong id="pricesSummaryAttention">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg">
|
||||||
|
<div class="app-market-summary-card">
|
||||||
|
<span class="app-market-summary-label">Refreshing</span>
|
||||||
|
<strong id="pricesSummaryLoading">0</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pricesLoading" class="app-placeholder">
|
||||||
|
<div>
|
||||||
|
<i class="fa-solid fa-spinner fa-spin text-body-secondary mb-3" aria-hidden="true"></i>
|
||||||
|
<h2 class="h5">Chargement du registry</h2>
|
||||||
|
<p class="text-body-secondary mb-0">Projection de l'inventaire provider-neutral depuis le runtime backend.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pricesEmpty" class="app-placeholder" hidden>
|
||||||
|
<div>
|
||||||
|
<i class="fa-solid fa-table-list text-body-secondary mb-3" aria-hidden="true"></i>
|
||||||
|
<h2 class="h5">Aucun provider configuré</h2>
|
||||||
|
<p class="text-body-secondary mb-0">Le runtime Off-chain ne contient actuellement aucune ligne provider.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pricesDiagnostic" class="alert alert-warning mb-3" role="status" aria-live="polite" hidden></div>
|
||||||
|
<div id="pricesTableContainer" class="table-responsive app-market-table-container" hidden>
|
||||||
|
<table id="marketPriceTable" class="table table-hover align-middle mb-0 app-market-table">
|
||||||
|
<caption class="visually-hidden">Providers SOL/USD, disponibilité, prix exacts, fraîcheur et actions de refresh.</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="app-market-selection-column">
|
||||||
|
<input id="selectAllProviders" class="form-check-input" type="checkbox" aria-label="Select all providers" disabled>
|
||||||
|
</th>
|
||||||
|
<th scope="col">Provider</th>
|
||||||
|
<th scope="col">Pair</th>
|
||||||
|
<th scope="col" class="d-none d-xl-table-cell">Semantics</th>
|
||||||
|
<th scope="col" class="d-none d-xl-table-cell">Auth</th>
|
||||||
|
<th scope="col">Availability</th>
|
||||||
|
<th scope="col">Exact price</th>
|
||||||
|
<th scope="col" class="app-market-time-column">Observation times</th>
|
||||||
|
<th scope="col" class="app-market-time-column">Retry</th>
|
||||||
|
<th scope="col" class="text-end">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="marketPriceRows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section data-view-panel="diagnostics" hidden>
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">Diagnostics</h1>
|
||||||
|
<p class="text-body-secondary mb-0">État sûr du bootstrap Config, Logging et runtime prix provider-neutral.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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</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 Off-chain</dt>
|
||||||
|
<dd id="runtimeOffchainProfile" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Providers</dt>
|
||||||
|
<dd id="runtimeProviderCount" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Providers ready</dt>
|
||||||
|
<dd id="runtimeReadyProviderCount" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Providers unavailable</dt>
|
||||||
|
<dd id="runtimeUnavailableProviderCount" 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">Fallback Logging</dt>
|
||||||
|
<dd id="runtimeLoggingFallback" class="col-sm-7">—</dd>
|
||||||
|
</dl>
|
||||||
|
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="app-footer bg-dark text-light">
|
||||||
|
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||||
|
<small>© 2026 SASEDEV</small>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="ts/main.ts" defer></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
235
crates/ksp-app-solprices-desk/frontend/sass/_app.scss
Normal file
235
crates/ksp-app-solprices-desk/frontend/sass/_app.scss
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/frontend/sass/_app.scss
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
$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,
|
||||||
|
.app-market-code,
|
||||||
|
.app-market-price,
|
||||||
|
.app-market-time-exact {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-toolbar {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-summary-card {
|
||||||
|
display: flex;
|
||||||
|
min-height: 58px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.65rem 0.8rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
background: var(--bs-tertiary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-summary-card strong {
|
||||||
|
min-width: 2ch;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-summary-label {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table-container {
|
||||||
|
max-height: min(62vh, 720px);
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table {
|
||||||
|
min-width: 1080px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--bs-tertiary-bg);
|
||||||
|
box-shadow: inset 0 -1px 0 var(--bs-border-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table tbody tr[data-availability="ready"] {
|
||||||
|
--bs-table-accent-bg: rgba($success, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table tbody tr[data-availability="cooling_down"],
|
||||||
|
.app-market-table tbody tr[data-availability="temporarily_unavailable"] {
|
||||||
|
--bs-table-accent-bg: rgba($warning, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-table tbody tr[data-availability="authentication_unavailable"],
|
||||||
|
.app-market-table tbody tr[data-availability="misconfigured"] {
|
||||||
|
--bs-table-accent-bg: rgba($danger, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-selection-column {
|
||||||
|
width: 42px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-time-column {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-provider-name,
|
||||||
|
.app-market-price {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-code,
|
||||||
|
.app-market-time-exact {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-price {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-time {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-time-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-time-label {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-time-value {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-row-loading {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.app-sidebar {
|
||||||
|
width: 190px;
|
||||||
|
min-width: 190px;
|
||||||
|
max-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-market-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
160
crates/ksp-app-solprices-desk/frontend/sass/_bootswatch.scss
Normal file
160
crates/ksp-app-solprices-desk/frontend/sass/_bootswatch.scss
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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;
|
||||||
248
crates/ksp-app-solprices-desk/frontend/sass/_simplebar.scss
Normal file
248
crates/ksp-app-solprices-desk/frontend/sass/_simplebar.scss
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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;
|
||||||
|
}
|
||||||
95
crates/ksp-app-solprices-desk/frontend/sass/_variables.scss
Normal file
95
crates/ksp-app-solprices-desk/frontend/sass/_variables.scss
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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;
|
||||||
10
crates/ksp-app-solprices-desk/frontend/sass/main.scss
Normal file
10
crates/ksp-app-solprices-desk/frontend/sass/main.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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";
|
||||||
137
crates/ksp-app-solprices-desk/frontend/sass/splash.scss
Normal file
137
crates/ksp-app-solprices-desk/frontend/sass/splash.scss
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/frontend/sass/splash.scss
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/ksp-app-solprices-desk/frontend/splash.html
Normal file
24
crates/ksp-app-solprices-desk/frontend/splash.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!-- file: crates/ksp-app-solprices-desk/frontend/splash.html -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link rel="icon" href="imgs/logo.png">
|
||||||
|
<link rel="stylesheet" href="sass/splash.scss">
|
||||||
|
<title>KSP SOL Prices Desk — Chargement</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="splash-container" style="opacity: 0;">
|
||||||
|
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP SOL Prices Desk">
|
||||||
|
<div id="app-name">SOL Prices 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>
|
||||||
110
crates/ksp-app-solprices-desk/frontend/ts/frontend_log.ts
Normal file
110
crates/ksp-app-solprices-desk/frontend/ts/frontend_log.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/frontend/ts/frontend_log.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_solprices_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);
|
||||||
|
}
|
||||||
17
crates/ksp-app-solprices-desk/frontend/ts/invoke.ts
Normal file
17
crates/ksp-app-solprices-desk/frontend/ts/invoke.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// file: crates/ksp-app-solprices-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
597
crates/ksp-app-solprices-desk/frontend/ts/main.ts
Normal file
597
crates/ksp-app-solprices-desk/frontend/ts/main.ts
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
|
||||||
|
// version: 8
|
||||||
|
|
||||||
|
import "bootstrap";
|
||||||
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
|
import "simplebar";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import type { MarketPriceRuntimeStatusDto } from "./bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts";
|
||||||
|
import type { MarketPriceProviderRowDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts";
|
||||||
|
import type { MarketPriceRefreshManyRequestDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts";
|
||||||
|
import type { MarketPriceRefreshResultDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.ts";
|
||||||
|
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||||
|
import { invokeKsp } from "./invoke";
|
||||||
|
|
||||||
|
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||||
|
installFrontendConsoleBridge("main");
|
||||||
|
|
||||||
|
type ViewId = "prices" | "diagnostics";
|
||||||
|
|
||||||
|
type AvailabilityPresentation = {
|
||||||
|
badgeClass: string;
|
||||||
|
iconClass: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewTitles: Record<ViewId, string> = {
|
||||||
|
prices: "Prices",
|
||||||
|
diagnostics: "Diagnostics",
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilityPresentations: Record<string, AvailabilityPresentation> = {
|
||||||
|
authentication_unavailable: { badgeClass: "text-bg-danger", iconClass: "fa-key", label: "Authentication unavailable" },
|
||||||
|
cooling_down: { badgeClass: "text-bg-warning", iconClass: "fa-clock", label: "Cooling down" },
|
||||||
|
disabled: { badgeClass: "text-bg-secondary", iconClass: "fa-ban", label: "Disabled" },
|
||||||
|
misconfigured: { badgeClass: "text-bg-danger", iconClass: "fa-triangle-exclamation", label: "Misconfigured" },
|
||||||
|
quota_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-gauge-high", label: "Quota unavailable" },
|
||||||
|
ready: { badgeClass: "text-bg-success", iconClass: "fa-circle-check", label: "Ready" },
|
||||||
|
temporarily_unavailable: { badgeClass: "text-bg-warning", iconClass: "fa-cloud-arrow-down", label: "Temporarily unavailable" },
|
||||||
|
unknown: { badgeClass: "text-bg-secondary", iconClass: "fa-circle-question", label: "Unknown" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const authLabels: Record<string, string> = {
|
||||||
|
none: "Keyless",
|
||||||
|
optional_api_key: "Optional API key",
|
||||||
|
required_api_key: "API key required",
|
||||||
|
unknown: "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
const semanticsLabels: Record<string, string> = {
|
||||||
|
aggregated_market: "Aggregated market",
|
||||||
|
dex_pair_usd: "DEX pair USD",
|
||||||
|
exchange_last_trade: "Exchange last trade",
|
||||||
|
solana_heuristic: "Solana heuristic",
|
||||||
|
solana_spot: "Solana spot",
|
||||||
|
unknown: "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsDateMaxUnixMillis = 8_640_000_000_000_000n;
|
||||||
|
|
||||||
|
let currentMarketPriceRows: MarketPriceProviderRowDto[] = [];
|
||||||
|
const selectedProviderIds = new Set<string>();
|
||||||
|
|
||||||
|
function isViewId(value: string | undefined): value is ViewId {
|
||||||
|
return value === "prices" || value === "diagnostics";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||||
|
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
||||||
|
panel.hidden = panel.dataset.viewPanel !== viewId;
|
||||||
|
});
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||||
|
button.classList.toggle("active", button.dataset.view === viewId);
|
||||||
|
});
|
||||||
|
const header = document.querySelector<HTMLElement>("#headerViewTitle");
|
||||||
|
if (header) {
|
||||||
|
header.textContent = viewTitles[viewId];
|
||||||
|
}
|
||||||
|
document.title = `SOL Prices Desk — ${viewTitles[viewId]}`;
|
||||||
|
if (source !== "startup") {
|
||||||
|
frontendDebug("main", "SOL Prices Desk view activated", { viewId, source });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNavigation(): void {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const viewId = button.dataset.view;
|
||||||
|
frontendDebug("main", "SOL Prices Desk navigation control clicked", { viewId: viewId ?? "unknown" });
|
||||||
|
if (isViewId(viewId)) {
|
||||||
|
activateView(viewId, "user");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
frontendTrace("main", "SOL Prices Desk navigation handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPricesDiagnostic(): void {
|
||||||
|
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
|
||||||
|
if (diagnostic) {
|
||||||
|
diagnostic.hidden = true;
|
||||||
|
diagnostic.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPricesDiagnostic(message: string): void {
|
||||||
|
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
|
||||||
|
if (diagnostic) {
|
||||||
|
diagnostic.hidden = false;
|
||||||
|
diagnostic.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedProviderIdsInRowOrder(): string[] {
|
||||||
|
return currentMarketPriceRows.filter(provider => selectedProviderIds.has(provider.providerId)).map(provider => provider.providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function synchronizeSelectedProviders(): void {
|
||||||
|
const currentIds = new Set(currentMarketPriceRows.map(provider => provider.providerId));
|
||||||
|
for (const providerId of selectedProviderIds) {
|
||||||
|
if (!currentIds.has(providerId)) {
|
||||||
|
selectedProviderIds.delete(providerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummary(): void {
|
||||||
|
const summaryValues: Record<string, number> = {
|
||||||
|
pricesSummaryProviders: currentMarketPriceRows.length,
|
||||||
|
pricesSummaryReady: currentMarketPriceRows.filter(provider => provider.availability === "ready" && !provider.loading).length,
|
||||||
|
pricesSummaryObserved: currentMarketPriceRows.filter(provider => provider.refreshed).length,
|
||||||
|
pricesSummaryAttention: currentMarketPriceRows.filter(provider => provider.availability !== "ready").length,
|
||||||
|
pricesSummaryLoading: currentMarketPriceRows.filter(provider => provider.loading).length,
|
||||||
|
};
|
||||||
|
for (const [elementId, value] of Object.entries(summaryValues)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function synchronizeBatchControls(): void {
|
||||||
|
const selectedCount = selectedProviderIdsInRowOrder().length;
|
||||||
|
const providerCount = currentMarketPriceRows.length;
|
||||||
|
const anyLoading = currentMarketPriceRows.some(provider => provider.loading);
|
||||||
|
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||||
|
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||||
|
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||||
|
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||||
|
const selectedCountLabel = document.querySelector<HTMLElement>("#selectedProviderCount");
|
||||||
|
if (selectedButton) {
|
||||||
|
selectedButton.disabled = selectedCount === 0 || anyLoading;
|
||||||
|
}
|
||||||
|
if (allButton) {
|
||||||
|
allButton.disabled = providerCount === 0 || anyLoading;
|
||||||
|
}
|
||||||
|
if (clearButton) {
|
||||||
|
clearButton.disabled = selectedCount === 0 || anyLoading;
|
||||||
|
}
|
||||||
|
if (selectAll) {
|
||||||
|
selectAll.disabled = providerCount === 0 || anyLoading;
|
||||||
|
selectAll.checked = providerCount !== 0 && selectedCount === providerCount;
|
||||||
|
selectAll.indeterminate = selectedCount > 0 && selectedCount < providerCount;
|
||||||
|
}
|
||||||
|
if (selectedCountLabel) {
|
||||||
|
selectedCountLabel.textContent = `${selectedCount} selected`;
|
||||||
|
}
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markProvidersLoading(providerIds: string[]): void {
|
||||||
|
const loadingIds = new Set(providerIds);
|
||||||
|
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
|
||||||
|
return loadingIds.has(provider.providerId) ? { ...provider, loading: true } : provider;
|
||||||
|
});
|
||||||
|
renderMarketPriceRows(currentMarketPriceRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRefreshResult(result: MarketPriceRefreshResultDto): void {
|
||||||
|
const refreshedRows = new Map<string, MarketPriceProviderRowDto>();
|
||||||
|
for (const provider of result.rows) {
|
||||||
|
refreshedRows.set(provider.providerId, provider);
|
||||||
|
}
|
||||||
|
currentMarketPriceRows = currentMarketPriceRows.map(provider => refreshedRows.get(provider.providerId) ?? provider);
|
||||||
|
renderMarketPriceRows(currentMarketPriceRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverMarketPriceRows(message: string): Promise<void> {
|
||||||
|
showPricesDiagnostic(message);
|
||||||
|
try {
|
||||||
|
await loadMarketPrices();
|
||||||
|
} catch {
|
||||||
|
frontendWarn("main", "SOL Prices Desk market-price refresh recovery reload failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMarketPrice(providerId: string): Promise<void> {
|
||||||
|
frontendDebug("main", "SOL Prices Desk market-price refresh control clicked", { providerId });
|
||||||
|
clearPricesDiagnostic();
|
||||||
|
markProvidersLoading([providerId]);
|
||||||
|
try {
|
||||||
|
const refreshed = await invokeKsp<MarketPriceProviderRowDto>("main", "refresh_market_price", { providerId });
|
||||||
|
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
|
||||||
|
return provider.providerId === refreshed.providerId ? refreshed : provider;
|
||||||
|
});
|
||||||
|
renderMarketPriceRows(currentMarketPriceRows);
|
||||||
|
frontendTrace("main", "SOL Prices Desk market-price refresh row applied", { providerId });
|
||||||
|
} catch {
|
||||||
|
frontendWarn("main", "SOL Prices Desk market-price refresh failed", { providerId });
|
||||||
|
await recoverMarketPriceRows("Le provider sélectionné n'a pas pu être rafraîchi. L'état précédent est conservé lorsqu'il reste disponible.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSelectedMarketPrices(): Promise<void> {
|
||||||
|
const providerIds = selectedProviderIdsInRowOrder();
|
||||||
|
frontendDebug("main", "SOL Prices Desk selected market-price refresh control clicked", { selectedCount: providerIds.length });
|
||||||
|
if (providerIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearPricesDiagnostic();
|
||||||
|
markProvidersLoading(providerIds);
|
||||||
|
const request: MarketPriceRefreshManyRequestDto = { providerIds };
|
||||||
|
try {
|
||||||
|
const result = await invokeKsp<MarketPriceRefreshResultDto>("main", "refresh_market_prices", { request });
|
||||||
|
applyRefreshResult(result);
|
||||||
|
frontendTrace("main", "SOL Prices Desk selected market-price refresh result applied", {
|
||||||
|
requestedCount: result.requestedCount,
|
||||||
|
refreshedCount: result.refreshedCount,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
frontendWarn("main", "SOL Prices Desk selected market-price refresh failed", { selectedCount: providerIds.length });
|
||||||
|
await recoverMarketPriceRows("La sélection n'a pas pu être rafraîchie. Les observations précédentes restent visibles avec leur état réel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAllMarketPrices(): Promise<void> {
|
||||||
|
frontendDebug("main", "SOL Prices Desk global market-price refresh control clicked", { providerCount: currentMarketPriceRows.length });
|
||||||
|
if (currentMarketPriceRows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearPricesDiagnostic();
|
||||||
|
markProvidersLoading(currentMarketPriceRows.map(provider => provider.providerId));
|
||||||
|
try {
|
||||||
|
const result = await invokeKsp<MarketPriceRefreshResultDto>("main", "refresh_all_market_prices");
|
||||||
|
applyRefreshResult(result);
|
||||||
|
frontendTrace("main", "SOL Prices Desk global market-price refresh result applied", {
|
||||||
|
requestedCount: result.requestedCount,
|
||||||
|
refreshedCount: result.refreshedCount,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
frontendWarn("main", "SOL Prices Desk global market-price refresh failed", { providerCount: currentMarketPriceRows.length });
|
||||||
|
await recoverMarketPriceRows("Le refresh global n'a pas pu être terminé. Les observations précédentes restent visibles avec leur état réel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "app-market-selection-column";
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.className = "form-check-input";
|
||||||
|
checkbox.checked = selectedProviderIds.has(provider.providerId);
|
||||||
|
checkbox.disabled = provider.loading;
|
||||||
|
checkbox.setAttribute("aria-label", `Select ${provider.displayName}`);
|
||||||
|
checkbox.addEventListener("change", () => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
selectedProviderIds.add(provider.providerId);
|
||||||
|
} else {
|
||||||
|
selectedProviderIds.delete(provider.providerId);
|
||||||
|
}
|
||||||
|
frontendDebug("main", "SOL Prices Desk market-price selection control changed", {
|
||||||
|
providerId: provider.providerId,
|
||||||
|
selected: checkbox.checked,
|
||||||
|
});
|
||||||
|
synchronizeBatchControls();
|
||||||
|
});
|
||||||
|
cell.append(checkbox);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendProviderCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.className = "app-market-provider-name";
|
||||||
|
name.textContent = provider.displayName;
|
||||||
|
const code = document.createElement("div");
|
||||||
|
code.className = "app-market-code";
|
||||||
|
code.textContent = provider.providerId;
|
||||||
|
cell.append(name, code);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBadgeCell(row: HTMLTableRowElement, label: string, badgeClass = "text-bg-light", className?: string): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
if (className) {
|
||||||
|
cell.className = className;
|
||||||
|
}
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = `badge ${badgeClass}`;
|
||||||
|
badge.textContent = label;
|
||||||
|
cell.append(badge);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function availabilityPresentation(provider: MarketPriceProviderRowDto): AvailabilityPresentation {
|
||||||
|
if (provider.loading) {
|
||||||
|
return { badgeClass: "text-bg-info", iconClass: "fa-spinner fa-spin", label: "Refreshing" };
|
||||||
|
}
|
||||||
|
return availabilityPresentations[provider.availability] ?? availabilityPresentations.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendAvailabilityCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
const presentation = availabilityPresentation(provider);
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = `badge ${presentation.badgeClass}`;
|
||||||
|
const icon = document.createElement("i");
|
||||||
|
icon.className = `fa-solid ${presentation.iconClass} me-1`;
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
badge.append(icon, document.createTextNode(presentation.label));
|
||||||
|
cell.append(badge);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendPriceCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
if (provider.price === null) {
|
||||||
|
const empty = document.createElement("span");
|
||||||
|
empty.className = "text-body-secondary";
|
||||||
|
empty.textContent = "No observation";
|
||||||
|
cell.append(empty);
|
||||||
|
} else {
|
||||||
|
const price = document.createElement("span");
|
||||||
|
price.className = "app-market-price";
|
||||||
|
price.textContent = provider.price;
|
||||||
|
const currency = document.createElement("small");
|
||||||
|
currency.className = "text-body-secondary ms-1";
|
||||||
|
currency.textContent = "USD";
|
||||||
|
cell.append(price, currency);
|
||||||
|
}
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnixMillis(value: string): { exact: string; iso: string; local: string } | null {
|
||||||
|
try {
|
||||||
|
const exact = BigInt(value);
|
||||||
|
if (exact < -jsDateMaxUnixMillis || exact > jsDateMaxUnixMillis) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = new Date(Number(exact));
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
exact: value,
|
||||||
|
iso: date.toISOString(),
|
||||||
|
local: date.toLocaleString("fr-CH", { dateStyle: "short", timeStyle: "medium", hour12: false }),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendTimestampValue(container: HTMLElement, label: string, value: string | null, emptyLabel: string): void {
|
||||||
|
const line = document.createElement("div");
|
||||||
|
line.className = "app-market-time-line";
|
||||||
|
const prefix = document.createElement("span");
|
||||||
|
prefix.className = "app-market-time-label";
|
||||||
|
prefix.textContent = label;
|
||||||
|
const valueContainer = document.createElement("span");
|
||||||
|
valueContainer.className = "app-market-time-value";
|
||||||
|
if (value === null) {
|
||||||
|
valueContainer.classList.add("text-body-secondary");
|
||||||
|
valueContainer.textContent = emptyLabel;
|
||||||
|
} else {
|
||||||
|
const formatted = formatUnixMillis(value);
|
||||||
|
if (formatted === null) {
|
||||||
|
const exact = document.createElement("span");
|
||||||
|
exact.className = "app-market-time-exact";
|
||||||
|
exact.textContent = `${value} ms`;
|
||||||
|
valueContainer.append(exact);
|
||||||
|
} else {
|
||||||
|
const time = document.createElement("time");
|
||||||
|
time.dateTime = formatted.iso;
|
||||||
|
time.textContent = formatted.local;
|
||||||
|
time.title = formatted.iso;
|
||||||
|
const exact = document.createElement("span");
|
||||||
|
exact.className = "app-market-time-exact d-block";
|
||||||
|
exact.textContent = `${formatted.exact} ms`;
|
||||||
|
valueContainer.append(time, exact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.append(prefix, valueContainer);
|
||||||
|
container.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendObservationTimeCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "app-market-time-column";
|
||||||
|
const time = document.createElement("div");
|
||||||
|
time.className = "app-market-time";
|
||||||
|
appendTimestampValue(time, "Provider", provider.providerTimestampUnixMillis, "Not supplied");
|
||||||
|
appendTimestampValue(time, "KSP", provider.receivedAtUnixMillis, "—");
|
||||||
|
cell.append(time);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendRetryCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "app-market-time-column";
|
||||||
|
const time = document.createElement("div");
|
||||||
|
time.className = "app-market-time";
|
||||||
|
appendTimestampValue(time, "Retry", provider.retryAtUnixMillis, "—");
|
||||||
|
cell.append(time);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
cell.className = "text-end";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "btn btn-sm btn-outline-primary text-nowrap";
|
||||||
|
button.disabled = provider.loading;
|
||||||
|
button.dataset.providerId = provider.providerId;
|
||||||
|
button.setAttribute("aria-label", `Refresh ${provider.displayName}`);
|
||||||
|
button.innerHTML = provider.loading
|
||||||
|
? '<i class="fa-solid fa-spinner fa-spin me-1" aria-hidden="true"></i>Refreshing…'
|
||||||
|
: '<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>Refresh';
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
void refreshMarketPrice(provider.providerId);
|
||||||
|
});
|
||||||
|
cell.append(button);
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
|
||||||
|
const loading = document.querySelector<HTMLElement>("#pricesLoading");
|
||||||
|
const empty = document.querySelector<HTMLElement>("#pricesEmpty");
|
||||||
|
const tableContainer = document.querySelector<HTMLElement>("#pricesTableContainer");
|
||||||
|
const tableBody = document.querySelector<HTMLTableSectionElement>("#marketPriceRows");
|
||||||
|
if (loading) {
|
||||||
|
loading.hidden = true;
|
||||||
|
}
|
||||||
|
if (!tableBody || !tableContainer || !empty) {
|
||||||
|
frontendWarn("main", "SOL Prices Desk market-price table elements are missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentMarketPriceRows = rows;
|
||||||
|
synchronizeSelectedProviders();
|
||||||
|
tableBody.replaceChildren();
|
||||||
|
empty.hidden = rows.length !== 0;
|
||||||
|
tableContainer.hidden = rows.length === 0;
|
||||||
|
for (const provider of rows) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
row.dataset.providerId = provider.providerId;
|
||||||
|
row.dataset.availability = provider.availability;
|
||||||
|
row.classList.toggle("app-market-row-loading", provider.loading);
|
||||||
|
appendSelectionCell(row, provider);
|
||||||
|
appendProviderCell(row, provider);
|
||||||
|
appendBadgeCell(row, provider.pair, "text-bg-primary");
|
||||||
|
appendBadgeCell(row, semanticsLabels[provider.semantics] ?? semanticsLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||||
|
appendBadgeCell(row, authLabels[provider.authMode] ?? authLabels.unknown, "text-bg-light", "d-none d-xl-table-cell");
|
||||||
|
appendAvailabilityCell(row, provider);
|
||||||
|
appendPriceCell(row, provider);
|
||||||
|
appendObservationTimeCell(row, provider);
|
||||||
|
appendRetryCell(row, provider);
|
||||||
|
appendRefreshCell(row, provider);
|
||||||
|
tableBody.append(row);
|
||||||
|
}
|
||||||
|
synchronizeBatchControls();
|
||||||
|
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", {
|
||||||
|
rowCount: rows.length,
|
||||||
|
observedCount: rows.filter(provider => provider.refreshed).length,
|
||||||
|
attentionCount: rows.filter(provider => provider.availability !== "ready").length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllProviders(selected: boolean): void {
|
||||||
|
selectedProviderIds.clear();
|
||||||
|
if (selected) {
|
||||||
|
for (const provider of currentMarketPriceRows) {
|
||||||
|
selectedProviderIds.add(provider.providerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frontendDebug("main", "SOL Prices Desk select-all providers control changed", {
|
||||||
|
selected,
|
||||||
|
selectedCount: selectedProviderIds.size,
|
||||||
|
});
|
||||||
|
renderMarketPriceRows(currentMarketPriceRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearProviderSelection(): void {
|
||||||
|
const selectedCount = selectedProviderIds.size;
|
||||||
|
frontendDebug("main", "SOL Prices Desk clear provider selection control clicked", { selectedCount });
|
||||||
|
selectedProviderIds.clear();
|
||||||
|
renderMarketPriceRows(currentMarketPriceRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindMarketPriceControls(): void {
|
||||||
|
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||||
|
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||||
|
const clearButton = document.querySelector<HTMLButtonElement>("#clearSelectionButton");
|
||||||
|
const selectAll = document.querySelector<HTMLInputElement>("#selectAllProviders");
|
||||||
|
selectedButton?.addEventListener("click", () => {
|
||||||
|
void refreshSelectedMarketPrices();
|
||||||
|
});
|
||||||
|
allButton?.addEventListener("click", () => {
|
||||||
|
void refreshAllMarketPrices();
|
||||||
|
});
|
||||||
|
clearButton?.addEventListener("click", () => {
|
||||||
|
clearProviderSelection();
|
||||||
|
});
|
||||||
|
selectAll?.addEventListener("change", () => {
|
||||||
|
selectAllProviders(selectAll.checked);
|
||||||
|
});
|
||||||
|
synchronizeBatchControls();
|
||||||
|
frontendTrace("main", "SOL Prices Desk market-price controls installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
runtimeCompositeProfile: status.activeCompositeProfile,
|
||||||
|
runtimeOffchainProfile: status.activeOffchainProfile,
|
||||||
|
runtimeVersion: status.applicationVersion,
|
||||||
|
runtimeShellPhase: status.shellPhase,
|
||||||
|
runtimeConfigDocuments: status.configDocumentCount.toString(),
|
||||||
|
runtimeProviderCount: status.providerCount.toString(),
|
||||||
|
runtimeReadyProviderCount: status.readyProviderCount.toString(),
|
||||||
|
runtimeUnavailableProviderCount: status.unavailableProviderCount.toString(),
|
||||||
|
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback",
|
||||||
|
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
|
||||||
|
};
|
||||||
|
for (const [elementId, value] of Object.entries(values)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
|
||||||
|
if (diagnostic) {
|
||||||
|
diagnostic.hidden = status.startupDiagnostic === null;
|
||||||
|
diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}:${status.startupDiagnostic.code} — ${status.startupDiagnostic.message}`;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "SOL Prices Desk market-price runtime status rendered", {
|
||||||
|
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||||
|
configDocumentCount: status.configDocumentCount,
|
||||||
|
providerCount: status.providerCount,
|
||||||
|
readyProviderCount: status.readyProviderCount,
|
||||||
|
unavailableProviderCount: status.unavailableProviderCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMarketPrices(): Promise<void> {
|
||||||
|
frontendTrace("main", "SOL Prices Desk provider registry load started");
|
||||||
|
const rows = await invokeKsp<MarketPriceProviderRowDto[]>("main", "list_market_prices");
|
||||||
|
currentMarketPriceRows = rows;
|
||||||
|
renderMarketPriceRows(rows);
|
||||||
|
frontendTrace("main", "SOL Prices Desk provider registry load completed", { rowCount: rows.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRuntimeStatus(): Promise<void> {
|
||||||
|
const status = await invokeKsp<MarketPriceRuntimeStatusDto>("main", "get_runtime_status");
|
||||||
|
renderRuntimeStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeMain(): Promise<void> {
|
||||||
|
const windowLabel = getCurrentWindow().label;
|
||||||
|
frontendInfo("main", "SOL Prices Desk main frontend loaded", { windowLabel });
|
||||||
|
bindNavigation();
|
||||||
|
bindMarketPriceControls();
|
||||||
|
activateView("prices", "startup");
|
||||||
|
try {
|
||||||
|
await loadRuntimeStatus();
|
||||||
|
} catch {
|
||||||
|
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
|
||||||
|
if (diagnostic) {
|
||||||
|
diagnostic.hidden = false;
|
||||||
|
diagnostic.textContent = "Le statut runtime initial n'a pas pu être chargé.";
|
||||||
|
}
|
||||||
|
frontendWarn("main", "SOL Prices Desk startup runtime status load failed");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await loadMarketPrices();
|
||||||
|
} catch {
|
||||||
|
const loading = document.querySelector<HTMLElement>("#pricesLoading");
|
||||||
|
if (loading) {
|
||||||
|
loading.hidden = true;
|
||||||
|
}
|
||||||
|
showPricesDiagnostic("L'inventaire provider-neutral n'a pas pu être chargé.");
|
||||||
|
frontendWarn("main", "SOL Prices Desk provider registry load failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
void initializeMain();
|
||||||
|
});
|
||||||
131
crates/ksp-app-solprices-desk/frontend/ts/splash.ts
Normal file
131
crates/ksp-app-solprices-desk/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/frontend/ts/splash.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import type { SplashOrderDto } from "./bindings/ksp_app_solprices_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", "SOL Prices 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();
|
||||||
|
});
|
||||||
BIN
crates/ksp-app-solprices-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-solprices-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-solprices-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-solprices-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
26
crates/ksp-app-solprices-desk/package.json
Normal file
26
crates/ksp-app-solprices-desk/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "ksp-app-solprices-desk",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.2.12-pre.9",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
158
crates/ksp-app-solprices-desk/src/app_state.rs
Normal file
158
crates/ksp-app-solprices-desk/src/app_state.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/app_state.rs
|
||||||
|
// version: 6
|
||||||
|
|
||||||
|
//! Shared backend state owned by the SOL Prices Desk Tauri application.
|
||||||
|
|
||||||
|
/// Shared SOL Prices Desk application state managed by Tauri.
|
||||||
|
pub(crate) struct AppState {
|
||||||
|
config_management: ksp_config_lib::ConfigManagement,
|
||||||
|
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||||
|
market_price_runtime: crate::MarketPriceRuntime,
|
||||||
|
splash_settings: crate::SplashSettings,
|
||||||
|
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::AppState {
|
||||||
|
/// Initializes Config ownership, composite-managed Logging and the provider-neutral market-price runtime.
|
||||||
|
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
|
||||||
|
let config_management = crate::config_management(arguments);
|
||||||
|
let config_management = match config_management {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let runtime_identity = crate::launch_identity();
|
||||||
|
let runtime_identity = match runtime_identity {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
|
||||||
|
let logging_startup = match logging_startup {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let offchain_transport_startup = crate::initialize_offchain_transport(&config_management);
|
||||||
|
let offchain_transport_startup = match offchain_transport_startup {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let market_price_runtime = crate::MarketPriceRuntime::new(offchain_transport_startup);
|
||||||
|
let market_price_runtime = match market_price_runtime {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let splash_settings = crate::SplashSettings::load();
|
||||||
|
let splash_settings = match splash_settings {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, error_domain = error.code().domain(), error_code = error.code().code(), "managed splash timings are invalid; using transient in-memory defaults");
|
||||||
|
crate::SplashSettings::fallback()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, minimum_ms = splash_settings.minimum_ms(), minimum_source = splash_settings.minimum_source(), fade_in_ms = splash_settings.fade_in_ms(), fade_in_source = splash_settings.fade_in_source(), fade_out_ms = splash_settings.fade_out_ms(), fade_out_source = splash_settings.fade_out_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved SOL Prices Desk splash timings");
|
||||||
|
return std::result::Result::Ok(Self {
|
||||||
|
config_management,
|
||||||
|
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||||
|
guard: logging_startup.guard,
|
||||||
|
active_profile_id: logging_startup.active_profile_id,
|
||||||
|
fallback_active: logging_startup.fallback_active,
|
||||||
|
startup_diagnostic: logging_startup.startup_diagnostic,
|
||||||
|
}),
|
||||||
|
market_price_runtime,
|
||||||
|
splash_settings,
|
||||||
|
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists the current provider-neutral market-price rows without triggering network refresh.
|
||||||
|
pub(crate) fn list_market_prices(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRowDto>> {
|
||||||
|
return self.market_price_runtime.list_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes one provider by opaque identifier and returns its updated safe row projection.
|
||||||
|
pub(crate) async fn refresh_market_price(&self, provider_id: String) -> ksp_core_lib::Result<crate::MarketPriceProviderRowDto> {
|
||||||
|
return self.market_price_runtime.refresh_one(provider_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes a selected provider set in deterministic request order and returns only safe updated rows/counts.
|
||||||
|
pub(crate) async fn refresh_market_prices(
|
||||||
|
&self,
|
||||||
|
request: crate::MarketPriceRefreshManyRequestDto,
|
||||||
|
) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||||
|
return self.market_price_runtime.refresh_many(request.into_provider_ids()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes every configured provider in the deterministic order owned by Off-chain Transport.
|
||||||
|
pub(crate) async fn refresh_all_market_prices(&self) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||||
|
return self.market_price_runtime.refresh_all().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the safe runtime status exposed by the SOL Prices Desk shell.
|
||||||
|
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::MarketPriceRuntimeStatusDto> {
|
||||||
|
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||||
|
let document_count = u32::try_from(document_count);
|
||||||
|
let document_count = match document_count {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||||
|
"Config registry contains too many descriptors for the SOL Prices Desk runtime DTO",
|
||||||
|
)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let provider_counts = self.market_price_runtime.provider_counts();
|
||||||
|
let (provider_count, ready_provider_count, unavailable_provider_count) = match provider_counts {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let runtime = self.logging_runtime.lock();
|
||||||
|
let runtime = match runtime {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||||
|
"SOL Prices Desk Logging runtime state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let _keep_guard_alive = &runtime.guard;
|
||||||
|
let startup = self.market_price_runtime.startup();
|
||||||
|
let offchain = startup.resolved();
|
||||||
|
return std::result::Result::Ok(crate::MarketPriceRuntimeStatusDto {
|
||||||
|
active_composite_profile: startup.composite_profile_id().to_owned(),
|
||||||
|
active_logging_profile: runtime.active_profile_id.clone(),
|
||||||
|
active_offchain_profile: offchain.profile_id().to_owned(),
|
||||||
|
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
config_document_count: document_count,
|
||||||
|
fallback_logging_active: runtime.fallback_active,
|
||||||
|
provider_count,
|
||||||
|
ready_provider_count,
|
||||||
|
shell_phase: "pre.007-ux-instrumentation".to_owned(),
|
||||||
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
|
unavailable_provider_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resolved splash timings captured during application bootstrap.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||||
|
return self.splash_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
|
||||||
|
pub(crate) fn begin_splash_sequence(&self) -> bool {
|
||||||
|
return self
|
||||||
|
.splash_sequence_started
|
||||||
|
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
|
||||||
|
.is_ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoggingRuntimeState {
|
||||||
|
guard: ksp_logging_lib::LoggingGuard,
|
||||||
|
active_profile_id: std::option::Option<String>,
|
||||||
|
fallback_active: bool,
|
||||||
|
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||||
|
}
|
||||||
197
crates/ksp-app-solprices-desk/src/bootstrap.rs
Normal file
197
crates/ksp-app-solprices-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/bootstrap.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Config composite and Logging bootstrap for SOL Prices 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the concrete SOL Prices Desk Config composite using its registered logical `file_id` and autonomous default profile.
|
||||||
|
pub(crate) fn load_solprices_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_SOLPRICES_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 SOL Prices 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, "SOL Prices 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,
|
||||||
|
"SOL Prices 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes Logging from the SOL Prices Desk composite, with the same bounded fallback policy as the other KSP desks.
|
||||||
|
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 SOL Prices 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)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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_solprices_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 offchain_profile = crate::required_composite_component_profile(
|
||||||
|
&composite,
|
||||||
|
crate::COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT,
|
||||||
|
ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT,
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = offchain_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 SOL Prices 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
27
crates/ksp-app-solprices-desk/src/constants.rs
Normal file
27
crates/ksp-app-solprices-desk/src/constants.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/constants.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Application-owned composition identifiers, tracing targets and domains.
|
||||||
|
|
||||||
|
/// Composite-local identifier for the standard Logging component.
|
||||||
|
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
|
||||||
|
/// Composite-local identifier for the standard Off-chain Transport component.
|
||||||
|
pub(crate) const COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT: &str = "offchain_transport";
|
||||||
|
/// Structured domain used while bootstrapping Config and Logging.
|
||||||
|
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "solprices.bootstrap";
|
||||||
|
/// Structured domain used by technical frontend events.
|
||||||
|
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||||
|
/// Structured domain used by the Config-selected Off-chain Transport runtime.
|
||||||
|
pub(crate) const TRACING_DOMAIN_OFFCHAIN_TRANSPORT: &str = "solprices.offchain_transport";
|
||||||
|
/// Structured domain used by the SOL Prices Desk shell.
|
||||||
|
pub(crate) const TRACING_DOMAIN_SHELL: &str = "solprices.shell";
|
||||||
|
/// Structured domain used by Tauri window lifecycle operations.
|
||||||
|
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||||
|
/// Owning target for backend events emitted by SOL Prices Desk.
|
||||||
|
pub(crate) const TRACING_TARGET: &str = "ksp-app-solprices-desk";
|
||||||
|
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-solprices-desk.frontend";
|
||||||
|
/// Owning target for main-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-solprices-desk.frontend.main";
|
||||||
|
/// Owning target for splash-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-solprices-desk.frontend.splash";
|
||||||
64
crates/ksp-app-solprices-desk/src/dto_common.rs
Normal file
64
crates/ksp-app-solprices-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/dto_common.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Common Tauri DTOs shared by the SOL Prices Desk shell.
|
||||||
|
|
||||||
|
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_solprices_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 crate::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 provider-neutral application/runtime snapshot exposed to the SOL Prices Desk shell.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts")]
|
||||||
|
pub(crate) struct MarketPriceRuntimeStatusDto {
|
||||||
|
/// SOL Prices Desk composite profile selected for this launch.
|
||||||
|
pub(crate) active_composite_profile: String,
|
||||||
|
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
|
||||||
|
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||||
|
/// Standard Off-chain Transport profile selected by the active composite.
|
||||||
|
pub(crate) active_offchain_profile: String,
|
||||||
|
/// Cargo application version.
|
||||||
|
pub(crate) application_version: String,
|
||||||
|
/// Number of logical Config resources registered by the current runtime.
|
||||||
|
pub(crate) config_document_count: u32,
|
||||||
|
/// Whether SOL Prices Desk had to install its transient in-memory Logging fallback.
|
||||||
|
pub(crate) fallback_logging_active: bool,
|
||||||
|
/// Total number of configured provider-neutral market-price rows.
|
||||||
|
pub(crate) provider_count: u32,
|
||||||
|
/// Number of providers currently classified as immediately ready.
|
||||||
|
pub(crate) ready_provider_count: u32,
|
||||||
|
/// Current implementation phase exposed by the 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 providers currently not classified as immediately ready.
|
||||||
|
pub(crate) unavailable_provider_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/dto_common.rs"]
|
||||||
|
mod tests;
|
||||||
35
crates/ksp-app-solprices-desk/src/errors.rs
Normal file
35
crates/ksp-app-solprices-desk/src/errors.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/errors.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Application-local error codes for SOL Prices Desk composition and desktop runtime surfaces.
|
||||||
|
|
||||||
|
/// Shared SOL Prices Desk application state is internally inconsistent.
|
||||||
|
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("solprices_desk", "app_state_invalid");
|
||||||
|
/// Shared SOL Prices 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("solprices_desk", "app_state_lock_failed");
|
||||||
|
/// The SOL Prices Desk Config composite is missing or misroutes one required standard component.
|
||||||
|
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("solprices_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("solprices_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("solprices_desk", "frontend_log_target_invalid");
|
||||||
|
/// SOL Prices 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("solprices_desk", "logging_bootstrap_failed");
|
||||||
|
/// One requested market-price provider already has an explicit refresh in flight.
|
||||||
|
pub(crate) const ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("solprices_desk", "market_price_refresh_conflict");
|
||||||
|
/// A selected market-price refresh contains duplicate or unknown provider identifiers.
|
||||||
|
pub(crate) const ERROR_CODE_MARKET_PRICE_SELECTION_INVALID: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("solprices_desk", "market_price_selection_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("solprices_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("solprices_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("solprices_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("solprices_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("solprices_desk", "tauri_window_operation_failed");
|
||||||
205
crates/ksp-app-solprices-desk/src/frontend_logging.rs
Normal file
205
crates/ksp-app-solprices-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/frontend_logging.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! KSP-owned bridge for technical log events emitted by SOL Prices Desk frontend scripts.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Log payload sent by SOL Prices Desk frontend scripts.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_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 {
|
||||||
|
Trace,
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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() {
|
||||||
|
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
||||||
|
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
||||||
|
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
||||||
|
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
||||||
|
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
||||||
|
_ => 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::Trace => ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET_FRONTEND,
|
||||||
|
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||||
|
action = "frontend_log",
|
||||||
|
target_id = "frontend",
|
||||||
|
"{message}"
|
||||||
|
),
|
||||||
|
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||||
|
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::Warn => ksp_logging_lib::warn!(
|
||||||
|
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}"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||||
|
return match level {
|
||||||
|
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::Debug => ksp_logging_lib::debug!(
|
||||||
|
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::Warn => ksp_logging_lib::warn!(
|
||||||
|
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}"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||||
|
return match level {
|
||||||
|
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::Debug => ksp_logging_lib::debug!(
|
||||||
|
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::Warn => ksp_logging_lib::warn!(
|
||||||
|
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}"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/frontend_logging.rs"]
|
||||||
|
mod tests;
|
||||||
120
crates/ksp-app-solprices-desk/src/lib.rs
Normal file
120
crates/ksp-app-solprices-desk/src/lib.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/lib.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Tauri desktop application scaffold for provider-neutral SOL/USD price visualization.
|
||||||
|
|
||||||
|
#![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 market_price_runtime;
|
||||||
|
mod offchain_runtime;
|
||||||
|
mod splash;
|
||||||
|
mod tauri;
|
||||||
|
mod tw_main;
|
||||||
|
mod tw_splash;
|
||||||
|
|
||||||
|
/// Runs the KSP SOL prices desktop application.
|
||||||
|
pub use self::tauri::run;
|
||||||
|
|
||||||
|
/// Shared SOL Prices 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 scaffold Logging runtime from Config, with a bounded in-memory fallback.
|
||||||
|
pub(crate) use self::bootstrap::initialize_logging;
|
||||||
|
/// Loads the SOL Prices Desk composite selected by its registered logical file identifier.
|
||||||
|
pub(crate) use self::bootstrap::load_solprices_desk_composite;
|
||||||
|
/// Resolves one required standard Config profile from the SOL Prices Desk composite.
|
||||||
|
pub(crate) use self::bootstrap::required_composite_component_profile;
|
||||||
|
/// Composite-local identifier for the standard Logging component.
|
||||||
|
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
|
||||||
|
/// Composite-local identifier for the standard Off-chain Transport component.
|
||||||
|
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT;
|
||||||
|
/// 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 Config-selected Off-chain Transport runtime.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_OFFCHAIN_TRANSPORT;
|
||||||
|
/// Structured domain used by the SOL Prices Desk shell.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||||
|
/// Structured domain used by Tauri window lifecycle operations.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||||
|
/// Owning target for backend events emitted by SOL Prices Desk.
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||||
|
/// Owning target for main-window frontend events.
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||||
|
/// Owning target for splash-window frontend events.
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||||
|
/// Safe command error projection exposed to Tauri commands.
|
||||||
|
pub(crate) use self::dto_common::CommandErrorDto;
|
||||||
|
/// Safe provider-neutral runtime snapshot exposed to the shell.
|
||||||
|
pub(crate) use self::dto_common::MarketPriceRuntimeStatusDto;
|
||||||
|
/// Shared SOL Prices Desk application state is internally inconsistent.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||||
|
/// Shared SOL Prices Desk runtime state cannot be locked safely.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||||
|
/// The SOL Prices Desk Config composite is missing or misroutes a required 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;
|
||||||
|
/// SOL Prices Desk could not install the managed Logging runtime or its safe fallback.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||||
|
/// One requested market-price provider already has an explicit refresh in flight.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT;
|
||||||
|
/// A selected market-price refresh contains duplicate or unknown provider identifiers.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID;
|
||||||
|
/// Splash readiness was invoked from a window other than the splash window.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||||
|
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||||
|
/// Tauri runtime assembly or execution failed.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||||
|
/// A required Tauri window is missing from the configured application runtime.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||||
|
/// A Tauri window show/focus/destroy/event operation failed.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||||
|
/// Log payload sent by SOL Prices 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 SOL Prices Desk process launch.
|
||||||
|
pub(crate) use self::logging_runtime::launch_identity;
|
||||||
|
/// Safe provider-neutral market-price row exposed to the frontend.
|
||||||
|
pub(crate) use self::market_price_runtime::MarketPriceProviderRowDto;
|
||||||
|
/// Bounded provider-neutral request used by the selected-provider refresh command.
|
||||||
|
pub(crate) use self::market_price_runtime::MarketPriceRefreshManyRequestDto;
|
||||||
|
/// Safe result returned by selected/global market-price refresh commands.
|
||||||
|
pub(crate) use self::market_price_runtime::MarketPriceRefreshResultDto;
|
||||||
|
/// Backend application runtime owning the Config-selected service and in-memory market-price presentation state.
|
||||||
|
pub(crate) use self::market_price_runtime::MarketPriceRuntime;
|
||||||
|
/// Resolved composite and Off-chain Transport configuration retained by SOL Prices Desk.
|
||||||
|
pub(crate) use self::offchain_runtime::OffchainTransportStartup;
|
||||||
|
/// Resolves the composite-selected Off-chain Transport profile and constructs the Config-owned service.
|
||||||
|
pub(crate) use self::offchain_runtime::initialize_offchain_transport;
|
||||||
|
/// Command emitted by Rust to the splash frontend.
|
||||||
|
pub(crate) use self::splash::SplashOrderDto;
|
||||||
|
/// Runtime timings used by the common desk splash lifecycle.
|
||||||
|
pub(crate) use self::splash::SplashSettings;
|
||||||
|
/// Resolves the required main window or returns a typed error.
|
||||||
|
pub(crate) use self::tw_main::require_main_window;
|
||||||
|
/// Shows and focuses the main SOL Prices 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;
|
||||||
10
crates/ksp-app-solprices-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-solprices-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/logging_runtime.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! SOL Prices Desk Logging runtime identity helpers.
|
||||||
|
|
||||||
|
/// Creates the stable runtime identity for this SOL Prices 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);
|
||||||
|
}
|
||||||
58
crates/ksp-app-solprices-desk/src/main.rs
Normal file
58
crates/ksp-app-solprices-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/main.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Binary entry point for the KSP SOL prices 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 SOL Prices 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_solprices_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-solprices-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_solprices_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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
611
crates/ksp-app-solprices-desk/src/market_price_runtime.rs
Normal file
611
crates/ksp-app-solprices-desk/src/market_price_runtime.rs
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Provider-neutral market-price presentation runtime owned by SOL Prices Desk.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS: usize = 64;
|
||||||
|
|
||||||
|
/// Safe provider-neutral row projected to the SOL Prices Desk frontend.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts")]
|
||||||
|
pub(crate) struct MarketPriceProviderRowDto {
|
||||||
|
/// Generic authentication capability code for the configured provider mode.
|
||||||
|
pub(crate) auth_mode: String,
|
||||||
|
/// Generic current availability code.
|
||||||
|
pub(crate) availability: String,
|
||||||
|
/// Safe provider display name supplied by Off-chain Transport.
|
||||||
|
pub(crate) display_name: String,
|
||||||
|
/// Whether one refresh is currently in flight for this provider.
|
||||||
|
pub(crate) loading: bool,
|
||||||
|
/// Stable V1 pair label.
|
||||||
|
pub(crate) pair: String,
|
||||||
|
/// Exact canonical SOL/USD decimal string, when an observation exists.
|
||||||
|
pub(crate) price: std::option::Option<String>,
|
||||||
|
/// Opaque provider identifier accepted by refresh commands.
|
||||||
|
pub(crate) provider_id: String,
|
||||||
|
/// Provider-supplied observation timestamp in exact Unix milliseconds, when genuinely supplied.
|
||||||
|
pub(crate) provider_timestamp_unix_millis: std::option::Option<String>,
|
||||||
|
/// KSP receipt timestamp in exact Unix milliseconds, when an observation exists.
|
||||||
|
pub(crate) received_at_unix_millis: std::option::Option<String>,
|
||||||
|
/// Whether this row currently retains at least one successful observation.
|
||||||
|
pub(crate) refreshed: bool,
|
||||||
|
/// Known next retry timestamp in exact Unix milliseconds for cooldown/temporary states.
|
||||||
|
pub(crate) retry_at_unix_millis: std::option::Option<String>,
|
||||||
|
/// Generic price-semantics code retained from the provider descriptor.
|
||||||
|
pub(crate) semantics: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded provider-neutral request used by the selected-provider refresh command.
|
||||||
|
#[derive(Debug, serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts")]
|
||||||
|
pub(crate) struct MarketPriceRefreshManyRequestDto {
|
||||||
|
/// Opaque provider identifiers requested by the frontend in deterministic row order.
|
||||||
|
pub(crate) provider_ids: std::vec::Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::MarketPriceRefreshManyRequestDto {
|
||||||
|
/// Consumes the IPC DTO and returns the opaque provider identifiers for authoritative backend validation.
|
||||||
|
pub(crate) fn into_provider_ids(self) -> std::vec::Vec<String> {
|
||||||
|
return self.provider_ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe result of one selected/global refresh operation.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.ts")]
|
||||||
|
pub(crate) struct MarketPriceRefreshResultDto {
|
||||||
|
/// Number of provider refreshes that produced a new normalized observation.
|
||||||
|
pub(crate) refreshed_count: u32,
|
||||||
|
/// Number of providers included in the validated operation.
|
||||||
|
pub(crate) requested_count: u32,
|
||||||
|
/// Updated provider-neutral rows in the exact dispatch/outcome order.
|
||||||
|
pub(crate) rows: std::vec::Vec<MarketPriceProviderRowDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backend application runtime that owns the Config-selected Off-chain service and current in-memory presentation state.
|
||||||
|
pub(crate) struct MarketPriceRuntime {
|
||||||
|
presentation: std::sync::Mutex<MarketPricePresentationState>,
|
||||||
|
startup: crate::OffchainTransportStartup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::MarketPriceRuntime {
|
||||||
|
/// Builds the application presentation runtime from the already Config-resolved Off-chain startup.
|
||||||
|
pub(crate) fn new(startup: crate::OffchainTransportStartup) -> ksp_core_lib::Result<Self> {
|
||||||
|
let registry = startup.resolved().service().registry();
|
||||||
|
let mut entries = std::collections::BTreeMap::new();
|
||||||
|
for entry in registry.entries() {
|
||||||
|
let previous = entries
|
||||||
|
.insert(entry.descriptor().id().as_str().to_owned(), MarketPricePresentationEntry { loading: false, observation: std::option::Option::None });
|
||||||
|
if previous.is_some() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime contains duplicate provider state")
|
||||||
|
.with_context("provider_id", entry.descriptor().id().as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_count = entries.len(),
|
||||||
|
"initialized SOL Prices Desk market-price presentation runtime"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(Self { presentation: std::sync::Mutex::new(MarketPricePresentationState { entries }), startup });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Config/bootstrap metadata retained by the price runtime.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn startup(&self) -> &crate::OffchainTransportStartup {
|
||||||
|
return &self.startup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns total, ready and unavailable provider counts from the detached generic registry snapshot.
|
||||||
|
pub(crate) fn provider_counts(&self) -> ksp_core_lib::Result<(u32, u32, u32)> {
|
||||||
|
let registry = self.startup.resolved().service().registry();
|
||||||
|
let total = count_to_u32(registry.len(), "provider_count");
|
||||||
|
let total = match total {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut ready_count = 0usize;
|
||||||
|
for entry in registry.entries() {
|
||||||
|
if matches!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready) {
|
||||||
|
ready_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ready = count_to_u32(ready_count, "ready_provider_count");
|
||||||
|
let ready = match ready {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let unavailable = total.saturating_sub(ready);
|
||||||
|
return std::result::Result::Ok((total, ready, unavailable));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Projects the current registry and in-memory observation state as deterministic frontend rows without network dispatch.
|
||||||
|
pub(crate) fn list_rows(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRowDto>> {
|
||||||
|
let registry = self.startup.resolved().service().registry();
|
||||||
|
let presentation = lock_presentation(&self.presentation);
|
||||||
|
let presentation = match presentation {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut rows = std::vec::Vec::with_capacity(registry.len());
|
||||||
|
for entry in registry.entries() {
|
||||||
|
let provider_id = entry.descriptor().id().as_str();
|
||||||
|
let presentation_entry = presentation.entries.get(provider_id);
|
||||||
|
let presentation_entry = match presentation_entry {
|
||||||
|
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, "SOL Prices Desk market-price runtime is missing one registry row")
|
||||||
|
.with_context("provider_id", provider_id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rows.push(project_registry_entry(entry, presentation_entry));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes one opaque provider through Off-chain Transport and updates only its in-memory presentation row.
|
||||||
|
pub(crate) async fn refresh_one(&self, provider_id: String) -> ksp_core_lib::Result<crate::MarketPriceProviderRowDto> {
|
||||||
|
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id);
|
||||||
|
let provider_id = match provider_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let provider_ids = [provider_id.clone()];
|
||||||
|
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(error) = loading {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_id = provider_id.as_str(),
|
||||||
|
"started SOL Prices Desk single-provider market-price refresh"
|
||||||
|
);
|
||||||
|
let outcome = self.startup.resolved().service().refresh(&provider_id).await;
|
||||||
|
let outcome = match outcome {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let row = self.apply_outcome(&outcome);
|
||||||
|
let row = match row {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_id = provider_id.as_str(),
|
||||||
|
refreshed = outcome.refreshed(),
|
||||||
|
availability = row.availability.as_str(),
|
||||||
|
"completed SOL Prices Desk single-provider market-price refresh"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes a frontend-selected provider set in deterministic request order through the generic Off-chain service.
|
||||||
|
pub(crate) async fn refresh_many(&self, provider_ids: std::vec::Vec<String>) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||||
|
let provider_ids = parse_provider_ids(provider_ids);
|
||||||
|
let provider_ids = match provider_ids {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(error) = loading {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_count = provider_ids.len(),
|
||||||
|
"started SOL Prices Desk selected-provider market-price refresh"
|
||||||
|
);
|
||||||
|
let outcomes = self.startup.resolved().service().refresh_many(provider_ids.as_slice()).await;
|
||||||
|
let outcomes = match outcomes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let result = self.apply_outcomes(outcomes.as_slice());
|
||||||
|
let result = match result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_count = result.requested_count,
|
||||||
|
refreshed_count = result.refreshed_count,
|
||||||
|
"completed SOL Prices Desk selected-provider market-price refresh"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes every configured provider through the generic Off-chain service in its stable provider order.
|
||||||
|
pub(crate) async fn refresh_all(&self) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||||
|
let registry = self.startup.resolved().service().registry();
|
||||||
|
let provider_ids = registry.entries().iter().map(|entry| return entry.descriptor().id().clone()).collect::<std::vec::Vec<_>>();
|
||||||
|
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(error) = loading {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_count = provider_ids.len(),
|
||||||
|
"started SOL Prices Desk global market-price refresh"
|
||||||
|
);
|
||||||
|
let outcomes = self.startup.resolved().service().refresh_all().await;
|
||||||
|
let outcomes = match outcomes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let result = self.apply_outcomes(outcomes.as_slice());
|
||||||
|
let result = match result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||||
|
if let std::result::Result::Err(clear_error) = cleared {
|
||||||
|
return std::result::Result::Err(clear_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||||
|
provider_count = result.requested_count,
|
||||||
|
refreshed_count = result.refreshed_count,
|
||||||
|
"completed SOL Prices Desk global market-price refresh"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_outcome(&self, outcome: &ksp_offchain_transport_lib::MarketPriceRefreshOutcome) -> ksp_core_lib::Result<crate::MarketPriceProviderRowDto> {
|
||||||
|
let registry = self.startup.resolved().service().registry();
|
||||||
|
let registry_entry = registry.entry(outcome.provider_id());
|
||||||
|
let registry_entry = match registry_entry {
|
||||||
|
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, "SOL Prices Desk refresh outcome references an unknown registry provider")
|
||||||
|
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let presentation = lock_presentation(&self.presentation);
|
||||||
|
let mut presentation = match presentation {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let applied = apply_outcome_to_state(&mut presentation, outcome);
|
||||||
|
if let std::result::Result::Err(error) = applied {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let presentation_entry = presentation.entries.get(outcome.provider_id().as_str());
|
||||||
|
return match presentation_entry {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(project_registry_entry(registry_entry, value)),
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime is missing refreshed provider state")
|
||||||
|
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_outcomes(&self, outcomes: &[ksp_offchain_transport_lib::MarketPriceRefreshOutcome]) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||||
|
let registry = self.startup.resolved().service().registry();
|
||||||
|
let presentation = lock_presentation(&self.presentation);
|
||||||
|
let mut presentation = match presentation {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut rows = std::vec::Vec::with_capacity(outcomes.len());
|
||||||
|
let mut refreshed_count = 0usize;
|
||||||
|
for outcome in outcomes {
|
||||||
|
let registry_entry = registry.entry(outcome.provider_id());
|
||||||
|
let registry_entry = match registry_entry {
|
||||||
|
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,
|
||||||
|
"SOL Prices Desk refresh outcome references an unknown registry provider",
|
||||||
|
)
|
||||||
|
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let applied = apply_outcome_to_state(&mut presentation, outcome);
|
||||||
|
if let std::result::Result::Err(error) = applied {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if outcome.refreshed() {
|
||||||
|
refreshed_count += 1;
|
||||||
|
}
|
||||||
|
let presentation_entry = presentation.entries.get(outcome.provider_id().as_str());
|
||||||
|
let presentation_entry = match presentation_entry {
|
||||||
|
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,
|
||||||
|
"SOL Prices Desk market-price runtime is missing refreshed provider state",
|
||||||
|
)
|
||||||
|
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rows.push(project_registry_entry(registry_entry, presentation_entry));
|
||||||
|
}
|
||||||
|
let requested_count = count_to_u32(outcomes.len(), "refresh_requested_count");
|
||||||
|
let requested_count = match requested_count {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let refreshed_count = count_to_u32(refreshed_count, "refresh_success_count");
|
||||||
|
let refreshed_count = match refreshed_count {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::MarketPriceRefreshResultDto { refreshed_count, requested_count, rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_refresh(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> {
|
||||||
|
let presentation = lock_presentation(&self.presentation);
|
||||||
|
let mut presentation = match presentation {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return begin_refresh_in_state(&mut presentation, provider_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_loading(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> {
|
||||||
|
let presentation = lock_presentation(&self.presentation);
|
||||||
|
let mut presentation = match presentation {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
for provider_id in provider_ids {
|
||||||
|
let cleared = set_loading_in_state(&mut presentation, provider_id.as_str(), false);
|
||||||
|
if let std::result::Result::Err(error) = cleared {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MarketPricePresentationEntry {
|
||||||
|
loading: bool,
|
||||||
|
observation: std::option::Option<ksp_offchain_transport_lib::MarketPriceObservation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MarketPricePresentationState {
|
||||||
|
entries: std::collections::BTreeMap<String, MarketPricePresentationEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_outcome_to_state(
|
||||||
|
presentation: &mut MarketPricePresentationState,
|
||||||
|
outcome: &ksp_offchain_transport_lib::MarketPriceRefreshOutcome,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let entry = presentation.entries.get_mut(outcome.provider_id().as_str());
|
||||||
|
let entry = match entry {
|
||||||
|
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, "SOL Prices Desk market-price presentation state is missing refresh provider")
|
||||||
|
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
entry.loading = false;
|
||||||
|
if let std::option::Option::Some(observation) = outcome.observation() {
|
||||||
|
entry.observation = std::option::Option::Some(observation.clone());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_mode_code(auth_mode: ksp_offchain_transport_lib::MarketPriceProviderAuthMode) -> &'static str {
|
||||||
|
return match auth_mode {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None => "none",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::OptionalApiKey => "optional_api_key",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::RequiredApiKey => "required_api_key",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn availability_code(availability: ksp_offchain_transport_lib::MarketPriceProviderAvailability) -> &'static str {
|
||||||
|
return match availability {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::AuthenticationUnavailable => "authentication_unavailable",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::CoolingDown { .. } => "cooling_down",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled => "disabled",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Misconfigured => "misconfigured",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::QuotaUnavailable => "quota_unavailable",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready => "ready",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderAvailability::TemporarilyUnavailable { .. } => "temporarily_unavailable",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_refresh_in_state(
|
||||||
|
presentation: &mut MarketPricePresentationState,
|
||||||
|
provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId],
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
|
for provider_id in provider_ids {
|
||||||
|
if !seen.insert(provider_id.as_str().to_owned()) {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID,
|
||||||
|
"SOL Prices Desk market-price refresh selection contains a duplicate provider",
|
||||||
|
)
|
||||||
|
.with_context("provider_id", provider_id.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let entry = presentation.entries.get(provider_id.as_str());
|
||||||
|
let entry = match entry {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID,
|
||||||
|
"SOL Prices Desk market-price refresh selection contains an unknown provider",
|
||||||
|
)
|
||||||
|
.with_context("provider_id", provider_id.as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if entry.loading {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT,
|
||||||
|
"SOL Prices Desk market-price provider already has a refresh in flight",
|
||||||
|
)
|
||||||
|
.with_context("provider_id", provider_id.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for provider_id in provider_ids {
|
||||||
|
let updated = set_loading_in_state(presentation, provider_id.as_str(), true);
|
||||||
|
if let std::result::Result::Err(error) = updated {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
|
||||||
|
let converted = u32::try_from(value);
|
||||||
|
return match converted {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime count exceeds DTO bounds")
|
||||||
|
.with_context("field", field)
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock_presentation(
|
||||||
|
presentation: &std::sync::Mutex<MarketPricePresentationState>,
|
||||||
|
) -> ksp_core_lib::Result<std::sync::MutexGuard<'_, MarketPricePresentationState>> {
|
||||||
|
return match presentation.lock() {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||||
|
"SOL Prices Desk market-price presentation state lock is poisoned",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_provider_ids(provider_ids: std::vec::Vec<String>) -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderId>> {
|
||||||
|
if provider_ids.is_empty() || provider_ids.len() > MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID,
|
||||||
|
"SOL Prices Desk market-price refresh selection has an invalid provider count",
|
||||||
|
)
|
||||||
|
.with_context("provider_count", provider_ids.len().to_string())
|
||||||
|
.with_context("max_provider_count", MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut parsed = std::vec::Vec::with_capacity(provider_ids.len());
|
||||||
|
for provider_id in provider_ids {
|
||||||
|
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id);
|
||||||
|
let provider_id = match provider_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
parsed.push(provider_id);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_registry_entry(
|
||||||
|
entry: &ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry,
|
||||||
|
presentation: &MarketPricePresentationEntry,
|
||||||
|
) -> crate::MarketPriceProviderRowDto {
|
||||||
|
let descriptor = entry.descriptor();
|
||||||
|
let availability = entry.state().availability();
|
||||||
|
let observation = presentation.observation.as_ref();
|
||||||
|
return crate::MarketPriceProviderRowDto {
|
||||||
|
auth_mode: auth_mode_code(descriptor.auth_mode()).to_owned(),
|
||||||
|
availability: availability_code(availability).to_owned(),
|
||||||
|
display_name: descriptor.display_name().to_owned(),
|
||||||
|
loading: presentation.loading,
|
||||||
|
pair: ksp_offchain_transport_lib::MarketPricePair::SolUsd.code().to_owned(),
|
||||||
|
price: observation.map(|value| return value.price().to_canonical_string()),
|
||||||
|
provider_id: descriptor.id().as_str().to_owned(),
|
||||||
|
provider_timestamp_unix_millis: observation.and_then(|value| return value.provider_timestamp()).map(|value| return value.unix_millis().to_string()),
|
||||||
|
received_at_unix_millis: observation.map(|value| return value.received_at().unix_millis().to_string()),
|
||||||
|
refreshed: observation.is_some(),
|
||||||
|
retry_at_unix_millis: availability.retry_at().map(|value| return value.unix_millis().to_string()),
|
||||||
|
semantics: semantics_code(descriptor.semantics()).to_owned(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn semantics_code(semantics: ksp_offchain_transport_lib::MarketPriceSemantics) -> &'static str {
|
||||||
|
return match semantics {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket => "aggregated_market",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd => "dex_pair_usd",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade => "exchange_last_trade",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic => "solana_heuristic",
|
||||||
|
ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot => "solana_spot",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading_in_state(presentation: &mut MarketPricePresentationState, provider_id: &str, loading: bool) -> ksp_core_lib::Result<()> {
|
||||||
|
let entry = presentation.entries.get_mut(provider_id);
|
||||||
|
return match entry {
|
||||||
|
std::option::Option::Some(value) => {
|
||||||
|
value.loading = loading;
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price presentation state is missing provider")
|
||||||
|
.with_context("provider_id", provider_id),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/market_price_runtime.rs"]
|
||||||
|
mod tests;
|
||||||
59
crates/ksp-app-solprices-desk/src/offchain_runtime.rs
Normal file
59
crates/ksp-app-solprices-desk/src/offchain_runtime.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// file: crates/ksp-app-solprices-desk/src/offchain_runtime.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Config-owned Off-chain Transport bootstrap for SOL Prices Desk.
|
||||||
|
|
||||||
|
/// Resolved SOL Prices Desk composite and Off-chain Transport configuration retained by application state.
|
||||||
|
pub(crate) struct OffchainTransportStartup {
|
||||||
|
composite_profile_id: String,
|
||||||
|
resolved: ksp_config_lib::ResolvedOffchainTransportConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::OffchainTransportStartup {
|
||||||
|
/// Returns the composite profile selected for this application launch.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn composite_profile_id(&self) -> &str {
|
||||||
|
return self.composite_profile_id.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Config-resolved Off-chain Transport runtime configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn resolved(&self) -> &ksp_config_lib::ResolvedOffchainTransportConfig {
|
||||||
|
return &self.resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the composite-selected Off-chain Transport profile and constructs the provider-neutral service owned by Config.
|
||||||
|
pub(crate) fn initialize_offchain_transport(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<crate::OffchainTransportStartup> {
|
||||||
|
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_solprices_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 offchain_profile = crate::required_composite_component_profile(
|
||||||
|
&composite,
|
||||||
|
crate::COMPOSITE_COMPONENT_ID_OFFCHAIN_TRANSPORT,
|
||||||
|
ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT,
|
||||||
|
);
|
||||||
|
let offchain_profile = match offchain_profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let resolved = management.engine().resolve_offchain_transport_config_profile(&offchain_profile, &environment);
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let provider_count = resolved.service().registry().len();
|
||||||
|
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, composite_profile = composite.profile_id(), offchain_profile = resolved.profile_id(), provider_count, "initialized SOL Prices Desk Off-chain Transport from composite-managed configuration");
|
||||||
|
return std::result::Result::Ok(crate::OffchainTransportStartup { composite_profile_id: composite.profile_id().to_owned(), resolved });
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user