Compare commits
581 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 002d08ba5f | |||
| a7ac30f39b | |||
| cd7cf2c4c6 | |||
| 8d8b6b9bcb | |||
| 02012f105a | |||
| 09c91172ae | |||
| 8a0717c839 | |||
| f3c4169752 | |||
| eedad4b81e | |||
| 4e39ddd5d0 | |||
| c99cb048bf | |||
| 8c4b458724 | |||
| 792560191c | |||
| c884ce88ba | |||
| 4bcd942928 | |||
| bf394e0e4b | |||
| ec94001ab8 | |||
| 505cc8c3f6 | |||
| 9fc30d3ab9 | |||
| 887664f3c0 | |||
| ae712eae64 | |||
| a3df8f6ef5 | |||
| 1006079654 | |||
| 6721a4142a | |||
| 7bf938c19f | |||
| 5932250314 | |||
| b2068e9ef2 | |||
| 2ca56dbaf4 | |||
| 9785cda39d | |||
| 0cea99cd5e | |||
| 71bc754feb | |||
| cabbfed730 | |||
| 8b6ef7778a | |||
| 06319655b0 | |||
| f636169783 | |||
| 0f8f38ff52 | |||
| 142998afb3 | |||
| 665d7d2bb6 | |||
| 1746d47412 | |||
| 9bb2f7bcae | |||
| 1f2720463b | |||
| 8bb530e9b4 | |||
| 72df965a9c | |||
| f688325170 | |||
| c711213dcc | |||
| 11ecdccd65 | |||
| 27ab84d198 | |||
| 28e4da3879 | |||
| 31c0a83e51 | |||
| 34fddc3bf5 | |||
| 13c3dac554 | |||
| f7ed053e8e | |||
| 2e8feb56dd | |||
| 793178b345 | |||
| ee0359efd5 | |||
| 1dc57a85c2 | |||
| e7e8960fec | |||
| 79e278064d | |||
| 474b894d1c | |||
| 0c76b74afe | |||
| b608ed2c50 | |||
| f9d4e6bc74 | |||
| 223f11b989 | |||
| 5095be0b0d | |||
| 2aa64cf0a4 | |||
| 90c266d68a | |||
| 8410b2f4ef | |||
| ba69a5ad20 | |||
| 07b9b20eb5 | |||
| 901a0515b7 | |||
| 50c01a19c9 | |||
| 3a8fa87acb | |||
| df1dbbb8fb | |||
| 531ca851c4 | |||
| b3fd74529d | |||
| 00b376d234 | |||
| fa720d0054 | |||
| 91438e8214 | |||
| 08e9a34183 | |||
| 85937b8cbb | |||
| 107986bf85 | |||
| a8cabbebd5 | |||
| 636a0c5f43 | |||
| bf46782db3 | |||
| 02919e959e | |||
| d231eba8be | |||
| 637382f364 | |||
| bf6ef5b426 | |||
| 822e8fcd86 | |||
| 63e1a866a3 | |||
| 22c88c449d | |||
| ed6c0ac10c | |||
| 355c5d1e9a | |||
| 61d8b1dee2 | |||
| 0db32a865c | |||
| a4061ee1e1 | |||
| ec64be340e | |||
| f1c383c392 | |||
| 984c327162 | |||
| e2689d1b04 | |||
| b5c84f3df8 | |||
| 36c683df9e | |||
| 510adcb49b | |||
| ab87fd23cd | |||
| 1a352aa8d0 | |||
| 919b4ed46d | |||
| f7c57f21c5 | |||
| d4f4237723 | |||
| ba574f3052 | |||
| 0683b70abe | |||
| eaf6fae94c | |||
| e19a202565 | |||
| 8309d829cc | |||
| aabe8dedb2 | |||
| d0da907c00 | |||
| 6b434750c5 | |||
| c9047af626 | |||
| 52d692e8ec | |||
| 5c3c8b3fef | |||
| b0079fc3ee | |||
| ecfa123a79 | |||
| 9da372e4a2 | |||
| 9a998e7b52 | |||
| c3c9a80310 | |||
| 116eabba7d | |||
| e6c2649410 | |||
| e85d6f6963 | |||
| eb39aaa904 | |||
| bb7ee3cc3a | |||
| 84647c12ea | |||
| 156d237460 | |||
| 7d0710a1e6 | |||
| 5c797827f7 | |||
| b87b7f4505 | |||
| 92f08ca5a8 | |||
| 85f292a36d | |||
| 877411c6e5 | |||
| 081a5352c0 | |||
| c0b3190256 | |||
| eaefec2259 | |||
| cfbf849e2a | |||
| e01fe88594 | |||
| 80fe2fd9e2 | |||
| 516c52ece9 | |||
| 2b79818848 | |||
| e2e603ca6d | |||
| 01502678bf | |||
| 32c4b67541 | |||
| 8c23625fee | |||
| 489d686023 | |||
| 10515d1562 | |||
| c9e52d1926 | |||
| 32577ea48d | |||
| d5b4aef194 | |||
| 8130e01b70 | |||
| 3a38914be7 | |||
| 62d6cf9dac | |||
| 4e2647cce1 | |||
| 9b92900dd7 | |||
| bf73481180 | |||
| 3bb73e81e4 | |||
| c4198ac656 | |||
| 009b957523 | |||
| 9300dccbe2 | |||
| 74890a6268 | |||
| 6a1892792c | |||
| 3df8fc74c1 | |||
| 68678a22c6 | |||
| 4506c2d12d | |||
| d8f7c9bd8b | |||
| a266a4d7b6 | |||
| 76f671b8c5 | |||
| 0847edda4c | |||
| fa04bdbcf6 | |||
| 7697f407db | |||
| 4c16607e22 | |||
| 618f940310 | |||
| 313d2a1ea6 | |||
| 97f84e5c58 | |||
| db78fea161 | |||
| ec8875e864 | |||
| ed72aaba67 | |||
| d49d7af0a4 | |||
| d7db8d69e0 | |||
| 48d26b43f9 | |||
| 6e406a6b84 | |||
| 9c4469248a | |||
| 6fc5b5b607 | |||
| 3c4ae51ae2 | |||
| 57ff11b774 | |||
| 500b87ef57 | |||
| 46a4ad7487 | |||
| 03a94f4fb6 | |||
| 862f40ad26 | |||
| bc7ccd97a5 | |||
| dd85ac5e77 | |||
| dd48ed9140 | |||
| 5a65c28898 | |||
| 42374115c7 | |||
| 9506df9487 | |||
| 7f8ce987a5 | |||
| 8dbd86c297 | |||
| 75b2d7e7f1 | |||
| b861a1e3b8 | |||
| d89aee9910 | |||
| 75cbf9df09 | |||
| 138395ac35 | |||
| be0032e76b | |||
| 8096e6f07b | |||
| 032919fa07 | |||
| ea5f756d17 | |||
| 4fdeab4d67 | |||
| 478548b7f4 | |||
| 20cf700f22 | |||
| f92c22d55a | |||
| cb670952e8 | |||
| a25d8dd55a | |||
| ecfc30a94b | |||
| 6f2d0a092a | |||
| 3de0488cab | |||
| 2460162243 | |||
| 1b9104d64b | |||
| 80be36bfdd | |||
| e8d2382ac3 | |||
| 749e53edc2 | |||
| 7377a6f6cd | |||
| 45601ce386 | |||
| 7d490eb5d8 | |||
| ca018095bf | |||
| 0dd722ffca | |||
| 75e8030b07 | |||
| c24e4c48f8 | |||
| c7cfb9557d | |||
| c27428d6c0 | |||
| dfa01b1381 | |||
| 5fc7f65b2c | |||
| bbf804b7fc | |||
| 13687ec2fe | |||
| b756da5149 | |||
| 04234926ce | |||
| bfc69f5631 | |||
| 902b5fba99 | |||
| fccb7d876c | |||
| 50d4142797 | |||
| eb6dbc31e8 | |||
| fb8b585aa2 | |||
| dad18c16d5 | |||
| a704a5722e | |||
| dfa3723a7f | |||
| 6bd593f467 | |||
| 03ad0ba063 | |||
| d95b1095e5 | |||
| bcce89f076 | |||
| 66d5b03495 | |||
| 7af751c888 | |||
| 9f2d5ea704 | |||
| 8428b75b96 | |||
| 282b616a0e | |||
| 12eaf7a3de | |||
| f28e4d87f0 | |||
| ba2e52f841 | |||
| 5f1938c7c3 | |||
| 5e2f959c01 | |||
| 8da04717b6 | |||
| c094828704 | |||
| ce658e58f7 | |||
| 7f0a642972 | |||
| 84ab2b3651 | |||
| 1f0b202135 | |||
| aa56a12846 | |||
| 10996f11f7 | |||
| 1352a61d70 | |||
| 66a3926adb | |||
| 917e602a87 | |||
| 56fadb364a | |||
| 8fa1c8ce8d | |||
| c17e78c6a8 | |||
| 61bf7ba468 | |||
| 9712c7e1f7 | |||
| a560df80ce | |||
| bb56574824 | |||
| 8363c8bdc9 | |||
| b375f263dc | |||
| e1fe419028 | |||
| 9ec4f26874 | |||
| 80eea82398 | |||
| e39cf656b2 | |||
| c4f56d9e85 | |||
| c1dbaad88d | |||
| d93184d41d | |||
| de3cec6a23 | |||
| 0a4cddafc4 | |||
| 6d2b1401aa | |||
| 0a8fb5dd1b | |||
| a8c90107b5 | |||
| ced653bfc0 | |||
| 153014be9d | |||
| 8d4b3b67fb | |||
| 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 |
77
.env.example
77
.env.example
@@ -1,24 +1,89 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 3
|
# version: 14
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
# PostgreSQL URI for the Devnet Store target.
|
||||||
|
KSP_SECRET_STORE_DEVNET_POSTGRES_URI=postgresql://...
|
||||||
|
|
||||||
|
# PostgreSQL URI for the Mainnet Store target.
|
||||||
|
KSP_SECRET_STORE_MAINNET_POSTGRES_URI=postgresql://...
|
||||||
|
|
||||||
|
# PostgreSQL URI for the Testnet Store target.
|
||||||
|
KSP_SECRET_STORE_TESTNET_POSTGRES_URI=postgresql://...
|
||||||
|
|
||||||
|
# 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.
|
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
|
||||||
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
|
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
|
||||||
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
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.
|
# 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.
|
# The committed Transport document falls back to https://api.mainnet.solana.com when this variable is absent.
|
||||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet.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.solana.com when this variable is absent.
|
||||||
|
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet.solana.com
|
||||||
|
|
||||||
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
# 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.
|
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||||
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
||||||
|
|
||||||
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
|
# 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
|
||||||
|
|||||||
272
CHANGELOG.md
272
CHANGELOG.md
@@ -1,10 +1,280 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 34 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
|
## 0.3.14 — gap repair et hardening multi-source du Worker RawTransaction — 2026-09-12
|
||||||
|
|
||||||
|
`0.3.14` ferme `ksp-worker-raw-transaction-ingest-lib` par une continuité run-local explicite au-dessus des cinq familles live de `0.3.13`, sans transformer le Worker en moteur historique. Le Worker distingue désormais reconnect, tentative de replay, delivery de replay et preuve de coverage ; il maintient des gaps bornés avec plage inclusive, `TargetCoverage` conservative et frontier de continuité distincte de la processing frontier. Une reprise live ou l'acceptation d'un `from_slot` ne vaut jamais à elle seule preuve de réparation.
|
||||||
|
|
||||||
|
La réconciliation peut s'appuyer sur une coverage réellement prouvée d'une source redondante, un scan HTTP borné de blocs/slots ou l'hydration `getTransaction observed` des références connues. Les bornes finales restent 64 gaps ouverts, 4096 slots par plage, fenêtres de discovery de 512 slots, 4 `getBlock` logiques en vol et un seul gap activement réparé. Les matériaux de repair repassent par la même canonicalisation Common RAW, la même admission, la même hydration globale et le même Store ; aucun second pipeline, retry réseau Worker, Job Backfill automatique ou backend Store direct n'est introduit.
|
||||||
|
|
||||||
|
La politique multi-source ne fault plus mécaniquement sur toute perte de source : la continuation n'est autorisée que lorsque les gaps concernés sont réconciliés et que la coverage présente/future requise est réellement démontrée. Les snapshots publics ajoutent une observabilité source-neutral et bornée des gaps avec états/raisons/méthodes et compteurs checked, sans source key, endpoint, token, signature, payload ou erreur provider arbitraire. La health policy reste conservative, la fairness alterne trafic nominal/repair sur les capacités existantes et le shutdown borne également le drain interne des sources avec abort+join des siblings non coopératifs.
|
||||||
|
|
||||||
|
La fermeture cross-layer confirme le pipeline unique `Transport -> Worker -> ksp-raw-transaction-lib -> Store`, la non-régression Legacy/V0/V1, les dependency firewalls et l'indépendance Worker/Backfill. Le gate technique final passe fmt, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features` et les graphes Cargo : `1 931` tests passent, `0` échoue et `15` restent ignored opt-in/operator-only. Les smokes keyless Solana HTTP Devnet et WebSocket Devnet passent `1/1` chacun ; Yellowstone/provider-gated, Helius live, PostgreSQL live et Worker->Store end-to-end restent explicitement non exécutés lorsqu'aucune ressource correspondante n'a été provisionnée.
|
||||||
|
|
||||||
|
`prompts/034-V0_3_15_START_PROMPT.md` ouvre `0.3.15` exclusivement depuis le futur tag stable `v0.3.14`. Cette release introduit `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri de composition/supervision : une **route** est une stratégie complète pouvant nécessiter plusieurs capacités Config/Transport, elle n'est sélectionnable que si toutes ses requirements sont composables, puis une instance Worker réelle est démarrée par route sélectionnée et valide l'opérabilité runtime au Start. Le frontend reste une surface sûre ; Config, Transport, Store et Worker conservent leurs responsabilités.
|
||||||
|
|
||||||
|
## 0.3.13 — convergence live multi-source du Worker RawTransaction — 2026-09-10
|
||||||
|
|
||||||
|
`0.3.13` généralise `ksp-worker-raw-transaction-ingest-lib` de la verticale Yellowstone de `0.3.12` vers une composition caller-owned de `1..32` sources logiques appartenant au même réseau. Cinq familles live convergent désormais vers le même pipeline Common RAW / admission / Store : Yellowstone, Standard Logs avec hydration `getTransaction observed`, Standard Block RAW-direct, Helius `transactionSubscribe` avec hydration et HTTP Block Polling run-local RAW-direct. Le Worker reste sans Config, Job Backfill, backend Store physique, client réseau direct ou SDK provider parallèle.
|
||||||
|
|
||||||
|
La convergence partage un registre global d'hydration entre Yellowstone, Standard Logs et Helius afin de coalescer `(network, signature, commitment)` avant fan-out HTTP. Après canonicalisation, les acquisitions de même `(network, signature)` sont sérialisées de manière bornée : un contenu identique conserve les observations/provenances distinctes, tandis qu'une divergence devient un `content_conflict` terminal explicite. Aucune majorité provider, préférence silencieuse ou stratégie `first-provider-wins` n'est introduite. Standard Block et HTTP Block Polling admettent directement les transactions Legacy/V0/V1 qualifiées avec `maxSupportedTransactionVersion = 1`.
|
||||||
|
|
||||||
|
Le supervisor multi-source possède toutes les tâches et applique des bornes source-neutral sur admission, pending signals, hydrations et persistence avec une fairness minimale entre sources reference-bearing. Les snapshots agrègent activity/health/backpressure/reconnect/replay/gap sans exposer le matériau provider. La politique reste conservative : une source configurée qui échoue est terminale faute d'équivalence de coverage prouvée ; le gap repair, les rôles degraded/failover et la preuve de couverture restent donc `0.3.14`. Le shutdown stop/fault/drain/abort+join est durci contre les races, leaders d'hydration abandonnés, Store lent et publications tardives.
|
||||||
|
|
||||||
|
Le gate déterministe final passe `cargo fmt`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features` et les graphes Cargo : `1 850` tests passent, `0` échoue et `15` restent ignored opt-in/operator-only. Les smokes keyless Solana HTTP Devnet et WebSocket Devnet passent `1/1` chacun. Yellowstone provider-gated, Helius `transactionSubscribe` live, `blockSubscribe` provider dédié, HTTP polling Worker end-to-end et Worker multi-source vers Store restent explicitement non exécutés faute de gate provisionné ; aucune fixture ne les requalifie en PASS.
|
||||||
|
|
||||||
|
`prompts/033-V0_3_14_START_PROMPT.md` ouvre `0.3.14` exclusivement depuis le futur tag stable `v0.3.13`. Cette release doit fermer le gap repair/hardening multi-source limité au run actif — replay adressable, coverage redondante, scan HTTP borné, hydration de réparation, unresolved gaps et health policy — sans transformer le Worker en moteur historique ni créer de dépendance vers `ksp-job-backfill-lib`.
|
||||||
|
|
||||||
|
## 0.3.12 — Yellowstone + hydration HTTP + continuité de run du Worker RawTransaction — 2026-09-09
|
||||||
|
|
||||||
|
`0.3.12` ouvre la première source réseau productive de `ksp-worker-raw-transaction-ingest-lib` sans remettre en cause la fondation source-neutral de `0.3.11`. Le Worker dépend désormais de la façade `ksp-onchain-transport-lib` mais reste sans Config, Job Backfill, backend Store physique, `reqwest`, `tonic` ou proto provider direct. `RawTransactionIngestRuntimeResources` reçoit une source Yellowstone caller-composed et un pool HTTP cohérent ; `start` conserve la fondation sans source, tandis que `start_with_runtime_resources` lance la verticale live supervisée.
|
||||||
|
|
||||||
|
Les updates Yellowstone `Transaction`, `TransactionStatus` et les transactions incluses dans `Block` sont projetées vers des signaux source-neutral, coalescées de manière bornée par `(network, signature, commitment)`, hydratées via `getTransaction` observed puis converties par `ksp-raw-transaction-lib` avant admission/persistence Store. `BlockMeta` et `Slot` restent continuity-only. La provenance composite conserve les identités logiques sûres de la source et de la route d’hydration, sans URL, secret ni payload distant. Les gardes réseau/signature/slot/index/commitment empêchent les croisements de matériau avant admission.
|
||||||
|
|
||||||
|
La release matérialise une processing frontier strictement run-local : le Worker distingue travail pending/settled, n’avance jamais au travers d’un pending plus ancien et compacte ses états sans revendiquer une preuve de complétude durable. Le reconnect/replay reste Transport-owned : `from_slot` et `SubscribeReplayInfo` ne sont pas pilotés par le Worker. Le Worker projette un état source-neutral `Active/Reconnecting/Closing/Closed/Failed`, compte reconnect/replay-attempt/continuity-gap et fault avec `source_failed` lorsqu’une hausse du gap Transport prouve que la rétention ne permet plus la reprise demandée. Aucun Backfill ou repair automatique n’est déclenché.
|
||||||
|
|
||||||
|
Le hardening final ferme les duplicate storms, saturation/coalescence, Store lent/fautif, stop/fault pendant hydration/reconnect, overflow de compteurs, abandon des pending sans faux `settled`, abort/join des tâches privées et absence de retry HTTP possédé par le Worker. `pre.010` ajoute les canaris cross-layer Transport -> Worker -> Common RAW -> Store et verrouille les dependency firewalls, fixtures Legacy/V0, redaction et inventaires publics.
|
||||||
|
|
||||||
|
Le gate déterministe `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, `cargo test --workspace --all-targets --all-features`, les suites ciblées et les trois inspections Cargo demandées. Les smokes live keyless HTTP Devnet et WebSocket Devnet sont exécutés séparément et passent ; les smokes Yellowstone token-gated et le Worker end-to-end Yellowstone + HTTP + Store restent explicitement non exécutés faute de ressources fournies, sans faux PASS. `pre.012` réconcilie ensuite README/USAGE et les architectures concernées avec cette verticale réellement matérialisée.
|
||||||
|
|
||||||
|
La suite reste découpée : `0.3.13` ajoute WS standard, Helius `transactionSubscribe`, HTTP live polling et la convergence multi-source ; `0.3.14` ferme le gap repair/hardening multi-source. `prompts/032-V0_3_13_START_PROMPT.md` ouvre `0.3.13` uniquement depuis le tag stable `v0.3.12` et impose un `pre.001` d’audit/sizing avant toute extension productive.
|
||||||
|
|
||||||
|
## 0.3.11 — Fondation runtime source-neutral du Worker RawTransaction ingest — 2026-09-08
|
||||||
|
|
||||||
|
`0.3.11` introduit `ksp-worker-raw-transaction-ingest-lib` comme premier Worker concret KSP pour l’acquisition continue de `RawTransaction`, tout en fermant volontairement la release **sans source réseau productive**. La crate consomme `ksp-worker-api`, `ksp-raw-transaction-lib` et la seule façade `ksp-store-lib`; elle ne dépend ni de Config, ni d’un Job, ni d’un backend Store physique, ni encore de `ksp-onchain-transport-lib`. Son identité stable est `raw_transaction_ingest`, ses settings bornent la capacité d’admission, la concurrence de persistence et la deadline de drain, et son `start` utilise exclusivement le runtime Tokio actif fourni par le caller.
|
||||||
|
|
||||||
|
Le runtime possède un supervisor privé, des tâches source/persistence détenues, une admission centrale `mpsc` bornée avec backpressure, la canonicalisation/assembly Common RAW, une observation key déterministe et la persistence atomique en mode `Normal`. Les outcomes distinguent insert, idempotence et tombstone purgée ; un contenu divergent devient `worker_raw_transaction_ingest.content_conflict`, les erreurs Store restent classifiées sans texte distant, et aucun provider gagnant n’est choisi implicitement. Les snapshots concrets latest-value se projettent directement sur `WorkerSnapshotSource`, utilisent séquences/compteurs checked et conservent le terminal pour les lecteurs tardifs.
|
||||||
|
|
||||||
|
Le hardening final ferme les races stop/fault, les source failures, la saturation observable, le Store lent/fautif et le shutdown borné. À l’expiration de `shutdown_drain_timeout`, le Worker ferme les admissions, abort les tâches restantes, les rejoint toutes puis publie `drain_timeout`; aucune tâche privée ne survit au terminal. Les tests externes de hardening/release verrouillent les sept codes d’erreur publics, les huit modules de production, les vingt-quatre exports crate-root, la redaction, le dependency firewall et l’absence de surface historique/Backfill/Transport/Config/backend direct.
|
||||||
|
|
||||||
|
Le gate technique final `pre.011` passe rustfmt check, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, 56 tests Worker, `cargo test --workspace --all-targets --all-features`, les arbres Cargo Worker normal/features et `cargo tree --duplicates`. La réconciliation `pre.012` ajoute ensuite les README/USAGE durables du Worker et réaligne l’architecture sur l’état matériel : la fondation `0.3.11` n’a aucun adapter HTTP/WS/Yellowstone productif, aucune API publique d’enqueue/source registration et aucun replay/gap-repair live.
|
||||||
|
|
||||||
|
La trajectoire live reste découpée : `0.3.12` ouvre Yellowstone transactions/blocks/status + hydration HTTP + continuité de run, `0.3.13` ajoute WS standard/Helius/HTTP live et la convergence multi-source, puis `0.3.14` ferme le gap repair/hardening multi-source. `prompts/031-V0_3_12_START_PROMPT.md` ouvre `0.3.12` uniquement depuis le tag stable `v0.3.11`; son `pre.001` doit réauditer les surfaces Yellowstone/Solana/provider actuelles, l’injection Transport, l’hydration, le frontier, `from_slot`/ReplayInfo, les smokes accessibles et le sizing avant toute implémentation lourde.
|
||||||
|
|
||||||
|
## 0.3.10 — Lower-layer RAW Transaction commune + qualification cross-source — 2026-09-08
|
||||||
|
|
||||||
|
`0.3.10` introduit `ksp-raw-transaction-lib` comme lower-layer source-neutral commune aux producteurs `RawTransaction`. La canonicalisation RAW v1 auparavant locale à `ksp-job-backfill-lib` est extraite sans modifier les golden bytes/hash existants ; le Backfill consomme désormais cette common crate sans créer de dépendance vers un Worker. La bibliothèque construit les modèles `RawTransaction`/`RawTransactionObservation` de `ksp-store-api`, conserve l'identité canonique `(network, signature)`, borne et redacted les entrées hostiles, et reste volontairement sans Transport, Config, runtime async, Job, Worker ou backend Store physique.
|
||||||
|
|
||||||
|
La common matérialise également un wire Solana source-neutral Legacy/V0/V1 avec sérialisation Base64 et extraction de signature depuis le matériau transactionnel complet. La représentation V1 couvre header, compiled instructions, configuration V1 et signatures terminales ; les gardes structurelles rejettent les états non supportés plutôt que d'inventer une compatibilité. `ksp-onchain-transport-lib` ajoute `get_block_observed`, qui conserve l'identité sûre de la route réellement gagnante tout en réutilisant la validation `getBlock` existante.
|
||||||
|
|
||||||
|
Les preuves cross-source ferment explicitement ce qui est qualifié et ce qui exige encore hydration. Un `blockSubscribe` standard `full/base64` peut produire le même RAW v1 qu'un `getBlock` HTTP sur le sous-ensemble déterministe prouvé. Helius `transactionSubscribe` reste un signal nécessitant hydration HTTP lorsque `blockTime`/version manquent. Yellowstone qualifie le wire Transaction V1 et la conservation du `block_time` dans Block, mais `Transaction` nécessite hydration pour le temps de bloc et le chemin Block complet reste conservateur tant que l'identité byte-for-byte entre meta protobuf et meta JSON HTTP n'est pas prouvée. Aucun RAW-direct Yellowstone complet ni feed EARLY n'est revendiqué sans preuve.
|
||||||
|
|
||||||
|
Le gate technique final `pre.007` passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, les suites ciblées common/Transport/Backfill et les graphes Cargo demandés ; les smokes live opt-in restent ignorés conformément à leur contrat. La réconciliation `pre.008` aligne ensuite la terminologie durable des couches Store sur `D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN`, réserve `N1–N4` aux niveaux architecturaux et complète `README.md`/`USAGE.md` de `ksp-raw-transaction-lib`. Son gate opérateur repasse format, audits, `cargo check --workspace` et Clippy strict sur `0.3.10-pre.8`.
|
||||||
|
|
||||||
|
Le runtime concret a été volontairement retiré de `0.3.10` après recalibrage du sizing. La trajectoire devient `0.3.11` fondation runtime/persistence déterministe, `0.3.12` Yellowstone + hydration/continuité, `0.3.13` WS standard/Helius/HTTP live + convergence multi-source, `0.3.14` gap repair/hardening/smokes, `0.3.15` Desk d'ingestion et `0.3.16` extension Backfill multi-source. Le Job historique paramétré et le Worker continu restent des producteurs indépendants du même Store.
|
||||||
|
|
||||||
|
`prompts/030-V0_3_11_START_PROMPT.md` ouvre `0.3.11` uniquement depuis le tag stable `v0.3.10`. Son `pre.001` doit auditer la base réelle, le graphe de dépendances, le runtime/supervision, les bornes de channels, la persistence/déduplication déterministe, les snapshots et le dimensionnement avant toute implémentation lourde ; aucune source live complexe n'est requise pour fermer cette release.
|
||||||
|
|
||||||
|
## 0.3.9 — Worker API générique + audit exhaustif d'acquisition RawTransaction — 2026-09-05
|
||||||
|
|
||||||
|
`0.3.9` introduit `ksp-worker-api` comme façade générique, courte et runtime-neutral pour services continus. La surface stable reste Core-only et expose `WorkerId`, `WorkerKindCode`, `WorkerState`, `WorkerHealth`, `WorkerActivity`, `WorkerLifecycle`, `WorkerStopToken`, `WorkerSnapshotSequence`, `WorkerSnapshot`, `WorkerSnapshotFuture` et `WorkerSnapshotSource`. Les identités sont bornées et redacted, le lifecycle possède des transitions explicites avec terminaux immuables, le stop token est partagé/idempotent, la séquence ne wrappe pas et le snapshot source latest-value reste object-safe, `Send + Sync` et implémentable depuis l'extérieur. L'API ne possède aucun runtime `start/stop` universel, aucune sémantique Job/checkpoint, aucun domaine Solana et aucune dépendance Transport/Store/Config/Tauri.
|
||||||
|
|
||||||
|
Après freeze de cette API, la seconde moitié de la release réalise puis consolide l'audit exhaustif des voies d'acquisition `RawTransaction`. Le document durable `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` est réécrit comme une synthèse unique Store-centrique : `RawTransaction` est le cœur RAW durable, tandis que `RawTransactionObservation` conserve les provenances multiples. L'audit distingue systématiquement possibilité protocolaire/provider, support architectural KSP, support implémenté et preuve live ; il couvre HTTP, WebSocket standard, Helius `transactionSubscribe`, Yellowstone gRPC, acquisition par blocs/slots, discovery + hydration, replay/gap repair, archives et sources EARLY/pre-execution. Les matrices recensent aussi les principaux providers/réseaux/tier/coûts connus sans réduire l'architecture aux seuls comptes gratuits actuellement disponibles.
|
||||||
|
|
||||||
|
La séparation des producteurs RAW est figée. `ksp-job-backfill-lib` reste un producteur historique **paramétré, borné et terminable** ; le futur `ksp-worker-raw-transaction-ingest-lib` sera un producteur live **continu start/stop sans requête métier historique**. Ils ne s'appellent pas, ne se supervisent pas et ne collaborent pas : chacun alimente indépendamment le même Store. HTTP, WS, gRPC, replay et archive sont des capabilities orthogonales à ces rôles ; un Worker peut utiliser HTTP pour hydration ou réparation de sa propre continuité live, tandis qu'un Job pourra utiliser gRPC/replay si une campagne historique bornée le justifie. Le handoff `0.3.10` retient `ksp-raw-transaction-lib` comme lower-layer source-neutral commune afin d'extraire la canonicalisation RAW v1 aujourd'hui locale au Backfill sans duplication ni edge Job ↔ Worker ; les golden bytes/hash RAW v1 doivent rester inchangés.
|
||||||
|
|
||||||
|
La release normalise aussi l'identité réseau KSP : `mainnet` devient l'identité canonique dans Config/Store/Transport/tests et `mainnet-beta` reste seulement un alias legacy/externe lorsque la frontière provider l'exige. Les données Mainnet N1 encore utilisées comme données de test ne dictent aucune compatibilité durable et aucune migration SQL n'est introduite pour préserver l'ancien libellé. Pendant le gate final, les baselines workspace sont également relevées à `jsonschema ^0.53` et `yellowstone-grpc-proto ^12.7`; Yellowstone 12.7 ajoute le RPC serveur `SubscribeGossip`, ce qui nécessite uniquement l'adaptation des fixtures `Geyser` avec une réponse `UNIMPLEMENTED` et n'ouvre aucune capability Gossip de production.
|
||||||
|
|
||||||
|
Le gate technique final passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, 385/385 tests unitaires `ksp-onchain-transport-lib`, 43/43 canaries `release_completeness`, `cargo test --workspace --all-targets --all-features`, le bundle complet `ksp-worker-api` et les graphes Cargo jusqu'à `cargo tree --duplicates`; seuls les smokes/probes explicitement opt-in restent ignorés. La réconciliation documentaire `pre.008` réaligne ensuite README/USAGE/indexes/architecture/plan/validation et son gate opérateur repasse audits Rust/Markdown plus `cargo check --workspace` en `0.3.9-pre.8`.
|
||||||
|
|
||||||
|
`prompts/029-V0_3_10_START_PROMPT.md` ouvre `0.3.10` exclusivement depuis le tag stable `v0.3.9`. La prochaine release doit d'abord matérialiser `ksp-raw-transaction-lib`, migrer le Backfill vers cette canonicalisation commune sans changement fonctionnel, puis construire `ksp-worker-raw-transaction-ingest-lib` multi-source dès V1. Son `pre.001` est obligatoirement un gate d'audit/sizing : inventaire exact des gaps Transport/Config, runtime Worker, capabilities live, continuité, preuves provider et dépendances avant toute implémentation lourde.
|
||||||
|
|
||||||
|
## 0.3.8 — Store Desk V1 RAW, inspection backend-neutral et trajectoire Worker multi-source — 2026-09-04
|
||||||
|
|
||||||
|
`0.3.8` introduit `ksp-app-store-desk`, application Tauri KSP read-only dédiée à l'inspection du Store. Le package reprend le gabarit Desk commun (splash/shell/assets/styles/tracing), compose `ksp-config-lib`, `ksp-logging-lib` et la seule façade `ksp-store-lib`, et interdit à l'application tout accès direct à `ksp-store-api`, `ksp-store-postgres-lib`, SQL, driver/pool PostgreSQL, Transport on-chain ou ressource physique. Le bootstrap ouvre un Store lié au profil/réseau composite, expose health/runtime backend-neutral et effectue un shutdown borné à la fermeture de la fenêtre principale.
|
||||||
|
|
||||||
|
Pour éviter de détourner la pagination machine existante, Store API ajoute une primitive d'inspection backend-neutral distincte : `RawInspectionPageRequest`/`RawInspectionPage<T>`, summaries `RawTransaction`/`RawAccountState` sans gros bytes et quatre capabilities d'inspection couvrant entités et observations. Le backend PostgreSQL implémente les vues d'inspection par `COUNT` exact + `OFFSET/LIMIT` contrôlé, tandis que les queries historiques `RawPage` restent keyset/cursor, sans `OFFSET`, pour workers, backfills et replays. La façade `ksp-store-lib` conserve ainsi le même ensemble de 14 capabilities RAW que le backend sans exposer de détail physique.
|
||||||
|
|
||||||
|
La Desk projette `RawTransaction` et `RawAccountState` via DataTables `serverSide` avec **un seul pager visible**, filtres Store explicites, tailles de page bornées et counts vérifiés avant conversion JavaScript. Les détails sont chargés à la demande avec preview RAW bornée à 512 octets ; les rows ne transportent pas le payload transactionnel ni les bytes account. Les observations Transaction/Account sont elles aussi paginées côté serveur dans les détails et n'exposent que la provenance sûre déjà normalisée par Store API : provider/protocol/method/origin, timestamps, endpoint/session/filter/commitment optionnels, hash/taille de payload source et metadata Yellowstone account lorsqu'elle existe, jamais les bytes source. Les identifiants longs utilisent affichage tronqué, tooltip et copie explicite ; le tracing frontend reste metadata-only et ne journalise aucune valeur métier copiée.
|
||||||
|
|
||||||
|
Le hardening final fige les inventaires Cargo/npm/modules/commands/capabilities Tauri, la composition Config, l'absence de réseau/browser-storage/SQL/backend physique dans le frontend, les DTO IPC sans curseur machine et la coexistence exacte entre navigation keyset et inspection random-access. Le correctif `pre.012-fix.001` réaligne aussi `tsconfig.json` Store Desk byte-for-byte sur le gabarit TypeScript commun des autres Desk, ce qui rétablit le build `tsc && vite build` des imports CSS DataTables et ajoute un canari de non-divergence.
|
||||||
|
|
||||||
|
Le gate technique autoritaire passe `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, `ksp-store-lib --no-default-features`, graphes Cargo et build Tauri Store Desk ; il comptabilise 1 592 tests passés, 0 échec et 15 tests opt-in/operator-only ignorés. Après la réconciliation documentaire, un rerun propre depuis `cargo clean` confirme encore les audits/check/Clippy/tests et reconstruit avec succès les cinq Desk KSP Config, SOL Prices, Wallet, Backfill et Store en bundles Linux `.deb` + `.rpm`, sans rouvrir le runtime.
|
||||||
|
|
||||||
|
La trajectoire suivante est explicitement redécoupée pour éviter de répéter l'hypothèse HTTP du premier backfill. `0.3.9` introduira d'abord `ksp-worker-api` comme abstraction générique de service continu, puis seulement **après fermeture fonctionnelle de cette API** produira un audit exhaustif des sources/méthodes `RawTransaction`. Cet audit couvrira HTTP, WS standard, extensions provider, Yellowstone gRPC, block/slot, discovery + hydration, replay/gap-repair et multi-provider, et classera les sources comme alternatives, complémentaires, redondantes ou spécialisées. Il servira d'entrée architecturale à `0.3.10`, qui introduira `ksp-worker-raw-transaction-ingest-lib` multi-source dès V1 et ne modifiera Transport/Config que pour les adaptations réellement justifiées par l'audit. Les futures sources Helius HTTP/WS Mainnet/Devnet devront réutiliser `KSP_SECRET_HELIUS_API_KEY` via Config ; aucune URL Helius n'est ajoutée par `0.3.8`. La terminologie `mainnet`/`mainnet-beta` sera auditée avant tout changement afin de préserver une identité KSP unique pour le même cluster.
|
||||||
|
|
||||||
|
`0.3.11` ouvrira ensuite `ksp-app-raw-transaction-ingest-desk`, capable de choisir/superviser une ou plusieurs sources/méthodes sans dupliquer la logique du worker. `0.3.12` reviendra sur `ksp-job-backfill-lib` et `ksp-app-backfill-desk` pour ajouter les autres stratégies historiques/catch-up pertinentes issues du même audit ; la verticale `getSignaturesForAddress + getTransaction` de `0.3.6`/`0.3.7` reste une première stratégie HTTP valide, pas la définition générale du backfill KSP. kbot3 reste uniquement une référence fonctionnelle pour ces audits, jamais une source de code, DTO, Config, URL ou dépendance.
|
||||||
|
|
||||||
|
`prompts/028-V0_3_9_START_PROMPT.md` ouvre `0.3.9` sur cette base : Worker API doit rester Core-only/générique et être stabilisée avant l'audit RAW Transaction, lequel prépare `0.3.10` sans ajouter de worker concret ni d'endpoints provider pendant `0.3.9`.
|
||||||
|
|
||||||
|
## 0.3.7 — Backfill Desk : contrôle, monitoring, Cancel/Resume et autocomplete — 2026-09-03
|
||||||
|
|
||||||
|
`0.3.7` introduit `ksp-app-backfill-desk`, première application Tauri KSP spécialisée dans le contrôle d'un `ksp-job-backfill-lib` historique `RawTransaction`. L'application reste une couche de composition : `ksp-config-lib` possède les profils/composites/secrets, `ksp-onchain-transport-lib` le pool HTTP et les politiques provider/retry/rate-limit, `ksp-store-lib` la persistence backend-neutral, `ksp-job-backfill-lib` la découverte/hydratation/frontier/checkpoint/runtime et `ksp-job-api` le lifecycle commun. La Desk ne dépend ni d'un backend Store physique, ni de SQL, ni d'un SDK provider.
|
||||||
|
|
||||||
|
Le package reprend le gabarit Tauri KSP courant avec cible lib + bin, launcher mince, splash commun, bridge `tracing`, frontend sous `frontend/`, bindings TS-RS applicatifs, ports stricts `1436/1437` et packaging Config/Schemas distribué. Le composite `cfg.composite.ksp-app-backfill-desk` couvre `mainnet`, `devnet` et `testnet`; Mainnet est le profil par défaut et la cohérence réseau Transport/Store est vérifiée avant ouverture d'une campagne. Les profils Store committed ont été réconciliés sur TLS PostgreSQL `disabled` pour l'environnement local de validation, sans retirer le support `verify_full` du Store/schema.
|
||||||
|
|
||||||
|
Le formulaire Backfill couvre `LatestAddress`, `BeforeAddress`, `AfterAddress` et `ExplicitSignatures`, les commitments `finalized`/`confirmed`, la sélection d'un rôle HTTP logique, `min_context_slot` lorsque le scope l'autorise, ainsi que les bornes publiques de page/candidats/concurrence. Le frontend ne choisit ni réseau physique, ni endpoint/provider, ni JobId; le réseau dérive de la composition Store et les validations finales restent déléguées à `BackfillRequest`.
|
||||||
|
|
||||||
|
Le Start réel conserve un seul run actif, génère le JobId côté backend et installe le handle de contrôle avant le spawn asynchrone. Le monitoring est latest-value : `BackfillSnapshotSource` est projeté via l'événement `ksp-backfill-status`, avec commande de resynchronisation explicite et rétention du dernier terminal. La projection expose lifecycle, phase, scope, boundary, compteurs de candidats/entités/observations, missing/conflicts/holes, concurrence maximale, frontier contiguë, présence de checkpoint et codes d'échec sûrs, sans adresse/signatures de campagne, payload RAW, URL, credential, contexte d'erreur arbitraire ni cursor/checkpoint concret.
|
||||||
|
|
||||||
|
Le Cancel est coopératif, ciblé par le JobId backend et idempotent : un Cancel IPC retardé ne peut pas annuler le run suivant, un terminal gagné reste autoritaire et la fermeture de l'application bloque d'abord les nouveaux Starts puis demande best-effort l'annulation avant le shutdown Store. Le Resume est limité à la session courante : la Desk conserve en mémoire Rust la requête terminale et le checkpoint opaque, `ksp-job-backfill-lib` valide puis réémet le checkpoint pour un nouveau JobId sans changer le scope fingerprint/frontier, et aucun checkpoint durable n'est promis après redémarrage.
|
||||||
|
|
||||||
|
Le polish final ajoute une saisie d'adresse libre avec autocomplete HTML `datalist` dérivé exclusivement de `ksp-core-lib::entries()`/`ProgramIdEntry`; aucune liste de Program IDs n'est hardcodée côté frontend et la sélection n'est jamais une allow-list. Les canaris de hardening figent ensuite l'inventaire des dépendances, modules, commandes Tauri, contrôles frontend, profils Config, frontières Store/provider, sécurité IPC et discipline crate-root des DTO partagés.
|
||||||
|
|
||||||
|
Le gate final passe `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, arbres Cargo ciblés, `cargo tree --duplicates` et `cargo tauri build`. Le run workspace enregistre 1 494 tests passés, 0 échec et 15 tests ignorés explicitement opt-in/operator-only; le build produit les bundles Linux `.deb`, `.rpm` et `.AppImage`.
|
||||||
|
|
||||||
|
`prompts/027-V0_3_8_START_PROMPT.md` ouvre `0.3.8` sur `ksp-app-store-desk` V1. Le gabarit, le visuel, la structure Tauri et les dépendances npm de base viennent exclusivement des Desk KSP actuelles; DataTables reprend le pattern déjà utilisé par Config/Wallet Desk. L'archive kbot3 reste obligatoire uniquement comme référence fonctionnelle/UX des anciens diagnostics et tableaux RAW : aucune source, DTO, commande, SQL, gabarit ou version npm kbot3 n'est reprise. La première tranche doit auditer les capabilities Store existantes et les gaps éventuels de listing/diagnostic avant d'ouvrir des tableaux `RawTransaction`, `RawAccountState`, observations et rétention/tombstones via la seule façade `ksp-store-lib`.
|
||||||
|
|
||||||
|
## 0.3.6 — Job API + backfill RAW historique observable — 2026-09-01
|
||||||
|
|
||||||
|
`0.3.6` introduit `ksp-job-api` comme contrat passif et runtime-neutral pour les traitements bornés/terminables : identité `JobId`/`JobKindCode`, lifecycle explicite, annulation coopérative partagée, snapshots typés et notifications latest-value consommables par plusieurs listeners sans transformer l'API en scheduler, runtime Tokio ou bus d'événements. La surface reste Core-only et sert immédiatement au premier consumer concret `ksp-job-backfill-lib`.
|
||||||
|
|
||||||
|
`ksp-job-backfill-lib` ferme une première verticale historique `RawTransaction` sur les abstractions KSP existantes. Les scopes `LatestAddress`, `BeforeAddress`, `AfterAddress` et `ExplicitSignatures` sont bornés par pages/candidats/concurrence, dédupliqués de manière stable et liés à un fingerprint sémantique réseau/scope qui exclut volontairement provider, endpoint, rôle Transport et `JobId`. La découverte utilise les wrappers typés de `ksp-onchain-transport-lib`; l'hydratation passe exclusivement par `get_transaction_observed`, conserve la provenance du provider/endpoint gagnant et transforme la réponse en RAW v1 canonique avec hash SHA-256 exact, sans SDK provider ni modèle wire dupliqué.
|
||||||
|
|
||||||
|
La persistance passe uniquement par `ksp-store-lib` et son écriture atomique `RawTransaction + RawTransactionObservation` en mode normal. Les résultats distinguent insertion, présence idempotente, tombstone purgé, transaction manquante et conflit de contenu ; aucun read-before-write, `ForceRehydrate`, SQL, backend PostgreSQL direct ou décision de rétention n'est déplacé dans Job. Le réseau Store est vérifié avant écriture et l'identité logique de transaction reste `(RawNetworkId, Signature)` indépendamment de la source d'acquisition.
|
||||||
|
|
||||||
|
Le runtime Backfill ajoute concurrence bornée, réconciliation des terminaisons hors ordre, frontier strictement contiguë et checkpoint caller-owned. Les reprises `Latest`, `Before`, `After` et `ExplicitSignatures` ne sautent que le préfixe durable réellement prouvé. `BackfillJobRuntime` expose un `BackfillJobHandle` clonable avec annulation coopérative et `BackfillSnapshotSource` latest-value ; les opérations pré-Store peuvent être abandonnées, tandis qu'une persistance déjà soumise est toujours drainée avant la décision terminale. Les snapshots conservent phase, compteurs, holes/conflicts/missing, maximum in-flight, frontier/checkpoint et code d'échec sans projeter payload RAW, secret, URL ou état runtime interne.
|
||||||
|
|
||||||
|
Le hardening externe verrouille les codes d'erreur, bornes adversariales, redaction `Debug`, indépendance du fingerprint, absence de fuite Tokio/Futures/backend dans la façade, dépendances backend-neutral et discipline d'import/visibilité crate-root. La réconciliation documentaire finale ajoute les README/USAGE durables de Job API et Backfill et aligne les architectures Jobs/Workers/Apps sans transformer les guides d'utilisation en journal de release.
|
||||||
|
|
||||||
|
Les gates de clôture passent `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace `--all-targets --all-features -- -D warnings`, `cargo test --workspace --all-targets --all-features`, tests ciblés Job/Backfill et graphes Cargo. Les smokes réseau/Tauri déjà explicitement opt-in restent ignorés par défaut ; aucune preuve non exécutée n'est déclarée PASS.
|
||||||
|
|
||||||
|
## 0.3.5 — Interface acquisition events partagés — 2026-08-31
|
||||||
|
|
||||||
|
`0.3.5` étend `ksp-interface-lib` avec deux familles passives d'acquisition réellement partagées, sans transformer Interface en runtime, en event bus ou en seconde couche RAW. `SlotLifecycleEvent` expose un `slot` et un `SlotLifecycleStage` non exhaustif limité à `Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed` et `Rooted`. La normalisation conserve la sémantique commune : les notifications Solana `optimisticConfirmation` et Yellowstone `Confirmed` convergent vers `OptimisticallyConfirmed`, tandis que Solana `root` et Yellowstone `Finalized` convergent vers `Rooted`; les différences d'ordre, de complétude, de replay et de transport restent la responsabilité du producteur/Transport.
|
||||||
|
|
||||||
|
La seconde famille matérialise le fait minimal d'exécution transactionnelle partagé par plusieurs sources. `TransactionSignature` possède exactement 64 octets et un `Debug` redacted, `TransactionExecutionOutcome` distingue seulement `Succeeded` et `Failed`, et `TransactionExecutionEvent` transporte uniquement `slot + signature + outcome`. Les logs, erreurs provider détaillées, commitments, indexes, timestamps, filtres et payloads complets restent Transport-owned. Les snapshots HTTP `getSignatureStatuses`, les transitions one-shot `signatureSubscribe`, un éventuel `TransactionLogEvent`, les votes et les entrées Yellowstone ne sont pas fusionnés artificiellement dans ce contrat.
|
||||||
|
|
||||||
|
La frontière d'ownership reste stricte : les DTOs wire/provider demeurent dans `ksp-onchain-transport-lib`, les événements passifs provider-neutral sont Interface-owned, et les modèles persistants/replayables `RawTransaction` / `RawAccountState` restent `ksp-store-api`. `ksp-interface-lib` conserve exactement `ksp-core-lib` comme seule dépendance normale, sans feature propre, dev/build dependency, serde, codec, logging ou runtime. Les canaris publics, external consumer, inventaires exacts et hardening vérifient également qu'aucun second RAW, metadata source ou payload hostile n'entre dans la surface Interface.
|
||||||
|
|
||||||
|
Les gates de clôture passent audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Interface/Program API, `cargo test --workspace` et graphes Cargo. `cargo tree -p ksp-interface-lib --edges normal` confirme le chemin `ksp-interface-lib -> ksp-core-lib -> solana-pubkey -> solana-address`; le graphe features ne montre aucune feature propre Interface et les doublons éventuels restent ceux du workspace global. La documentation durable a été réconciliée et `TransactionLogEvent` est conservé comme idée différée soumise à un nouveau gate consumer/bornes.
|
||||||
|
|
||||||
|
`prompts/025-V0_3_6_START_PROMPT.md` ouvre `0.3.6` sur le développement parallèle de `ksp-job-api` et `ksp-job-backfill-lib`. La release doit reprendre fonctionnellement le backfill historique kbot3 sans en copier le code : audit obligatoire de l'archive historique, `RawTransaction` par adresse via `getSignaturesForAddress` + `getTransaction`, directions/anchors et déduplication, hydratation/persistence via les façades KSP, frontier/checkpoint/reprise, cancellation/concurrency bornées, idempotence et distinctions missing/conflit. `ksp-job-api` doit en parallèle stabiliser lifecycle/progress/outcome et un contrat de notifications/listeners borné pour qu'une couche supérieure puisse visualiser l'état sans parser les logs. Le ROADMAP enchaîne ensuite l'app de backfill/inspection `0.3.7`, `ksp-worker-api` `0.3.8`, `ksp-worker-live-transactions-retriever-lib` `0.3.9` puis une application de monitoring/visualisation Jobs + Workers `0.3.10`, avec reprise du même pattern d'observabilité côté Worker sans fusionner les sémantiques Job/Worker/Store.
|
||||||
|
|
||||||
|
## 0.3.4 — Store/PostgreSQL RawAccountState + complétude RAW — 2026-08-31
|
||||||
|
|
||||||
|
`0.3.4` complète la seconde vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` et ferme la conformance PostgreSQL des **10 capabilities** backend-agnostic de `ksp-store-api` : les six capabilities `RawTransaction*` acquises en `0.3.3` restent intactes et les quatre capabilities `RawAccountStateRead`, `RawAccountStateWrite`, `RawAccountObservationRead` et `RawAccountObservationWrite` sont désormais implémentées par `PostgresBackend` puis dispatchées par la façade `Store`. La séparation reste stricte : les consommateurs ordinaires passent par `ksp-store-lib`, le backend PostgreSQL conserve SQL/driver/pool/TLS/migrations privés, et la façade reste compilable/testable sans backend via `--no-default-features`.
|
||||||
|
|
||||||
|
La migration additive V002 introduit `ksp_raw_account_states` et `ksp_raw_account_observations` au-dessus de V000/V001 sans modifier leurs bytes. L'identité canonique account est `(pubkey, slot, state_hash)` : plusieurs états d'un même compte dans un même slot restent représentables lorsque `state_hash` diffère, tandis qu'une collision sur la référence complète déclenche une comparaison exacte de `lamports`, `owner`, `executable`, `rent_epoch` et `data` avant de conclure à l'idempotence ou à `store_api.raw_conflict`. Les `u64` physiques utilisent `NUMERIC(20,0)`, les clés/hashes/signatures utilisent des `BYTEA` de largeur contrainte, et les bytes account restent complets jusqu'à la borne KSP de 16 MiB. V002 contient exactement 32 ressources gérées et son checksum final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`; V000 et V001 restent respectivement `d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450` et `31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51`.
|
||||||
|
|
||||||
|
L'acquisition `RawAccountState + RawAccountObservation` est transactionnelle : `INSERT ... ON CONFLICT DO NOTHING`, lecture/verrouillage du canonical en collision, comparaison exacte et rollback complet lorsque l'observation diverge. Une observation supplémentaire vérifie la référence existante sous transaction et ne crée jamais implicitement son state. Les métadonnées Yellowstone `is_startup`, `transaction_signature` et `write_version` restent optionnelles et observation-only ; aucune FK transaction n'est inventée. La navigation account utilise une keyset `(slot, pubkey, state_hash)` ASC/DESC, avec filtre pubkey optionnel et cursor KSPA V1 opaque de 109 octets lié au réseau, au filtre, au range, à la direction et à la famille afin d'empêcher les replays cross-query/cross-family. Aucun `OFFSET`, plafond métier de batch, index owner/provider/time ou lifecycle destructif account n'est introduit.
|
||||||
|
|
||||||
|
Les gates de clôture valident audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config, `cargo test --workspace`, façade sans feature PostgreSQL et graphes Cargo. Le live `postgres_raw_account_live` passe sur **PostgreSQL 17** avec bootstrap V000/V001/V002, round-trip des bytes et du domaine `u64`, idempotence/conflit, états distincts même pubkey+slot, observations/metadata optionnelles, pagination/cursors, concurrence, annulation/rollback et coexistence `RawTransaction`. La réconciliation documentaire finale est ensuite validée sans réouvrir code ni migrations.
|
||||||
|
|
||||||
|
`prompts/024-V0_3_5_START_PROMPT.md` ouvre `0.3.5` sur `ksp-interface-lib` uniquement. Cette release doit réauditer les surfaces d'acquisition actuelles et matérialiser seulement les modèles passifs/event-only réellement partagés, sans recopier `RawTransaction`/`RawAccountState`, sans créer un event bus et sans déplacer les DTOs provider/transport. Les candidats logs, slot/root/slotsUpdates, transaction status et vote sont traités par matrice sémantique ; une famille reste reportée si la convergence ou le consumer réel n'est pas démontré. L'archive historique kbot3 n'est pas requise pour ce gate : les sources de vérité sont la base KSP stable et les contrats officiels actuels des transports concernés.
|
||||||
|
|
||||||
|
## 0.3.3 — Store/PostgreSQL RawTransaction vertical slice — 2026-08-30
|
||||||
|
|
||||||
|
`0.3.3` complète la première vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` sans modifier les contrats backend-agnostic acquis dans `ksp-store-api`. `PostgresBackend` et la façade `Store` implémentent désormais les six capabilities `RawTransactionRead`, `RawTransactionWrite`, `RawTransactionObservationRead`, `RawTransactionObservationWrite`, `RawTransactionRetentionRead` et `RawTransactionRetentionWrite`. Une base PostgreSQL reste liée à un unique `RawNetworkId` par `ksp_store_identity`; le mauvais réseau est refusé avant I/O, les slots `u64` sont conservés exactement en `NUMERIC(20,0)`, et la migration logique V001 reste découpée en ressources tables/contraintes/indexes avec vérification de compatibilité du schéma effectif.
|
||||||
|
|
||||||
|
La persistence canonique utilise une transaction PostgreSQL unique pour `RawTransaction + observation`, tente l'insert sous contrainte d'unicité puis compare le contenu réel sous verrou en cas de collision : contenu identique -> idempotence, divergence -> `store_api.raw_conflict`. Les observations supplémentaires conservent leur provenance complète et ne créent jamais implicitement un canonical absent. La lecture reconstruit `Full` depuis le payload chaud et `Archived` depuis la relation archive ; `Purged` reste un tombstone minimal. La navigation est une keyset stricte `(slot, signature)` ASC/DESC sans `OFFSET`, avec cursor V1 opaque de 109 octets lié au réseau, à la direction, aux bornes et à la dernière clé. Aucun plafond métier de batch n'est introduit : seule la borne physique PostgreSQL de `LIMIT requested + 1` est appliquée.
|
||||||
|
|
||||||
|
La rétention physique supportée est `Full -> Archived -> Purged`, sérialisée par `FOR UPDATE`, avec outcomes `Applied`, `AlreadyAtTarget` et `ExpectedStateMismatch`. Le mode normal respecte le tombstone après purge ; `ForceRehydrate` reste explicite et atomique. `Compacted` demeure volontairement non supporté physiquement et retourne `store.postgres_retention_compaction_unsupported` plutôt que de présenter TOAST ou une compression implicite comme contrat KSP. Les erreurs backend sont réduites à des classes/phases statiques puis projetées vers des codes Store/API stables, sans URI, SQL, SQLSTATE, bind ni texte serveur.
|
||||||
|
|
||||||
|
La preuve PostgreSQL réelle a conduit à durcir l'introspection de schéma PostgreSQL 17 : canonicalisation ciblée des CHECK numériques reconstruits par le catalogue, conservation des littéraux texte, restauration des helpers de classification de schéma et distinction d'un drift d'une migration déjà enregistrée lorsque `schema_autoupdate=false`. Les ressources SQL V000/V001 et leurs checksums sont restés inchangés pendant ces corrections (`V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450`, `V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51`).
|
||||||
|
|
||||||
|
Le gate technique final `pre.011` passe audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, les tests ciblés Store/API/PostgreSQL/Config, les tests et checks façade avec `--no-default-features`, `cargo test --workspace` et les graphes Cargo. Le live `postgres_raw_transaction_live` est ensuite rejoué avec succès sur **PostgreSQL 17**, couvrant bootstrap/drift-repair, atomicité, concurrence identique/divergente, rollback sur collision et annulation, pagination/cursor, rétention/races, ForceRehydrate et réouverture durable. Aucun build Tauri supplémentaire n'est requis : `0.3.3` ne change ni resources applicatives ni packaging desktop. `RawAccountState` PostgreSQL et la complétude RAW restent réservés à `0.3.4`. `prompts/023-V0_3_4_START_PROMPT.md` ouvre cette slice suivante sur les quatre capabilities `RawAccount*`, une migration additive au-dessus de V000/V001, puis la conformance finale des dix capabilities RAW ; l'archive historique kbot3 y reste une source de comparaison ciblée account/observation, jamais une architecture à recopier.
|
||||||
|
|
||||||
|
## 0.3.2 — Store/PostgreSQL runtime foundation — 2026-08-30
|
||||||
|
|
||||||
|
`0.3.2` introduit ensemble `ksp-store-lib` et `ksp-store-postgres-lib` comme fondation runtime/backend PostgreSQL au-dessus des contrats backend-agnostic de `ksp-store-api`, sans encore implémenter de capability RAW métier. La façade `Store` conserve un backend connu `Postgres` activé par défaut via Cargo feature, reste compilable avec `--no-default-features`, lie chaque instance à exactement un `RawNetworkId` et n'expose aucun `Pool`, `Client`, `Row`, `Statement`, driver ou SQL physique. `ksp-config-lib` possède désormais `std.store` avec trois targets nommés `devnet`, `mainnet` et `testnet`, chacun associé à un réseau explicite et à une URI PostgreSQL Secret indépendante ; Store/backend ne lisent directement ni `.env`, ni `KSP_*`, ni `PG*`, ni `.pgpass`.
|
||||||
|
|
||||||
|
Le backend de référence utilise `tokio-postgres 0.7.18`, `deadpool-postgres 0.14.2`, `tokio-postgres-rustls 0.14.0`, Rustls 0.23 avec roots système/AWS-LC et des modes TLS KSP limités à `Disabled` et `VerifyFull`. Les URI sont parsées puis normalisées sous la policy typée KSP, le pool et les deadlines connect/wait/create/recycle/shutdown sont bornés, et une ouverture réussie exige une connexion physique puis un bootstrap valide. Le moteur de migrations privé commence par `V000__bootstrap.sql`, enregistre version/nom/SHA-256 dans `ksp_store_schema_migrations`, sérialise les runners par advisory transaction lock borné et refuse checksum mismatch ou schema plus récent sans down automatique. La façade fournit également `runtime_snapshot()` et `health().await` avec une projection portable/redacted de readiness et des compteurs pool sûrs.
|
||||||
|
|
||||||
|
Les canaris de hardening verrouillent 84 exports crate-root côté façade, la frontière backend/Config, l'absence de bypass environnement, la redaction des URI/erreurs, la matrice de features et l'absence de persistence `RawTransaction`/`RawAccountState`. Le gate technique final a été rejoué après `cargo clean` : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés de toutes les crates, `cargo test --workspace`, graphes Cargo et les trois builds Tauri Linux passent. Le smoke PostgreSQL réel passe sur **PostgreSQL 17** et prouve bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready` et fermeture bornée ; la policy de fondation reste PostgreSQL >= 15 sans maximum KSP artificiel.
|
||||||
|
|
||||||
|
`prompts/022-V0_3_3_START_PROMPT.md` ouvre ensuite uniquement la vertical slice PostgreSQL `RawTransaction` complète sur les mêmes crates et la même fondation : six capabilities transaction/observation/rétention, atomicité, idempotence/conflit, get/list cursorisé, tombstone et `ForceRehydrate`. `RawAccountState` PostgreSQL et la complétude RAW restent réservés à `0.3.4`. L'archive historique `khadhroony-bot3_v0.5.3-pre.005-fix010.zip` reste obligatoire au `pre.001` de `0.3.3`, mais seulement pour réauditer l'ancien schéma/repository RAW transaction comme source d'héritage, jamais comme autorité 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 — 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.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`.
|
||||||
|
|||||||
40
Cargo.toml
40
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 109
|
# version: 593
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.1"
|
version = "0.3.14"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -14,21 +14,43 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
argon2 = { version = "^0.6", default-features = false }
|
||||||
|
base64 = { version = "^0.23" }
|
||||||
|
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||||
|
chrono = { version = "^0.4", default-features = false }
|
||||||
|
deadpool-postgres = { version = "^0.14", default-features = false }
|
||||||
|
directories = { version = "^6.0" }
|
||||||
|
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||||
fs2 = { version = "^0.4" }
|
fs2 = { version = "^0.4" }
|
||||||
|
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.53", default-features = false }
|
||||||
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
|
rustls = { version = "^0.23", default-features = false }
|
||||||
|
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||||
serde = { version = "^1.0" }
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
jsonschema = { version = "^0.49", default-features = false }
|
sha2 = { version = "^0.11", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
solana-keypair = { version = "^3.1", default-features = false }
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
|
tauri = { version = "^2.11" }
|
||||||
|
tauri-build = { version = "^2.6" }
|
||||||
|
tauri-plugin-dialog = { version = "^2.7" }
|
||||||
|
tauri-plugin-tracing = { version = "^0.3" }
|
||||||
|
tempfile = { version = "^3.27" }
|
||||||
tracing = { version = "^0.1", default-features = false }
|
tracing = { version = "^0.1", default-features = false }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
tokio = { version = "^1.53", default-features = false }
|
tokio = { version = "^1.53", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
tokio-postgres = { version = "^0.7", default-features = false }
|
||||||
tauri = { version = "^2.11" }
|
tokio-postgres-rustls = { version = "^0.14", default-features = false }
|
||||||
tauri-build = { version = "^2.6" }
|
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||||
tauri-plugin-tracing = { version = "^0.3" }
|
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.7", default-features = false }
|
||||||
|
zeroize = { version = "^1.9" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
missing_docs = "warn"
|
missing_docs = "warn"
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: README.md -->
|
<!-- file: README.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# Khadhroony Solana Project
|
# Khadhroony Solana Project
|
||||||
|
|
||||||
@@ -47,6 +47,18 @@ Les besoins du trading constituent une priorité produit à court terme mais ne
|
|||||||
- Aucun `rust-toolchain.toml` n'est utilisé.
|
- Aucun `rust-toolchain.toml` n'est utilisé.
|
||||||
- Les environnements et contraintes des démonstrations doivent être visibles dans leur nomenclature lorsqu'ils ne sont pas sélectionnables.
|
- Les environnements et contraintes des démonstrations doivent être visibles dans leur nomenclature lorsqu'ils ne sont pas sélectionnables.
|
||||||
|
|
||||||
|
## Fondations opérationnelles actuelles
|
||||||
|
|
||||||
|
La couche RAW dispose d'une façade Store backend-neutral et d'un premier job historique concret. `ksp-job-api` porte les contrats passifs communs des jobs bornés ; `ksp-job-backfill-lib` compose Transport observé et Store pour découvrir, hydrater et persister des transactions historiques RAW avec concurrence bornée, checkpoint contigu caller-owned, annulation coopérative et snapshots latest-value.
|
||||||
|
|
||||||
|
`ksp-app-backfill-desk` fournit la surface desktop spécialisée de composition de ce runtime : sélection d'un scope historique et d'un rôle HTTP logique, validation backend des bornes, Start single-run, monitoring latest-value, Cancel ciblé et Resume in-session sur checkpoint Rust-only. L'application ouvre Transport et Store via leurs façades KSP, ne dépend d'aucun backend Store concret et n'expose ni URL/credential, ni payload RAW, ni checkpoint opaque au frontend.
|
||||||
|
|
||||||
|
`ksp-app-store-desk` fournit l'inspection desktop read-only du Store RAW via `ksp-store-lib` : health/runtime backend-neutral, tables server-side `RawTransaction` et `RawAccountState`, observations associées, détails avec previews bornées et lecture des états de rétention/tombstones. DataTables possède l'unique pagination visible de l'inspection random-access ; la pagination cursor/keyset des consumers machine reste distincte et intacte.
|
||||||
|
|
||||||
|
`ksp-worker-api` fournit désormais la fondation générique des services continus : identité Worker, lifecycle borné, health/activity, intention de stop coopératif et observation latest-value. Cette API reste Core-only, runtime-neutral et sans connaissance Solana, Transport, Store ou Job. Elle ne démarre ni n'arrête elle-même un runtime concret.
|
||||||
|
|
||||||
|
`ksp-worker-raw-transaction-ingest-lib` est le premier Worker concret distinct du Job Backfill. Il conserve sa fondation Start/Stop, admission bornée, Common RAW, Store backend-neutral, shutdown borné et snapshots latest-value. Ses sources productives couvrent maintenant Yellowstone + hydration HTTP `getTransaction`, Standard WS `logsSubscribe` + hydration, Standard WS `blockSubscribe` RAW-direct, Helius `transactionSubscribe` + hydration et Solana HTTP live block polling. Le polling HTTP démarre au `getSlot` observé du run, découvre uniquement les blocs live via `getBlocksWithLimit` puis matérialise les slots listés via `getBlock observed`; il n'est jamais utilisé comme Backfill implicite. Les signaux hydratés de même `(network, signature, commitment)` convergent vers le même coordinator, tandis que les voies block RAW-direct passent par la même admission Common RAW. Le reconnect/replay reste propriétaire de Transport, la frontier de traitement reste run-local et un gap de rétention prouvé provoque un fault au lieu de lancer une campagne historique. `ksp-job-backfill-lib` conserve son rôle historique paramétré et borné ; les deux producteurs restent indépendants et convergent uniquement vers les mêmes contrats RAW/Store.
|
||||||
|
|
||||||
## Points d'entrée
|
## Points d'entrée
|
||||||
|
|
||||||
- [`RULES.md`](RULES.md) — index des règles normatives ;
|
- [`RULES.md`](RULES.md) — index des règles normatives ;
|
||||||
|
|||||||
115
ROADMAP.md
115
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 34 -->
|
<!-- version: 112 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -40,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 fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/STRUCTURAL/DECODED/DOMAIN stabilisés.
|
||||||
- [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.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`.
|
||||||
|
|
||||||
### Releases fonctionnelles décidées/pressenties
|
### Releases fonctionnelles décidées/pressenties
|
||||||
|
|
||||||
- [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.
|
- [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` — Compléter HTTP Accounts + Tokens + Cluster : 5 méthodes Accounts restantes + 5 Tokens + 12 Cluster restantes, soit 22 méthodes.
|
- [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` — Compléter les 11 méthodes HTTP Transactions, y compris write/submission technique avec politique no-resend ambigu.
|
- [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` — Compléter les 10 méthodes HTTP Blocks + 5 Economics et exécuter la compliance finale de toute la surface HTTP 52 current + 14 deprecated historiques.
|
- [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` — 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.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
- [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` — É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.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` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
- [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` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
- [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-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
- [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.
|
||||||
- [ ] `0.2.11` — Introduire une petite application desk de visualisation des prix.
|
- [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.
|
||||||
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
- [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.
|
||||||
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
- [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
|
||||||
|
|
||||||
@@ -69,33 +80,71 @@ 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 D1, 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 D1 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.
|
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||||
- [ ] `0.3.3` — Introduire `ksp-job-api` et un job de backfill historique concret.
|
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
||||||
- [ ] `0.3.4` — Introduire une application spécialisée de backfill/inspection RAW.
|
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
||||||
- [ ] 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.
|
- [X] `0.3.5` — `ksp-interface-lib` étendu avec deux familles passives réellement partagées : `SlotLifecycleEvent` (`Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed`, `Rooted`) et `TransactionExecutionEvent` (`slot + TransactionSignature[64] + Succeeded/Failed`). Interface reste Core-only, provider-neutral, sans serde/codec/runtime/event bus et sans duplication de `RawTransaction`/`RawAccountState`; les DTOs riches restent Transport-owned et les candidats non convergents restent différés.
|
||||||
|
- [X] `0.3.6` — `ksp-job-api` + `ksp-job-backfill-lib` stables pour la première verticale historique `RawTransaction` : lifecycle/cancellation/latest-value runtime-neutral, scopes `Latest`/`Before`/`After`/signatures explicites, découverte/hydratation Transport observée, RAW v1 canonique, persistance Store atomique/idempotente, concurrence bornée, frontier contiguë, checkpoint/reprise caller-owned, snapshots sûrs et hardening externe sans dépendance backend/provider directe.
|
||||||
|
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
||||||
|
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
||||||
|
- [X] `0.3.9` — `ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
|
||||||
|
- [X] `0.3.10` — Lower-layer RAW Transaction commune stabilisée : `ksp-raw-transaction-lib` possède la canonicalisation RAW v1 source-neutral partagée, le Backfill est migré sans changement des golden bytes/hash, Transport expose `get_block_observed`, le wire Solana Legacy/V0/V1 est matérialisé et les canaris HTTP/WS standard/Helius/Yellowstone qualifient précisément les voies directes ou nécessitant hydration. La release se ferme sans runtime Worker concret ; celui-ci commence en `0.3.11` selon le redécoupage `0.3.11`–`0.3.14`.
|
||||||
|
- [X] `0.3.11` — Fondation runtime de `ksp-worker-raw-transaction-ingest-lib` livrée : crate/dependency firewall, settings source-neutral, handle/start-stop, lifecycle, snapshots, supervisor, admission bornée, canonicalisation Common RAW, persistence/déduplication Store déterministe, backpressure et shutdown/fault hardening. La release se ferme volontairement sans source réseau productive ; Yellowstone + hydration HTTP commencent en `0.3.12`.
|
||||||
|
- [X] `0.3.12` — Première source live productive du Worker livrée : Yellowstone `Transaction`/`TransactionStatus`/`Block` vers signaux source-neutral, coalescence bornée puis hydration HTTP `getTransaction` observed avant Common RAW/Store ; `BlockMeta`/`Slot` restent continuity-only. Processing frontier run-local, reconnect/from_slot/ReplayInfo Transport-owned, compteurs source-neutral et fault sur gap de rétention prouvé sans Backfill automatique. Hardening duplicate/backpressure/stop/fault et canaris cross-layer fermés ; gate workspace complet vert, smokes live HTTP Devnet + WebSocket Devnet verts, Yellowstone token-gated/Worker end-to-end laissés explicitement non exécutés.
|
||||||
|
- [X] `0.3.13` — Convergence live multi-source du Worker RAW livrée : composition caller-owned `1..32` sources d’un même réseau, cinq familles Yellowstone / Standard Logs / Standard Block / Helius Transaction / HTTP Block Polling, hydration globale des trois voies reference-bearing, RAW-direct Legacy/V0/V1 pour les deux voies blocs, observations multiples, content conflict explicite, coalescence cross-source, fairness/backpressure/health source-neutral et shutdown hardening sans second actor Transport. Gate workspace complet vert (`1 850` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; les preuves provider/resource-gated non exécutées restent explicitement non revendiquées.
|
||||||
|
- [X] `0.3.14` — Gap repair/hardening multi-source du Worker RAW livré : gaps run-local bornés, distinction reconnect/replay/delivery/coverage, TargetCoverage conservative, coverage redondante prouvée, discovery HTTP bornée, known-reference hydration, réconciliation source-loss, health gap-aware, fairness nominal/repair, snapshots publics source-neutral et shutdown/drain renforcé. Le pipeline Common RAW/Store reste unique, Worker/Backfill restent indépendants, EARLY n’est pas ajouté. Gate workspace complet vert (`1 931` PASS / `0` échec / `15` ignored) et smokes keyless HTTP + WebSocket Devnet verts ; preuves provider/resource-gated non provisionnées explicitement non revendiquées.
|
||||||
|
- [ ] `0.3.15` — Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP de composition/supervision des routes live `RawTransaction`. Une route est une stratégie complète pouvant nécessiter plusieurs capabilities/endpoints (par exemple gRPC + HTTP ou WS + HTTP) et n’est sélectionnable que si toutes ses requirements sont composables depuis Config sur le réseau choisi ; la présence d’un provider ne vaut jamais capability implicite. Au Start, la Config est revalidée et **une instance Worker est lancée par route sélectionnée** — pas par source physique — afin de valider l’opérabilité réelle puis exposer lifecycle, health, backpressure, reconnect/replay, gaps/repair et compteurs sûrs. Plusieurs Workers peuvent partager le même Store, sans coordination implicite de gaps entre routes. La Desk ne réimplémente ni Transport, discovery, hydration, déduplication, repair ni persistence.
|
||||||
|
- [ ] `0.3.16` — Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres, sans supposer qu'une source live WS constitue un historique universel.
|
||||||
|
|
||||||
## Série CORE suivante
|
### TODO/IDEAS — applications spécialisées et control plane
|
||||||
|
|
||||||
- [ ] Définir la persistence CORE canonique Solana générique.
|
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
||||||
- [ ] Implémenter `RAW -> CORE` sans decoder Program : blocs, slots, signatures, transactions/messages, comptes, instructions/CPI brutes, logs/meta et relations structurelles.
|
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'une couche D3 DECODED de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
||||||
- [ ] Ajouter replay/backfill RAW -> CORE.
|
|
||||||
- [ ] Ajouter worker/service CORE.
|
|
||||||
- [ ] Ajouter l'application de contrôle/inspection CORE utile.
|
|
||||||
|
|
||||||
## Séries DECODE/SPECIALIZED/EXECUTION — progression verticale
|
### TODO/IDEAS — taxonomie D1, processing et rétention
|
||||||
|
|
||||||
|
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille D1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||||
|
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11`–`0.3.14` Worker live / `0.3.16` Backfill historique.
|
||||||
|
- [X] **TODO Helius `0.3.13`** — la voie Worker Helius `transactionSubscribe` réutilise la surface Transport et les profils Config Helius déjà existants avec `KSP_SECRET_HELIUS_API_KEY`; aucun nouveau profil, secret, SDK provider, tier codé ou second client parallèle n’a été nécessaire. Le payload riche reste Transport-owned et la voie Worker hydrate via `getTransaction observed` tant que le RAW complet n’est pas prouvé directement.
|
||||||
|
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données D1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
|
||||||
|
- [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 est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
||||||
|
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
||||||
|
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
||||||
|
- [X] slot/root/slotsUpdates — `0.3.5` stabilise `SlotLifecycleEvent` pour l’intersection réellement partagée, sans persistence Store par défaut et sans promettre l’ordre/complétude du flux.
|
||||||
|
- [ ] **TODO** — vote realtime : reste hors Interface tant qu’aucun consumer transversal et aucune sémantique provider-neutral suffisamment précise ne justifient un contrat partagé.
|
||||||
|
- [ ] **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`.
|
||||||
|
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
||||||
|
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
||||||
|
- [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 D2/D3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||||
|
|
||||||
|
## 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 D1 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 le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
||||||
|
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
||||||
|
|
||||||
|
## Séries DECODED/DOMAIN/EXECUTION — progression verticale
|
||||||
|
|
||||||
### Priorité 1 — Solana Core Programs
|
### Priorité 1 — Solana Core Programs
|
||||||
|
|
||||||
@@ -108,7 +157,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
|
||||||
|
|
||||||
@@ -132,7 +181,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
|
||||||
|
|
||||||
|
|||||||
66
config/composite.ksp-app-backfill-desk.json
Normal file
66
config/composite.ksp-app-backfill-desk.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "mainnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "transport",
|
||||||
|
"file_id": "cfg.std.transport",
|
||||||
|
"profile_id": "devnet_public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "devnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "transport",
|
||||||
|
"file_id": "cfg.std.transport",
|
||||||
|
"profile_id": "mainnet_backfill_pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "mainnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "testnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "console_info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "transport",
|
||||||
|
"file_id": "cfg.std.transport",
|
||||||
|
"profile_id": "publicnode_testnet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "testnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
config/composite.ksp-app-store-desk.json
Normal file
51
config/composite.ksp-app-store-desk.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "mainnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "devnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "mainnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "testnet",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"component_id": "logging",
|
||||||
|
"file_id": "cfg.std.logging",
|
||||||
|
"profile_id": "supertrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_id": "store",
|
||||||
|
"file_id": "cfg.std.store",
|
||||||
|
"profile_id": "testnet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
config/examples/std.store.example.json
Normal file
81
config/examples/std.store.example.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"default_profile": "devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"network": "devnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "verify_full"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet",
|
||||||
|
"network": "mainnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "verify_full"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "testnet",
|
||||||
|
"network": "testnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "verify_full"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
{
|
{
|
||||||
"format_version": 1,
|
"format_version": 2,
|
||||||
"retry": {
|
"retry": {
|
||||||
"max_retries": 3,
|
"max_retries": 3,
|
||||||
"initial_backoff_ms": 150,
|
"initial_backoff_ms": 150,
|
||||||
"max_backoff_ms": 3000
|
"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",
|
"default_profile": "mainnet_mixed",
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
@@ -14,8 +31,8 @@
|
|||||||
"name": "mainnet_public",
|
"name": "mainnet_public",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -23,7 +40,9 @@
|
|||||||
{
|
{
|
||||||
"role": "default",
|
"role": "default",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"request_kinds": ["*"],
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
"priority": 200,
|
"priority": 200,
|
||||||
"limits": {
|
"limits": {
|
||||||
"requests_per_second": 5,
|
"requests_per_second": 5,
|
||||||
@@ -38,7 +57,7 @@
|
|||||||
"name": "mainnet_private",
|
"name": "mainnet_private",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "private-provider",
|
"provider": "private-provider",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 10000,
|
"request_timeout_ms": 10000,
|
||||||
@@ -47,7 +66,9 @@
|
|||||||
{
|
{
|
||||||
"role": "default",
|
"role": "default",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"request_kinds": ["*"],
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
"priority": 100,
|
"priority": 100,
|
||||||
"limits": {
|
"limits": {
|
||||||
"requests_per_second": 20,
|
"requests_per_second": 20,
|
||||||
@@ -58,6 +79,69 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "mainnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mainnet_helius_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
277
config/schemas/std.store.schema.json
Normal file
277
config/schemas/std.store.schema.json
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "urn:ksp:schema:std.store:v2",
|
||||||
|
"title": "KSP standard Store configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"format_version",
|
||||||
|
"default_profile",
|
||||||
|
"profiles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"enum": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_profile": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/profileV1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"const": 2
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/profileV2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"profileId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
||||||
|
},
|
||||||
|
"networkId": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128,
|
||||||
|
"pattern": "^[A-Za-z0-9_.:-]+$"
|
||||||
|
},
|
||||||
|
"duration100To60000": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 60000
|
||||||
|
},
|
||||||
|
"profileV1": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"profile_id",
|
||||||
|
"network",
|
||||||
|
"backend",
|
||||||
|
"postgres"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"profile_id": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"$ref": "#/$defs/networkId"
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"const": "postgres"
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"$ref": "#/$defs/postgresV1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profileV2": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"profile_id",
|
||||||
|
"network",
|
||||||
|
"backend",
|
||||||
|
"postgres"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"profile_id": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"$ref": "#/$defs/networkId"
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"const": "postgres"
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"$ref": "#/$defs/postgresV2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgresV1": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"connection_uri",
|
||||||
|
"pool",
|
||||||
|
"tls",
|
||||||
|
"bootstrap",
|
||||||
|
"shutdown_timeout_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"connection_uri": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"pool": {
|
||||||
|
"$ref": "#/$defs/pool"
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"$ref": "#/$defs/tls"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"$ref": "#/$defs/bootstrapV1"
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgresV2": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"connection_uri",
|
||||||
|
"pool",
|
||||||
|
"tls",
|
||||||
|
"bootstrap",
|
||||||
|
"shutdown_timeout_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"connection_uri": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"pool": {
|
||||||
|
"$ref": "#/$defs/pool"
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"$ref": "#/$defs/tls"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"$ref": "#/$defs/bootstrapV2"
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pool": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"max_connections",
|
||||||
|
"connect_timeout_ms",
|
||||||
|
"wait_timeout_ms",
|
||||||
|
"create_timeout_ms",
|
||||||
|
"recycle_timeout_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"max_connections": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 64
|
||||||
|
},
|
||||||
|
"connect_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/duration100To60000"
|
||||||
|
},
|
||||||
|
"wait_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/duration100To60000"
|
||||||
|
},
|
||||||
|
"create_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/duration100To60000"
|
||||||
|
},
|
||||||
|
"recycle_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/duration100To60000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"disabled",
|
||||||
|
"verify_full"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bootstrapV1": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"auto_migrate",
|
||||||
|
"migration_timeout_ms",
|
||||||
|
"migration_lock_timeout_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"auto_migrate": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"migration_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000
|
||||||
|
},
|
||||||
|
"migration_lock_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 120000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bootstrapV2": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_autocreate",
|
||||||
|
"schema_autoupdate",
|
||||||
|
"migration_timeout_ms",
|
||||||
|
"migration_lock_timeout_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_autocreate": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"schema_autoupdate": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"migration_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1000,
|
||||||
|
"maximum": 300000
|
||||||
|
},
|
||||||
|
"migration_lock_timeout_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 120000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "urn:ksp:schema:std.transport:v1",
|
"$id": "urn:ksp:schema:std.transport:v3",
|
||||||
"title": "KSP standard HTTP Transport configuration",
|
"title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
|
||||||
"type": "object",
|
"oneOf": [
|
||||||
"additionalProperties": false,
|
{
|
||||||
"required": ["format_version", "retry", "default_profile", "profiles"],
|
"$ref": "#/$defs/documentV1"
|
||||||
"properties": {
|
},
|
||||||
"format_version": {"const": 1},
|
{
|
||||||
"retry": {"$ref": "#/$defs/retry"},
|
"$ref": "#/$defs/documentV2"
|
||||||
"default_profile": {"$ref": "#/$defs/profileId"},
|
},
|
||||||
"profiles": {
|
{
|
||||||
"type": "array",
|
"$ref": "#/$defs/documentV3"
|
||||||
"minItems": 1,
|
|
||||||
"items": {"$ref": "#/$defs/profile"}
|
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"profileId": {
|
"profileId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -35,50 +33,99 @@
|
|||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"maximum": 4294967295
|
"maximum": 4294967295
|
||||||
},
|
},
|
||||||
|
"positiveUsize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 4294967295
|
||||||
|
},
|
||||||
"retry": {
|
"retry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
|
"required": [
|
||||||
|
"max_retries",
|
||||||
|
"initial_backoff_ms",
|
||||||
|
"max_backoff_ms"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"max_retries": {"type": "integer", "minimum": 0, "maximum": 100},
|
"max_retries": {
|
||||||
"initial_backoff_ms": {"$ref": "#/$defs/positiveMs"},
|
"type": "integer",
|
||||||
"max_backoff_ms": {"$ref": "#/$defs/positiveMs"}
|
"minimum": 0,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"initial_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/positiveMs"
|
||||||
|
},
|
||||||
|
"max_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/positiveMs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"limits": {
|
"limits": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"requests_per_second": {"$ref": "#/$defs/positiveU32"},
|
"requests_per_second": {
|
||||||
"burst_capacity": {"$ref": "#/$defs/positiveU32"},
|
"$ref": "#/$defs/positiveU32"
|
||||||
"max_concurrent_requests": {"$ref": "#/$defs/positiveU32"},
|
},
|
||||||
"pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"}
|
"burst_capacity": {
|
||||||
|
"$ref": "#/$defs/positiveU32"
|
||||||
|
},
|
||||||
|
"max_concurrent_requests": {
|
||||||
|
"$ref": "#/$defs/positiveU32"
|
||||||
|
},
|
||||||
|
"pause_after_rate_limit_ms": {
|
||||||
|
"$ref": "#/$defs/positiveMs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["role", "enabled", "request_kinds", "priority", "limits"],
|
"required": [
|
||||||
|
"role",
|
||||||
|
"enabled",
|
||||||
|
"request_kinds",
|
||||||
|
"priority",
|
||||||
|
"limits"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"role": {"$ref": "#/$defs/descriptor"},
|
"role": {
|
||||||
"enabled": {"type": "boolean"},
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"request_kinds": {
|
"request_kinds": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"items": {"$ref": "#/$defs/descriptor"},
|
"items": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"if": {"contains": {"const": "*"}},
|
"if": {
|
||||||
"then": {"maxItems": 1}
|
"contains": {
|
||||||
|
"const": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
|
"priority": {
|
||||||
"limits": {"$ref": "#/$defs/limits"}
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 4294967295
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"$ref": "#/$defs/limits"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"endpoint": {
|
"httpEndpoint": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
@@ -92,31 +139,592 @@
|
|||||||
"roles"
|
"roles"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"$ref": "#/$defs/descriptor"},
|
"name": {
|
||||||
"enabled": {"type": "boolean"},
|
"$ref": "#/$defs/descriptor"
|
||||||
"provider": {"$ref": "#/$defs/descriptor"},
|
},
|
||||||
"cluster": {"$ref": "#/$defs/descriptor"},
|
"enabled": {
|
||||||
"url": {"type": "string", "minLength": 1},
|
"type": "boolean"
|
||||||
"connect_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
},
|
||||||
"request_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
"provider": {
|
||||||
"max_idle_connections_per_host": {"type": "integer", "minimum": 1},
|
"$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": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"items": {"$ref": "#/$defs/role"}
|
"items": {
|
||||||
|
"$ref": "#/$defs/role"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profile": {
|
"wsReconnect": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["profile_id", "endpoints"],
|
"required": [
|
||||||
|
"max_retries",
|
||||||
|
"initial_backoff_ms",
|
||||||
|
"max_backoff_ms"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"profile_id": {"$ref": "#/$defs/profileId"},
|
"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": {
|
"endpoints": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"items": {"$ref": "#/$defs/endpoint"}
|
"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",
|
||||||
@@ -93,6 +243,290 @@
|
|||||||
"level": "info"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
81
config/std.store.json
Normal file
81
config/std.store.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"default_profile": "devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet",
|
||||||
|
"network": "devnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_DEVNET_POSTGRES_URI:-postgresql://localhost/ksp_devnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "disabled"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet",
|
||||||
|
"network": "mainnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "disabled"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "testnet",
|
||||||
|
"network": "testnet",
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"connection_uri": "${KSP_SECRET_STORE_TESTNET_POSTGRES_URI:-postgresql://localhost/ksp_testnet}",
|
||||||
|
"pool": {
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"wait_timeout_ms": 5000,
|
||||||
|
"create_timeout_ms": 10000,
|
||||||
|
"recycle_timeout_ms": 5000
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"mode": "disabled"
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"schema_autocreate": true,
|
||||||
|
"schema_autoupdate": true,
|
||||||
|
"migration_timeout_ms": 30000,
|
||||||
|
"migration_lock_timeout_ms": 10000
|
||||||
|
},
|
||||||
|
"shutdown_timeout_ms": 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,41 @@
|
|||||||
{
|
{
|
||||||
"format_version": 1,
|
"format_version": 3,
|
||||||
"retry": {
|
"retry": {
|
||||||
"max_retries": 2,
|
"max_retries": 2,
|
||||||
"initial_backoff_ms": 100,
|
"initial_backoff_ms": 100,
|
||||||
"max_backoff_ms": 2000
|
"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",
|
"default_profile": "devnet_public",
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
@@ -23,7 +54,9 @@
|
|||||||
{
|
{
|
||||||
"role": "default",
|
"role": "default",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"request_kinds": ["*"],
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
"priority": 100,
|
"priority": 100,
|
||||||
"limits": {
|
"limits": {
|
||||||
"requests_per_second": 5,
|
"requests_per_second": 5,
|
||||||
@@ -34,6 +67,74 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,8 +144,8 @@
|
|||||||
"name": "solana_mainnet_public",
|
"name": "solana_mainnet_public",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "solana-public",
|
"provider": "solana-public",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet",
|
||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"request_timeout_ms": 15000,
|
"request_timeout_ms": 15000,
|
||||||
"max_idle_connections_per_host": 8,
|
"max_idle_connections_per_host": 8,
|
||||||
@@ -52,7 +153,9 @@
|
|||||||
{
|
{
|
||||||
"role": "default",
|
"role": "default",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"request_kinds": ["*"],
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
"priority": 100,
|
"priority": 100,
|
||||||
"limits": {
|
"limits": {
|
||||||
"requests_per_second": 5,
|
"requests_per_second": 5,
|
||||||
@@ -63,6 +166,225 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet_backfill_pool",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "backfill_pool",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "backfill_solana_public",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_mainnet_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"url": "https://solana-rpc.publicnode.com",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "backfill_pool",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "backfill_publicnode",
|
||||||
|
"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",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "publicnode_mainnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet.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",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_mainnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "mainnet",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
crates/ksp-app-backfill-desk/Cargo.toml
Normal file
44
crates/ksp-app-backfill-desk/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# file: crates/ksp-app-backfill-desk/Cargo.toml
|
||||||
|
# version: 4
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-app-backfill-desk"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ksp_app_backfill_desk_lib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ksp-app-backfill-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-job-api = { path = "../ksp-job-api" }
|
||||||
|
ksp-job-backfill-lib = { path = "../ksp-job-backfill-lib" }
|
||||||
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
|
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||||
|
ksp-store-lib = { path = "../ksp-store-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
|
||||||
105
crates/ksp-app-backfill-desk/README.md
Normal file
105
crates/ksp-app-backfill-desk/README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!-- file: crates/ksp-app-backfill-desk/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# `ksp-app-backfill-desk`
|
||||||
|
|
||||||
|
`ksp-app-backfill-desk` est l'application desktop spécialisée de contrôle d'un backfill historique `RawTransaction` KSP.
|
||||||
|
|
||||||
|
Elle reste une couche de composition Tauri : Config sélectionne les profils, Transport possède les endpoints/rôles/retry/rate-limit, Store possède la persistence et le backend, et `ksp-job-backfill-lib` possède découverte, hydratation, frontier/checkpoint et lifecycle du job.
|
||||||
|
|
||||||
|
## Package
|
||||||
|
|
||||||
|
```text
|
||||||
|
package : ksp-app-backfill-desk
|
||||||
|
lib : ksp_app_backfill_desk_lib
|
||||||
|
bin : ksp-app-backfill-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Le binaire est un launcher mince. La bibliothèque applicative possède le bootstrap, l'état Rust single-run, les DTOs Tauri, les commands et le bridge latest-value vers le frontend.
|
||||||
|
|
||||||
|
## Composition
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib -> composite, profils, secrets et runtime packagé
|
||||||
|
ksp-core-lib -> erreurs, Pubkey et registre Program IDs
|
||||||
|
ksp-onchain-transport-lib -> HTTP pool, rôles, admission, retry/rate-limit
|
||||||
|
ksp-store-lib -> façade Store backend-neutral
|
||||||
|
ksp-job-api -> lifecycle Job commun
|
||||||
|
ksp-job-backfill-lib -> request/runtime/checkpoint/snapshots Backfill
|
||||||
|
ksp-logging-lib -> logging/tracing applicatif
|
||||||
|
ksp-app-backfill-desk -> orchestration Tauri + projections sûres
|
||||||
|
```
|
||||||
|
|
||||||
|
L'application ne dépend pas directement de `ksp-store-api`, `ksp-store-postgres-lib`, `tokio-postgres`, `reqwest`, `tonic` ou `yellowstone-grpc-proto`.
|
||||||
|
|
||||||
|
## Capacités
|
||||||
|
|
||||||
|
La surface utilisateur comprend :
|
||||||
|
|
||||||
|
- quatre scopes : latest address, before address, after address et explicit signatures ;
|
||||||
|
- engagements `finalized` et `confirmed` ;
|
||||||
|
- sélection d'un rôle HTTP logique compatible avec `getSignaturesForAddress` et `getTransaction` ;
|
||||||
|
- bornes page/candidats/concurrence validées côté Rust par le contrat Backfill ;
|
||||||
|
- validation de la requête avant Start ;
|
||||||
|
- un seul run actif ;
|
||||||
|
- monitoring latest-value par événement `ksp-backfill-status` avec resynchronisation explicite ;
|
||||||
|
- Cancel ciblé par JobId backend et idempotent ;
|
||||||
|
- Resume in-session lorsqu'un checkpoint terminal est disponible ;
|
||||||
|
- autocomplete libre des Program IDs provenant du registre canonique `ksp-core-lib`.
|
||||||
|
|
||||||
|
L'autocomplete n'est jamais une allow-list : une adresse Solana valide non enregistrée reste saisissable.
|
||||||
|
|
||||||
|
## Checkpoint et Resume
|
||||||
|
|
||||||
|
Le checkpoint concret ne traverse jamais IPC. Le backend conserve uniquement en mémoire Rust le dernier terminal compatible et la requête nécessaire à une reprise dans la même session.
|
||||||
|
|
||||||
|
Un Resume crée un nouveau JobId backend. `ksp-job-backfill-lib` valide le checkpoint contre la requête d'origine puis le réémet pour ce nouveau Job sans modifier le scope fingerprint ni la frontier. Il n'existe pas de checkpoint durable de Backfill Desk après redémarrage de l'application.
|
||||||
|
|
||||||
|
## Config et réseaux
|
||||||
|
|
||||||
|
Le composite dédié est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cfg.composite.ksp-app-backfill-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Les profils committed couvrent `mainnet`, `devnet` et `testnet`. Mainnet est le profil par défaut. Transport et Store doivent sélectionner exactement le même réseau avant que la composition soit déclarée ready.
|
||||||
|
|
||||||
|
Les profils Store committed utilisent actuellement TLS PostgreSQL `disabled`; cette politique de profil n'enlève pas le support `verify_full` du Store/schema.
|
||||||
|
|
||||||
|
Le frontend ne reçoit jamais URI Store, URL RPC, credential provider, secret Config ou backend physique.
|
||||||
|
|
||||||
|
## Monitoring et sécurité
|
||||||
|
|
||||||
|
Le monitoring expose des compteurs et codes sûrs : lifecycle, phase, boundary, candidats, entités/observations, missing/conflicts/holes, concurrence maximale, frontier contiguë, présence de checkpoint et éventuel domain/code d'échec.
|
||||||
|
|
||||||
|
Il n'expose pas :
|
||||||
|
|
||||||
|
- payload RAW ;
|
||||||
|
- adresse ou signatures de campagne ;
|
||||||
|
- checkpoint/cursor concret ;
|
||||||
|
- endpoint URL ou credential ;
|
||||||
|
- contexte d'erreur arbitraire.
|
||||||
|
|
||||||
|
Le frontend ne possède ni accès direct filesystem/réseau, ni persistence navigateur de données Backfill. Les capabilities Tauri guest restent `core:default` et `tracing:default`.
|
||||||
|
|
||||||
|
## Ports et développement
|
||||||
|
|
||||||
|
```text
|
||||||
|
Vite HTTP : 1436
|
||||||
|
Vite WS : 1437
|
||||||
|
```
|
||||||
|
|
||||||
|
Lancement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-backfill-desk && cargo tauri dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Build production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-backfill-desk && cargo tauri build)
|
||||||
|
```
|
||||||
|
|
||||||
|
Voir également [`USAGE.md`](USAGE.md), [`../ksp-job-backfill-lib/README.md`](../ksp-job-backfill-lib/README.md), [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) et [`../../docs/validation/024-V0_3_7_BACKFILL_DESK.md`](../../docs/validation/024-V0_3_7_BACKFILL_DESK.md).
|
||||||
145
crates/ksp-app-backfill-desk/USAGE.md
Normal file
145
crates/ksp-app-backfill-desk/USAGE.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<!-- file: crates/ksp-app-backfill-desk/USAGE.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Utilisation de `ksp-app-backfill-desk`
|
||||||
|
|
||||||
|
## 1. Lancement
|
||||||
|
|
||||||
|
Depuis la racine du workspace :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-backfill-desk && cargo tauri dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le runtime charge le composite Backfill Desk via `ksp-config-lib`, construit le pool HTTP, ouvre Store puis vérifie que les deux surfaces utilisent le même réseau avant d'autoriser une campagne.
|
||||||
|
|
||||||
|
## 2. Choix du profil et readiness
|
||||||
|
|
||||||
|
Le composite `cfg.composite.ksp-app-backfill-desk` utilise Mainnet par défaut. Devnet et Testnet restent disponibles via la sélection Config du profil composite.
|
||||||
|
|
||||||
|
Avant Start, l'écran indique séparément :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport ready
|
||||||
|
Store ready
|
||||||
|
Network coherent
|
||||||
|
Composition ready
|
||||||
|
```
|
||||||
|
|
||||||
|
Une composition non ready doit être corrigée côté Config/runtime ; le frontend ne peut pas fournir une URL RPC, une URI Store ou un secret pour contourner ces contrôles.
|
||||||
|
|
||||||
|
## 3. Adresse et autocomplete Program IDs
|
||||||
|
|
||||||
|
Le champ adresse accepte toute Pubkey Solana valide. Le `datalist` propose en complément les Program IDs du registre canonique `ksp-core-lib` avec leurs metadata publiques.
|
||||||
|
|
||||||
|
Choisir une proposition remplit simplement le champ ; une adresse absente du dataset reste autorisée. Le dataset n'est pas une allow-list.
|
||||||
|
|
||||||
|
## 4. Scopes
|
||||||
|
|
||||||
|
Les scopes disponibles sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
latest_address
|
||||||
|
before_address
|
||||||
|
after_address
|
||||||
|
explicit_signatures
|
||||||
|
```
|
||||||
|
|
||||||
|
`latest_address` recherche l'historique récent d'une adresse. `before_address` et `after_address` ajoutent une signature d'ancrage selon la sémantique Backfill. `explicit_signatures` traite une liste explicite de signatures et n'accepte pas `min_context_slot`.
|
||||||
|
|
||||||
|
Les signatures et adresses sont revalidées côté Rust ; leur présence dans le formulaire ne les rend pas persistantes dans le frontend.
|
||||||
|
|
||||||
|
## 5. Engagement, route HTTP et bornes
|
||||||
|
|
||||||
|
Les engagements disponibles sont `finalized` et `confirmed`.
|
||||||
|
|
||||||
|
Le sélecteur HTTP expose uniquement des rôles logiques que le backend a vérifiés compatibles avec les deux méthodes nécessaires au Backfill. Le rôle pool permet au Transport de choisir/rerouter entre endpoints selon ses propres règles ; les rôles ciblés permettent de sélectionner une route logique spécifique sans exposer l'URL physique.
|
||||||
|
|
||||||
|
Les bornes de campagne comprennent notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
page size
|
||||||
|
max pages
|
||||||
|
max candidates
|
||||||
|
hydration concurrency
|
||||||
|
min context slot
|
||||||
|
```
|
||||||
|
|
||||||
|
Les limites maximales proviennent de `ksp-job-backfill-lib` et sont revalidées lors de la conversion vers `BackfillRequest`.
|
||||||
|
|
||||||
|
## 6. Valider puis démarrer
|
||||||
|
|
||||||
|
**Valider la requête** projette un résumé sûr de la campagne sans démarrer le job.
|
||||||
|
|
||||||
|
**Démarrer** crée ensuite un JobId backend et installe le handle de contrôle avant le spawn asynchrone. Un second Start est refusé tant qu'un run est actif, y compris pendant `cancelling`.
|
||||||
|
|
||||||
|
## 7. Monitoring latest-value
|
||||||
|
|
||||||
|
La carte de monitoring est alimentée par les snapshots latest-value du runtime. Le backend émet `ksp-backfill-status`; **Resynchroniser** appelle également `backfill_status` afin de récupérer explicitement le dernier état complet.
|
||||||
|
|
||||||
|
Les informations utiles incluent :
|
||||||
|
|
||||||
|
- lifecycle et phase ;
|
||||||
|
- scope et discovery boundary ;
|
||||||
|
- candidats selected/admitted/finished ;
|
||||||
|
- entités et observations inserted/existing ;
|
||||||
|
- purged/missing/conflicts/holes/cancelled candidates ;
|
||||||
|
- maximum in flight ;
|
||||||
|
- contiguous completed ;
|
||||||
|
- présence d'un checkpoint ;
|
||||||
|
- domain/code stable d'échec éventuel.
|
||||||
|
|
||||||
|
Le monitoring ne doit pas être interprété depuis les logs texte.
|
||||||
|
|
||||||
|
## 8. Annuler
|
||||||
|
|
||||||
|
**Annuler** envoie le JobId actuellement affiché. Cette cible protège un nouveau run contre une requête Cancel retardée destinée au run précédent.
|
||||||
|
|
||||||
|
Le premier Cancel pré-terminal demande une annulation coopérative. Une répétition est idempotente. Le lifecycle peut passer par `cancelling`; les opérations déjà admises vers la persistence ne sont pas présentées comme brutalement interrompues.
|
||||||
|
|
||||||
|
## 9. Reprendre
|
||||||
|
|
||||||
|
**Reprendre** est disponible uniquement lorsqu'un snapshot terminal retenu contient un checkpoint. La commande n'accepte aucun checkpoint ou payload de campagne depuis le frontend.
|
||||||
|
|
||||||
|
Le backend :
|
||||||
|
|
||||||
|
1. exige qu'aucun run ne soit actif ;
|
||||||
|
2. récupère la requête terminale Rust-only ;
|
||||||
|
3. revalide le réseau Store et le rôle HTTP courant ;
|
||||||
|
4. génère un nouveau JobId ;
|
||||||
|
5. demande à `ksp-job-backfill-lib` de réémettre le checkpoint pour ce JobId ;
|
||||||
|
6. relance le même runtime/monitoring.
|
||||||
|
|
||||||
|
La reprise est limitée à la session courante. Fermer l'application supprime ce checkpoint applicatif en mémoire.
|
||||||
|
|
||||||
|
## 10. Fermeture de l'application
|
||||||
|
|
||||||
|
La fermeture de la fenêtre principale bloque d'abord tout nouveau Start, tente une annulation coopérative du run actif puis ferme Store avant l'exit.
|
||||||
|
|
||||||
|
Le shutdown applicatif n'est pas un mécanisme de checkpoint durable ou de reprise après crash.
|
||||||
|
|
||||||
|
## 11. Données qui ne traversent pas le frontend
|
||||||
|
|
||||||
|
Backfill Desk ne projette jamais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
URI Store
|
||||||
|
URL RPC
|
||||||
|
credentials/secrets
|
||||||
|
payload RAW
|
||||||
|
checkpoint/cursor concret
|
||||||
|
handles Store/Transport/Job
|
||||||
|
contexte d'erreur arbitraire
|
||||||
|
```
|
||||||
|
|
||||||
|
Les actions utilisateur et transitions sont instrumentées via Logging KSP sans journaliser adresse, signature ou payload métier.
|
||||||
|
|
||||||
|
## 12. Runtime packagé
|
||||||
|
|
||||||
|
Le build Tauri embarque les documents Config/schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable et conserve les Config utilisateur existantes ; `.env` n'est jamais embarqué.
|
||||||
|
|
||||||
|
Le build production est :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd crates/ksp-app-backfill-desk && cargo tauri build)
|
||||||
|
```
|
||||||
12
crates/ksp-app-backfill-desk/build.rs
Normal file
12
crates/ksp-app-backfill-desk/build.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/build.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Build script for the KSP Backfill desktop application.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
13
crates/ksp-app-backfill-desk/capabilities/default.json
Normal file
13
crates/ksp-app-backfill-desk/capabilities/default.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for KSP Backfill Desk",
|
||||||
|
"windows": [
|
||||||
|
"splash",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"tracing:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
25
crates/ksp-app-backfill-desk/frontend/fonts/README.md
Normal file
25
crates/ksp-app-backfill-desk/frontend/fonts/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!-- file: crates/ksp-app-backfill-desk/frontend/fonts/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Fonts du splash Backfill Desk
|
||||||
|
|
||||||
|
Backfill 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-backfill-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-backfill-desk/frontend/imgs/logo.png
Normal file
BIN
crates/ksp-app-backfill-desk/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/ksp-app-backfill-desk/frontend/imgs/splash.png
Normal file
BIN
crates/ksp-app-backfill-desk/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
231
crates/ksp-app-backfill-desk/frontend/main.html
Normal file
231
crates/ksp-app-backfill-desk/frontend/main.html
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<!-- file: crates/ksp-app-backfill-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>Backfill Desk — Backfill</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">Backfill 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">Backfill</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 Backfill Desk">
|
||||||
|
<button class="nav-link text-start active" type="button" data-view="backfill">
|
||||||
|
<i class="fa-solid fa-clock-rotate-left me-2" aria-hidden="true"></i>Backfill
|
||||||
|
</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="backfill">
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">Backfill</h1>
|
||||||
|
<p class="text-body-secondary mb-0">Mainnet par défaut, transport HTTP sélectionnable et mapping de campagne validé côté Rust avant tout démarrage de job.</p>
|
||||||
|
</div>
|
||||||
|
<span id="appVersionBadge" class="badge text-bg-secondary">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-xl-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">Transport HTTP</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<label for="backfillHttpRoute" class="form-label">Route de lecture</label>
|
||||||
|
<select id="backfillHttpRoute" class="form-select" disabled>
|
||||||
|
<option value="">Chargement des routes…</option>
|
||||||
|
</select>
|
||||||
|
<div id="backfillHttpRouteHelp" class="form-text">Le choix reste logique : aucune URL ni clé provider n'est exposée au frontend.</div>
|
||||||
|
<dl class="row mt-4 mb-0 app-runtime-list">
|
||||||
|
<dt class="col-sm-5">Réseau</dt><dd id="backfillNetwork" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Transport</dt><dd id="backfillTransportReady" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Store</dt><dd id="backfillStoreReady" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Composition</dt><dd id="backfillCompositionReady" class="col-sm-7">—</dd>
|
||||||
|
</dl>
|
||||||
|
<div id="backfillOptionsDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-8">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">Campagne Backfill</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="backfillCampaignForm" novalidate>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<label for="backfillScopeKind" class="form-label">Scope</label>
|
||||||
|
<select id="backfillScopeKind" class="form-select" disabled></select>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<label for="backfillCommitment" class="form-label">Commitment</label>
|
||||||
|
<select id="backfillCommitment" class="form-select" disabled></select>
|
||||||
|
</div>
|
||||||
|
<div id="backfillAddressGroup" class="col-12">
|
||||||
|
<label for="backfillAddress" class="form-label">Adresse Solana</label>
|
||||||
|
<input id="backfillAddress" class="form-control font-monospace" type="text" list="backfillProgramIds" autocomplete="off" spellcheck="false" aria-describedby="backfillProgramIdAutocompleteHelp">
|
||||||
|
<datalist id="backfillProgramIds"></datalist>
|
||||||
|
<div id="backfillProgramIdAutocompleteHelp" class="form-text">Suggestions issues du registre canonique KSP ; toute adresse Solana valide reste acceptée.</div>
|
||||||
|
</div>
|
||||||
|
<div id="backfillAnchorGroup" class="col-12" hidden>
|
||||||
|
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
|
||||||
|
<input id="backfillAnchorSignature" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div id="backfillExplicitSignaturesGroup" class="col-12" hidden>
|
||||||
|
<label for="backfillExplicitSignatures" class="form-label">Signatures explicites</label>
|
||||||
|
<textarea id="backfillExplicitSignatures" class="form-control font-monospace" rows="5" autocomplete="off" spellcheck="false"></textarea>
|
||||||
|
<div id="backfillExplicitSignaturesHelp" class="form-text">Une signature par ligne.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-xxl-3">
|
||||||
|
<label for="backfillPageSize" class="form-label">Page size</label>
|
||||||
|
<input id="backfillPageSize" class="form-control" type="number" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-xxl-3">
|
||||||
|
<label for="backfillMaxPages" class="form-label">Max pages</label>
|
||||||
|
<input id="backfillMaxPages" class="form-control" type="number" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-xxl-3">
|
||||||
|
<label for="backfillMaxCandidates" class="form-label">Max candidates</label>
|
||||||
|
<input id="backfillMaxCandidates" class="form-control" type="number" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-xxl-3">
|
||||||
|
<label for="backfillHydrationConcurrency" class="form-label">Concurrence</label>
|
||||||
|
<input id="backfillHydrationConcurrency" class="form-control" type="number" step="1">
|
||||||
|
</div>
|
||||||
|
<div id="backfillMinContextSlotGroup" class="col-lg-6">
|
||||||
|
<label for="backfillMinContextSlot" class="form-label">Min context slot</label>
|
||||||
|
<input id="backfillMinContextSlot" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" placeholder="optionnel">
|
||||||
|
<div class="form-text">Chaîne décimale validée côté Rust pour préserver la précision u64.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
|
||||||
|
<button id="validateBackfillRequest" class="btn btn-outline-primary" type="submit" disabled>
|
||||||
|
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
|
||||||
|
</button>
|
||||||
|
<button id="startBackfillRequest" class="btn btn-primary" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-play me-2" aria-hidden="true"></i>Démarrer le backfill
|
||||||
|
</button>
|
||||||
|
<span class="text-body-secondary small">Un seul run peut être actif ; le monitoring latest-value est coalescé côté backend.</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>
|
||||||
|
<div id="backfillRunStatusCard" class="card shadow-sm mt-4" hidden>
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
|
||||||
|
<span class="fw-semibold">Monitoring latest-value</span>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<button id="cancelBackfillRun" class="btn btn-outline-danger btn-sm" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-ban me-2" aria-hidden="true"></i>Annuler
|
||||||
|
</button>
|
||||||
|
<button id="resumeBackfillRun" class="btn btn-outline-success btn-sm" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-forward-step me-2" aria-hidden="true"></i>Reprendre
|
||||||
|
</button>
|
||||||
|
<button id="refreshBackfillStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||||
|
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Resynchroniser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
||||||
|
<div id="backfillResumeFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
||||||
|
<div id="backfillTerminalSummary" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
||||||
|
<dl class="row mb-0 app-runtime-list">
|
||||||
|
<dt class="col-sm-5">Job</dt><dd id="runStatusJobId" class="col-sm-7 font-monospace">—</dd>
|
||||||
|
<dt class="col-sm-5">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Phase</dt><dd id="runStatusPhase" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Séquence</dt><dd id="runStatusSequence" class="col-sm-7 font-monospace">—</dd>
|
||||||
|
<dt class="col-sm-5">Scope</dt><dd id="runStatusScope" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Boundary</dt><dd id="runStatusBoundary" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Candidates sel./adm./fin.</dt><dd id="runStatusCandidates" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Frontier / holes</dt><dd id="runStatusFrontier" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">RAW ins./exist./purged</dt><dd id="runStatusEntities" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Observations ins./exist.</dt><dd id="runStatusObservations" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Anomalies</dt><dd id="runStatusAnomalies" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Max in-flight</dt><dd id="runStatusConcurrency" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Checkpoint</dt><dd id="runStatusCheckpoint" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Failure</dt><dd id="runStatusFailure" class="col-sm-7">—</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="backfillRequestPreview" class="card bg-body-tertiary border-0 mt-3" hidden>
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 mb-3">Projection backend sûre</h2>
|
||||||
|
<dl class="row mb-0 app-runtime-list">
|
||||||
|
<dt class="col-sm-5">Réseau</dt><dd id="validatedNetwork" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Route HTTP</dt><dd id="validatedHttpRole" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Commitment</dt><dd id="validatedCommitment" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Scope</dt><dd id="validatedScopeKind" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Signatures explicites</dt><dd id="validatedExplicitSignatureCount" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">page/pages/candidates/concurrence</dt><dd id="validatedBounds" class="col-sm-7">—</dd>
|
||||||
|
<dt class="col-sm-5">Min context slot</dt><dd id="validatedMinContextSlot" class="col-sm-7">—</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section data-view-panel="diagnostics" hidden>
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">Diagnostics</h1>
|
||||||
|
<p class="text-body-secondary mb-0">État sûr du scaffold Config, Logging et desktop.</p>
|
||||||
|
</div>
|
||||||
|
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||||
|
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||||
|
</button>
|
||||||
|
</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 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>
|
||||||
113
crates/ksp-app-backfill-desk/frontend/sass/_app.scss
Normal file
113
crates/ksp-app-backfill-desk/frontend/sass/_app.scss
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/frontend/sass/_app.scss
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
$app-header-height: 72px;
|
||||||
|
$app-footer-height: 42px;
|
||||||
|
$app-sidebar-width: 250px;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
height: $app-header-height;
|
||||||
|
z-index: 1020;
|
||||||
|
border-bottom: 2px solid rgba($primary, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto 0 0;
|
||||||
|
height: $app-footer-height;
|
||||||
|
z-index: 1020;
|
||||||
|
border-top: 2px solid rgba($primary, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
position: relative;
|
||||||
|
height: calc(100vh - $app-header-height - $app-footer-height);
|
||||||
|
margin-top: $app-header-height;
|
||||||
|
margin-bottom: $app-footer-height;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.app-sidebar {
|
||||||
|
width: 190px;
|
||||||
|
min-width: 190px;
|
||||||
|
max-width: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
160
crates/ksp-app-backfill-desk/frontend/sass/_bootswatch.scss
Normal file
160
crates/ksp-app-backfill-desk/frontend/sass/_bootswatch.scss
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/ksp-app-backfill-desk/frontend/sass/_fontawesome.scss
Normal file
19
crates/ksp-app-backfill-desk/frontend/sass/_fontawesome.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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-backfill-desk/frontend/sass/_simplebar.scss
Normal file
248
crates/ksp-app-backfill-desk/frontend/sass/_simplebar.scss
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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-backfill-desk/frontend/sass/_variables.scss
Normal file
95
crates/ksp-app-backfill-desk/frontend/sass/_variables.scss
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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-backfill-desk/frontend/sass/main.scss
Normal file
10
crates/ksp-app-backfill-desk/frontend/sass/main.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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-backfill-desk/frontend/sass/splash.scss
Normal file
137
crates/ksp-app-backfill-desk/frontend/sass/splash.scss
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/frontend/sass/splash.scss
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Dos Amazigh";
|
||||||
|
src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::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-backfill-desk/frontend/splash.html
Normal file
24
crates/ksp-app-backfill-desk/frontend/splash.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!-- file: crates/ksp-app-backfill-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 Backfill Desk — Chargement</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="splash-container" style="opacity: 0;">
|
||||||
|
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Backfill Desk">
|
||||||
|
<div id="app-name">Backfill</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-backfill-desk/frontend/ts/frontend_log.ts
Normal file
110
crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_backfill_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-backfill-desk/frontend/ts/invoke.ts
Normal file
17
crates/ksp-app-backfill-desk/frontend/ts/invoke.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
778
crates/ksp-app-backfill-desk/frontend/ts/main.ts
Normal file
778
crates/ksp-app-backfill-desk/frontend/ts/main.ts
Normal file
@@ -0,0 +1,778 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||||
|
// version: 8
|
||||||
|
|
||||||
|
import "bootstrap";
|
||||||
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
|
import "simplebar";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
|
||||||
|
import type { BackfillCancelResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts";
|
||||||
|
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
|
||||||
|
import type { BackfillResumeResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts";
|
||||||
|
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
|
||||||
|
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts";
|
||||||
|
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
|
||||||
|
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
|
||||||
|
import type { ProgramIdAutocompleteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts";
|
||||||
|
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.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 = "backfill" | "diagnostics";
|
||||||
|
|
||||||
|
const viewTitles: Record<ViewId, string> = {
|
||||||
|
backfill: "Backfill",
|
||||||
|
diagnostics: "Diagnostics",
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitmentLabels: Record<string, string> = {
|
||||||
|
confirmed: "confirmed",
|
||||||
|
finalized: "finalized",
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopeLabels: Record<string, string> = {
|
||||||
|
latest_address: "Adresse — historique le plus récent",
|
||||||
|
before_address: "Adresse — avant une signature",
|
||||||
|
after_address: "Adresse — après une signature",
|
||||||
|
explicit_signatures: "Signatures explicites",
|
||||||
|
};
|
||||||
|
|
||||||
|
let compositionReadyForStart = false;
|
||||||
|
let currentRunStatus: BackfillRunStatusDto | null = null;
|
||||||
|
let runStartAccepted = false;
|
||||||
|
let resumeInFlight = false;
|
||||||
|
|
||||||
|
function isViewId(value: string | undefined): value is ViewId {
|
||||||
|
return value === "backfill" || 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 => {
|
||||||
|
const active = button.dataset.view === viewId;
|
||||||
|
button.classList.toggle("active", active);
|
||||||
|
button.setAttribute("aria-current", active ? "page" : "false");
|
||||||
|
});
|
||||||
|
const header = document.querySelector<HTMLElement>("#headerViewTitle");
|
||||||
|
if (header) {
|
||||||
|
header.textContent = viewTitles[viewId];
|
||||||
|
}
|
||||||
|
document.title = `Backfill Desk — ${viewTitles[viewId]}`;
|
||||||
|
frontendTrace("main", "Backfill Desk view DOM updated", { viewId, source });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindFrontendInteractions(): void {
|
||||||
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
event => {
|
||||||
|
const source = event.target;
|
||||||
|
if (!(source instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
|
||||||
|
if (!control) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk frontend control clicked", {
|
||||||
|
controlId: control.id || null,
|
||||||
|
role: control.getAttribute("role"),
|
||||||
|
tagName: control.tagName.toLowerCase(),
|
||||||
|
viewId: control.dataset.view ?? null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
frontendTrace("main", "Backfill Desk frontend control interaction logger installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNavigation(): void {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const viewId = button.dataset.view;
|
||||||
|
frontendDebug("main", "Backfill Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
|
||||||
|
if (isViewId(viewId)) {
|
||||||
|
activateView(viewId, "user");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Backfill Desk navigation handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCodeSelect(selectId: string, values: string[], labels: Record<string, string>): void {
|
||||||
|
const select = document.querySelector<HTMLSelectElement>(`#${selectId}`);
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous = select.value;
|
||||||
|
select.replaceChildren();
|
||||||
|
for (const value of values) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = value;
|
||||||
|
option.textContent = labels[value] ?? value;
|
||||||
|
select.append(option);
|
||||||
|
}
|
||||||
|
select.disabled = values.length === 0;
|
||||||
|
if (values.includes(previous)) {
|
||||||
|
select.value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
|
||||||
|
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||||
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.min = "1";
|
||||||
|
input.max = maximum.toString();
|
||||||
|
if (input.value === "") {
|
||||||
|
input.value = defaultValue.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStartButton(): void {
|
||||||
|
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||||||
|
if (startButton) {
|
||||||
|
startButton.disabled = !compositionReadyForStart || runStartAccepted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderProgramIdAutocomplete(options: ProgramIdAutocompleteOptionDto[]): void {
|
||||||
|
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
|
||||||
|
if (!datalist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
datalist.replaceChildren();
|
||||||
|
for (const program of options) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = program.programId;
|
||||||
|
option.label = `${program.name} — ${program.code}`;
|
||||||
|
option.dataset.code = program.code;
|
||||||
|
option.dataset.domain = program.domain;
|
||||||
|
option.dataset.family = program.family;
|
||||||
|
option.dataset.name = program.name;
|
||||||
|
option.dataset.protocol = program.protocol;
|
||||||
|
datalist.append(option);
|
||||||
|
}
|
||||||
|
const help = document.querySelector<HTMLElement>("#backfillProgramIdAutocompleteHelp");
|
||||||
|
if (help) {
|
||||||
|
help.textContent = `${options.length} Program ID KSP disponibles comme suggestions ; la saisie reste libre.`;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk Program ID autocomplete dataset rendered", { optionCount: options.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedProgramAutocompleteEntry(address: string): HTMLOptionElement | null {
|
||||||
|
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
|
||||||
|
if (!datalist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const option of Array.from(datalist.options)) {
|
||||||
|
if (option.value === address) {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
|
||||||
|
if (typeof caughtError === "object" && caughtError !== null) {
|
||||||
|
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
|
||||||
|
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
|
||||||
|
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
|
||||||
|
renderProgramIdAutocomplete(options.programIdOptions);
|
||||||
|
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
|
||||||
|
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
|
||||||
|
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
|
||||||
|
applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
|
||||||
|
applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
|
||||||
|
applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
|
||||||
|
const signatureHelp = document.querySelector<HTMLElement>("#backfillExplicitSignaturesHelp");
|
||||||
|
if (signatureHelp) {
|
||||||
|
signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}–${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
|
||||||
|
}
|
||||||
|
const validateButton = document.querySelector<HTMLButtonElement>("#validateBackfillRequest");
|
||||||
|
if (validateButton) {
|
||||||
|
validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
|
||||||
|
}
|
||||||
|
updateCampaignScopeFields();
|
||||||
|
frontendTrace("main", "Backfill Desk campaign contract rendered", {
|
||||||
|
commitmentCount: options.commitments.length,
|
||||||
|
compositionReady: options.compositionReady,
|
||||||
|
scopeCount: options.scopeKinds.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentScopeKind(): string {
|
||||||
|
const select = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||||||
|
return select?.value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCampaignScopeFields(): void {
|
||||||
|
const scopeKind = currentScopeKind();
|
||||||
|
const addressGroup = document.querySelector<HTMLElement>("#backfillAddressGroup");
|
||||||
|
const anchorGroup = document.querySelector<HTMLElement>("#backfillAnchorGroup");
|
||||||
|
const signaturesGroup = document.querySelector<HTMLElement>("#backfillExplicitSignaturesGroup");
|
||||||
|
const minContextSlotGroup = document.querySelector<HTMLElement>("#backfillMinContextSlotGroup");
|
||||||
|
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||||||
|
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||||||
|
const explicitScope = scopeKind === "explicit_signatures";
|
||||||
|
if (addressGroup) {
|
||||||
|
addressGroup.hidden = !addressScope;
|
||||||
|
}
|
||||||
|
if (anchorGroup) {
|
||||||
|
anchorGroup.hidden = !anchoredScope;
|
||||||
|
}
|
||||||
|
if (signaturesGroup) {
|
||||||
|
signaturesGroup.hidden = !explicitScope;
|
||||||
|
}
|
||||||
|
if (minContextSlotGroup) {
|
||||||
|
minContextSlotGroup.hidden = explicitScope;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
|
||||||
|
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||||
|
if (status) {
|
||||||
|
status.hidden = true;
|
||||||
|
status.textContent = "";
|
||||||
|
status.classList.remove("alert-success", "alert-danger");
|
||||||
|
status.classList.add("alert-secondary");
|
||||||
|
}
|
||||||
|
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||||||
|
if (summary) {
|
||||||
|
summary.hidden = true;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
function numericInputValue(inputId: string): number {
|
||||||
|
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||||
|
if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return input.valueAsNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rawOptionalInput(inputId: string, enabled: boolean): string | null {
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||||
|
if (!input || input.value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function explicitSignatureLines(): string[] {
|
||||||
|
const textarea = document.querySelector<HTMLTextAreaElement>("#backfillExplicitSignatures");
|
||||||
|
if (!textarea || currentScopeKind() !== "explicit_signatures") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBackfillStartRequest(): BackfillStartRequestDto {
|
||||||
|
const scopeKind = currentScopeKind();
|
||||||
|
const role = document.querySelector<HTMLSelectElement>("#backfillHttpRoute")?.value ?? "";
|
||||||
|
const commitment = document.querySelector<HTMLSelectElement>("#backfillCommitment")?.value ?? "";
|
||||||
|
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||||||
|
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||||||
|
const minContextSlotEnabled = scopeKind !== "explicit_signatures";
|
||||||
|
return {
|
||||||
|
address: rawOptionalInput("backfillAddress", addressScope),
|
||||||
|
anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
|
||||||
|
commitment,
|
||||||
|
explicitSignatures: explicitSignatureLines(),
|
||||||
|
hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
|
||||||
|
httpRole: role,
|
||||||
|
maxCandidates: numericInputValue("backfillMaxCandidates"),
|
||||||
|
maxPages: numericInputValue("backfillMaxPages"),
|
||||||
|
minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
|
||||||
|
pageSize: numericInputValue("backfillPageSize"),
|
||||||
|
scopeKind,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
validatedNetwork: preview.network,
|
||||||
|
validatedHttpRole: preview.httpRole,
|
||||||
|
validatedCommitment: preview.commitment,
|
||||||
|
validatedScopeKind: preview.scopeKind,
|
||||||
|
validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
|
||||||
|
validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
|
||||||
|
validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
|
||||||
|
};
|
||||||
|
for (const [id, value] of Object.entries(values)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||||||
|
if (summary) {
|
||||||
|
summary.hidden = false;
|
||||||
|
}
|
||||||
|
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||||
|
if (status) {
|
||||||
|
status.hidden = false;
|
||||||
|
status.classList.remove("alert-secondary", "alert-danger");
|
||||||
|
status.classList.add("alert-success");
|
||||||
|
status.textContent = "Requête valide. Aucun job n'a été démarré.";
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk validated request preview rendered", {
|
||||||
|
commitment: preview.commitment,
|
||||||
|
httpRole: preview.httpRole,
|
||||||
|
scopeKind: preview.scopeKind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRequestValidationError(caughtError: unknown): void {
|
||||||
|
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = safeCommandErrorText(caughtError, "Requête refusée par le backend.");
|
||||||
|
status.hidden = false;
|
||||||
|
status.classList.remove("alert-secondary", "alert-success");
|
||||||
|
status.classList.add("alert-danger");
|
||||||
|
status.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startBackfillRequest(): Promise<void> {
|
||||||
|
const request = buildBackfillStartRequest();
|
||||||
|
frontendDebug("main", "Backfill Desk campaign Start requested", {
|
||||||
|
commitment: request.commitment,
|
||||||
|
httpRole: request.httpRole,
|
||||||
|
scopeKind: request.scopeKind,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const started = await invokeKsp<BackfillStartResponseDto>("main", "backfill_start", { request });
|
||||||
|
runStartAccepted = true;
|
||||||
|
refreshStartButton();
|
||||||
|
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||||
|
if (status) {
|
||||||
|
status.hidden = false;
|
||||||
|
status.classList.remove("alert-secondary", "alert-danger");
|
||||||
|
status.classList.add("alert-success");
|
||||||
|
status.textContent = `Job ${started.jobId} démarré (${started.state}).`;
|
||||||
|
}
|
||||||
|
await syncBackfillStatus("start");
|
||||||
|
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
|
||||||
|
} catch (caughtError) {
|
||||||
|
renderRequestValidationError(caughtError);
|
||||||
|
frontendWarn("main", "Backfill Desk campaign Start failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateBackfillRequest(): Promise<void> {
|
||||||
|
const request = buildBackfillStartRequest();
|
||||||
|
frontendDebug("main", "Backfill Desk campaign validation requested", {
|
||||||
|
commitment: request.commitment,
|
||||||
|
httpRole: request.httpRole,
|
||||||
|
scopeKind: request.scopeKind,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const preview = await invokeKsp<BackfillRequestPreviewDto>("main", "backfill_validate_request", { request });
|
||||||
|
renderRequestPreview(preview);
|
||||||
|
frontendDebug("main", "Backfill Desk campaign validation completed", {
|
||||||
|
commitment: preview.commitment,
|
||||||
|
httpRole: preview.httpRole,
|
||||||
|
scopeKind: preview.scopeKind,
|
||||||
|
});
|
||||||
|
} catch (caughtError) {
|
||||||
|
renderRequestValidationError(caughtError);
|
||||||
|
frontendWarn("main", "Backfill Desk campaign validation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCampaignForm(): void {
|
||||||
|
const scope = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||||||
|
if (scope) {
|
||||||
|
scope.addEventListener("change", () => {
|
||||||
|
frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
|
||||||
|
updateCampaignScopeFields();
|
||||||
|
invalidateCampaignValidation("scope");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const form = document.querySelector<HTMLFormElement>("#backfillCampaignForm");
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener("input", () => invalidateCampaignValidation("input"));
|
||||||
|
form.addEventListener("submit", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
void validateBackfillRequest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const address = document.querySelector<HTMLInputElement>("#backfillAddress");
|
||||||
|
if (address) {
|
||||||
|
address.addEventListener("change", () => {
|
||||||
|
const selected = selectedProgramAutocompleteEntry(address.value);
|
||||||
|
frontendTrace("main", "Backfill Desk address autocomplete selection evaluated", {
|
||||||
|
programCode: selected?.dataset.code ?? null,
|
||||||
|
registryMatch: selected !== null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||||||
|
if (startButton) {
|
||||||
|
startButton.addEventListener("click", () => {
|
||||||
|
void startBackfillRequest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk campaign form handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||||
|
if (route.pooled) {
|
||||||
|
return `Pool multi-provider — ${route.providers.join(" + ")}`;
|
||||||
|
}
|
||||||
|
return route.providers.length === 1 ? `Provider — ${route.providers[0]}` : route.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
|
||||||
|
compositionReadyForStart = options.compositionReady;
|
||||||
|
renderCampaignContract(options);
|
||||||
|
refreshStartButton();
|
||||||
|
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||||||
|
if (select) {
|
||||||
|
const previous = select.value;
|
||||||
|
select.replaceChildren();
|
||||||
|
for (const route of options.httpRoutes) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = route.role;
|
||||||
|
option.textContent = routeLabel(route);
|
||||||
|
select.append(option);
|
||||||
|
}
|
||||||
|
select.disabled = !options.transportReady || options.httpRoutes.length === 0;
|
||||||
|
const reusable = options.httpRoutes.some(route => route.role === previous);
|
||||||
|
if (reusable) {
|
||||||
|
select.value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
backfillNetwork: options.configuredNetworks.length === 1 ? options.configuredNetworks[0] : options.configuredNetworks.join(", ") || "indisponible",
|
||||||
|
backfillTransportReady: options.transportReady ? "ready" : "non ready",
|
||||||
|
backfillStoreReady: options.storeReady ? "ready" : "non ready",
|
||||||
|
backfillCompositionReady: options.compositionReady ? "ready" : "non ready",
|
||||||
|
};
|
||||||
|
for (const [id, value] of Object.entries(values)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const diagnostic = document.querySelector<HTMLElement>("#backfillOptionsDiagnostic");
|
||||||
|
if (diagnostic) {
|
||||||
|
const error = options.transportDiagnostic ?? options.storeDiagnostic;
|
||||||
|
diagnostic.hidden = error === null;
|
||||||
|
diagnostic.textContent = error === null ? "" : `${error.domain}/${error.code}: ${error.message}`;
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk HTTP route options rendered", {
|
||||||
|
compositionReady: options.compositionReady,
|
||||||
|
routeCount: options.httpRoutes.length,
|
||||||
|
transportReady: options.transportReady,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBackfillOptions(source: "startup" | "user"): Promise<void> {
|
||||||
|
frontendDebug("main", "Backfill Desk HTTP route options load started", { source });
|
||||||
|
const options = await invokeKsp<BackfillDeskOptionsDto>("main", "backfill_options");
|
||||||
|
renderBackfillOptions(options);
|
||||||
|
frontendDebug("main", "Backfill Desk HTTP route options load completed", { source, routeCount: options.httpRoutes.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindBackfillRouteSelection(): void {
|
||||||
|
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.addEventListener("change", () => {
|
||||||
|
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
|
||||||
|
invalidateCampaignValidation("route");
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "cancel" | "event" | "resume" | "startup" | "start" | "user"): void {
|
||||||
|
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
|
||||||
|
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
|
||||||
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
|
currentRunStatus = status;
|
||||||
|
if (!status) {
|
||||||
|
runStartAccepted = false;
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.disabled = true;
|
||||||
|
}
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = true;
|
||||||
|
}
|
||||||
|
resumeInFlight = false;
|
||||||
|
refreshStartButton();
|
||||||
|
if (card) {
|
||||||
|
card.hidden = true;
|
||||||
|
}
|
||||||
|
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
|
||||||
|
if (terminalSummary) {
|
||||||
|
terminalSummary.hidden = true;
|
||||||
|
terminalSummary.textContent = "";
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk latest-value status cleared", { source });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runStartAccepted = status.active;
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.disabled = !status.active;
|
||||||
|
}
|
||||||
|
if (status.active) {
|
||||||
|
resumeInFlight = false;
|
||||||
|
}
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = !status.terminal || !status.checkpointPresent || resumeInFlight;
|
||||||
|
}
|
||||||
|
refreshStartButton();
|
||||||
|
if (card) {
|
||||||
|
card.hidden = false;
|
||||||
|
}
|
||||||
|
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
|
||||||
|
if (terminalSummary) {
|
||||||
|
terminalSummary.hidden = !status.terminal;
|
||||||
|
terminalSummary.classList.remove("alert-success", "alert-warning", "alert-danger", "alert-secondary");
|
||||||
|
if (status.terminal) {
|
||||||
|
const failure = status.failureDomain === null || status.failureCode === null ? null : `${status.failureDomain}/${status.failureCode}`;
|
||||||
|
if (status.state === "completed") {
|
||||||
|
terminalSummary.classList.add("alert-success");
|
||||||
|
terminalSummary.textContent = `Campagne terminée — ${status.contiguousCompleted} candidats contigus durablement traités.`;
|
||||||
|
} else if (status.state === "cancelled") {
|
||||||
|
terminalSummary.classList.add("alert-warning");
|
||||||
|
terminalSummary.textContent = `Campagne annulée coopérativement — checkpoint ${status.checkpointPresent ? "disponible" : "absent"}.`;
|
||||||
|
} else {
|
||||||
|
terminalSummary.classList.add("alert-danger");
|
||||||
|
terminalSummary.textContent = `Campagne terminée en échec${failure === null ? "" : ` — ${failure}`}.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
terminalSummary.classList.add("alert-secondary");
|
||||||
|
terminalSummary.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
runStatusJobId: status.jobId,
|
||||||
|
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
|
||||||
|
runStatusPhase: status.phase,
|
||||||
|
runStatusSequence: status.sequence,
|
||||||
|
runStatusScope: status.scopeKind,
|
||||||
|
runStatusBoundary: status.discoveryBoundary ?? "—",
|
||||||
|
runStatusCandidates: `${status.candidatesSelected} / ${status.candidatesAdmitted} / ${status.candidatesFinished}`,
|
||||||
|
runStatusFrontier: `${status.contiguousCompleted} (holes ${status.holes})`,
|
||||||
|
runStatusEntities: `${status.entitiesInserted} / ${status.entitiesExisting} / ${status.entitiesPurged}`,
|
||||||
|
runStatusObservations: `${status.observationsInserted} / ${status.observationsExisting}`,
|
||||||
|
runStatusAnomalies: `missing ${status.missing} / conflits ${status.conflicts} / annulés ${status.cancelledCandidates}`,
|
||||||
|
runStatusConcurrency: status.maximumInFlight.toString(),
|
||||||
|
runStatusCheckpoint: status.checkpointPresent ? "présent" : "absent",
|
||||||
|
runStatusFailure: status.failureDomain === null || status.failureCode === null ? "—" : `${status.failureDomain}/${status.failureCode}`,
|
||||||
|
};
|
||||||
|
for (const [id, value] of Object.entries(values)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk latest-value status rendered", {
|
||||||
|
active: status.active,
|
||||||
|
phase: status.phase,
|
||||||
|
sequence: status.sequence,
|
||||||
|
source,
|
||||||
|
state: status.state,
|
||||||
|
terminal: status.terminal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncBackfillStatus(source: "cancel" | "resume" | "startup" | "start" | "user"): Promise<void> {
|
||||||
|
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
|
||||||
|
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
|
||||||
|
renderBackfillRunStatus(status, source);
|
||||||
|
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeBackfillRun(): Promise<void> {
|
||||||
|
const status = currentRunStatus;
|
||||||
|
if (!status || !status.terminal || !status.checkpointPresent || resumeInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
|
resumeInFlight = true;
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = true;
|
||||||
|
}
|
||||||
|
frontendDebug("main", "Backfill Desk in-session Resume requested", { jobId: status.jobId, state: status.state });
|
||||||
|
try {
|
||||||
|
const response = await invokeKsp<BackfillResumeResponseDto>("main", "backfill_resume");
|
||||||
|
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.hidden = false;
|
||||||
|
feedback.textContent = `Checkpoint repris dans ${response.jobId} (${response.state}).`;
|
||||||
|
}
|
||||||
|
frontendInfo("main", "Backfill Desk in-session Resume accepted", {
|
||||||
|
accepted: response.accepted,
|
||||||
|
jobId: response.jobId,
|
||||||
|
state: response.state,
|
||||||
|
});
|
||||||
|
await syncBackfillStatus("resume");
|
||||||
|
} catch (caughtError) {
|
||||||
|
resumeInFlight = false;
|
||||||
|
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
|
||||||
|
resumeButton.disabled = false;
|
||||||
|
}
|
||||||
|
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.hidden = false;
|
||||||
|
feedback.classList.remove("alert-secondary", "alert-success");
|
||||||
|
feedback.classList.add("alert-danger");
|
||||||
|
feedback.textContent = safeCommandErrorText(caughtError, "La reprise du checkpoint a été refusée.");
|
||||||
|
}
|
||||||
|
frontendWarn("main", "Backfill Desk in-session Resume failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bindBackfillStatusMonitoring(): Promise<void> {
|
||||||
|
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
|
||||||
|
renderBackfillRunStatus(event.payload, "event");
|
||||||
|
});
|
||||||
|
const button = document.querySelector<HTMLButtonElement>("#refreshBackfillStatus");
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
frontendDebug("main", "Backfill Desk latest-value status refresh button clicked");
|
||||||
|
void syncBackfillStatus("user").catch(() => frontendWarn("main", "Backfill Desk latest-value status refresh failed"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
|
||||||
|
if (cancelButton) {
|
||||||
|
cancelButton.addEventListener("click", () => {
|
||||||
|
const status = currentRunStatus;
|
||||||
|
if (!status || !status.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelButton.disabled = true;
|
||||||
|
frontendDebug("main", "Backfill Desk cooperative cancellation requested", { jobId: status.jobId, state: status.state });
|
||||||
|
void invokeKsp<BackfillCancelResponseDto>("main", "backfill_cancel", { jobId: status.jobId })
|
||||||
|
.then(response => {
|
||||||
|
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.hidden = false;
|
||||||
|
feedback.textContent = response.accepted
|
||||||
|
? `Annulation coopérative acceptée pour ${response.jobId}.`
|
||||||
|
: `Aucune nouvelle annulation acceptée pour ${response.jobId} (${response.state}).`;
|
||||||
|
}
|
||||||
|
frontendInfo("main", "Backfill Desk cooperative cancellation response received", {
|
||||||
|
accepted: response.accepted,
|
||||||
|
jobId: response.jobId,
|
||||||
|
state: response.state,
|
||||||
|
});
|
||||||
|
return syncBackfillStatus("cancel");
|
||||||
|
})
|
||||||
|
.catch(caughtError => {
|
||||||
|
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.hidden = false;
|
||||||
|
feedback.classList.remove("alert-secondary", "alert-success");
|
||||||
|
feedback.classList.add("alert-danger");
|
||||||
|
feedback.textContent = safeCommandErrorText(caughtError, "La demande d'annulation a été refusée.");
|
||||||
|
}
|
||||||
|
frontendWarn("main", "Backfill Desk cooperative cancellation failed");
|
||||||
|
if (currentRunStatus?.active) {
|
||||||
|
cancelButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.addEventListener("click", () => {
|
||||||
|
void resumeBackfillRun();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk latest-value monitoring, cancellation and Resume handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRuntimeStatus(status: ShellStatusDto): void {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
runtimeVersion: status.applicationVersion,
|
||||||
|
runtimeShellPhase: status.shellPhase,
|
||||||
|
runtimeConfigDocuments: status.configDocumentCount.toString(),
|
||||||
|
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback transitoire",
|
||||||
|
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
|
||||||
|
};
|
||||||
|
for (const [id, value] of Object.entries(values)) {
|
||||||
|
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const badge = document.querySelector<HTMLElement>("#appVersionBadge");
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = status.applicationVersion;
|
||||||
|
}
|
||||||
|
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", "Backfill Desk runtime status rendered", {
|
||||||
|
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||||
|
configDocumentCount: status.configDocumentCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRuntimeStatus(source: "startup" | "user"): Promise<void> {
|
||||||
|
frontendDebug("main", "Backfill Desk runtime status load started", { source });
|
||||||
|
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
|
||||||
|
renderRuntimeStatus(status);
|
||||||
|
frontendDebug("main", "Backfill Desk runtime status load completed", { source });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRuntimeStatusRefresh(): void {
|
||||||
|
const button = document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus");
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
frontendDebug("main", "Backfill Desk runtime status refresh button clicked");
|
||||||
|
void loadRuntimeStatus("user").catch(() => frontendWarn("main", "Backfill Desk runtime status refresh failed"));
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Backfill Desk runtime status refresh handler installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeMain(): Promise<void> {
|
||||||
|
const windowLabel = getCurrentWindow().label;
|
||||||
|
frontendInfo("main", "Backfill Desk main frontend loaded", { windowLabel });
|
||||||
|
bindFrontendInteractions();
|
||||||
|
bindNavigation();
|
||||||
|
bindRuntimeStatusRefresh();
|
||||||
|
bindBackfillRouteSelection();
|
||||||
|
bindCampaignForm();
|
||||||
|
await bindBackfillStatusMonitoring();
|
||||||
|
activateView("backfill", "startup");
|
||||||
|
try {
|
||||||
|
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup"), syncBackfillStatus("startup")]);
|
||||||
|
} catch {
|
||||||
|
frontendWarn("main", "Backfill Desk startup runtime status load failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
void initializeMain();
|
||||||
|
});
|
||||||
131
crates/ksp-app-backfill-desk/frontend/ts/splash.ts
Normal file
131
crates/ksp-app-backfill-desk/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// file: crates/ksp-app-backfill-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_backfill_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", "Backfill 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-backfill-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-backfill-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-backfill-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-backfill-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
26
crates/ksp-app-backfill-desk/package.json
Normal file
26
crates/ksp-app-backfill-desk/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "ksp-app-backfill-desk",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.3.7",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
448
crates/ksp-app-backfill-desk/src/app_state.rs
Normal file
448
crates/ksp-app-backfill-desk/src/app_state.rs
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
||||||
|
// version: 10
|
||||||
|
|
||||||
|
//! Shared backend state owned by the Backfill Desk Tauri application.
|
||||||
|
|
||||||
|
/// Shared Backfill Desk application state managed by Tauri.
|
||||||
|
pub(crate) struct AppState {
|
||||||
|
backfill_runs: crate::BackfillRunState,
|
||||||
|
config_management: ksp_config_lib::ConfigManagement,
|
||||||
|
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||||
|
shutdown_started: std::sync::atomic::AtomicBool,
|
||||||
|
splash_settings: crate::SplashSettings,
|
||||||
|
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||||
|
store_startup: crate::StoreStartup,
|
||||||
|
transport_runtime: std::option::Option<crate::TransportRuntime>,
|
||||||
|
transport_startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::AppState {
|
||||||
|
/// Initializes the scaffold Config ownership, Logging runtime and splash settings.
|
||||||
|
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 transport_startup = crate::initialize_transport(&config_management);
|
||||||
|
let (transport_runtime, transport_startup_diagnostic) = match transport_startup {
|
||||||
|
std::result::Result::Ok(value) => (std::option::Option::Some(value), std::option::Option::None),
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let diagnostic = crate::CommandErrorDto::from_error(&error);
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_TRANSPORT,
|
||||||
|
error_domain = diagnostic.domain.as_str(),
|
||||||
|
error_code = diagnostic.code.as_str(),
|
||||||
|
"Backfill Desk Transport readiness is unavailable; keeping desktop shell available"
|
||||||
|
);
|
||||||
|
(std::option::Option::None, std::option::Option::Some(diagnostic))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let store_startup = tauri::async_runtime::block_on(crate::initialize_store(&config_management, transport_runtime.as_ref()));
|
||||||
|
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 Backfill Desk splash timings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(Self {
|
||||||
|
backfill_runs: crate::BackfillRunState::new(),
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
shutdown_started: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
splash_settings,
|
||||||
|
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
store_startup,
|
||||||
|
transport_runtime,
|
||||||
|
transport_startup_diagnostic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the safe scaffold status exposed by the Backfill Desk shell.
|
||||||
|
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||||
|
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 count cannot be represented by Backfill Desk")
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let runtime = self.logging_runtime.lock();
|
||||||
|
let runtime = match runtime {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||||
|
"Backfill Desk Logging runtime state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let _keep_guard_alive = &runtime.guard;
|
||||||
|
return std::result::Result::Ok(crate::ShellStatusDto {
|
||||||
|
active_logging_profile: runtime.active_profile_id.clone(),
|
||||||
|
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
config_document_count: document_count,
|
||||||
|
fallback_logging_active: runtime.fallback_active,
|
||||||
|
shell_phase: "pre.012-frontend-polish".to_owned(),
|
||||||
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the safe Transport-and-Store readiness subset of Backfill Desk options.
|
||||||
|
pub(crate) fn backfill_options(&self) -> ksp_core_lib::Result<crate::BackfillDeskOptionsDto> {
|
||||||
|
let runtime = self.transport_runtime.as_ref();
|
||||||
|
let options = match runtime {
|
||||||
|
std::option::Option::Some(value) => value.options(),
|
||||||
|
std::option::Option::None => {
|
||||||
|
let limits = crate::backfill_request_limits();
|
||||||
|
let limits = match limits {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
std::result::Result::Ok(crate::BackfillDeskOptionsDto {
|
||||||
|
commitments: crate::backfill_commitment_codes(),
|
||||||
|
composition_ready: false,
|
||||||
|
configured_networks: std::vec::Vec::new(),
|
||||||
|
http_routes: std::vec::Vec::new(),
|
||||||
|
limits,
|
||||||
|
network_coherent: false,
|
||||||
|
program_id_options: crate::program_id_autocomplete_options(),
|
||||||
|
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||||
|
store_diagnostic: std::option::Option::None,
|
||||||
|
store_network: std::option::Option::None,
|
||||||
|
store_ready: false,
|
||||||
|
transport_diagnostic: self.transport_startup_diagnostic.clone(),
|
||||||
|
transport_ready: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut options = match options {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
self.store_startup.apply_to(&mut options);
|
||||||
|
return std::result::Result::Ok(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates and maps one frontend campaign request without starting a Backfill Job.
|
||||||
|
pub(crate) fn validate_backfill_request(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
|
||||||
|
let options = self.backfill_options();
|
||||||
|
let options = match options {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let job_id = ksp_job_api::JobId::new("backfill-desk-validation");
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mapped = crate::map_backfill_request(request, &options, job_id);
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let preview = crate::project_backfill_request(&mapped);
|
||||||
|
let preview = match preview {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_REQUEST,
|
||||||
|
commitment = preview.commitment.as_str(),
|
||||||
|
http_role = preview.http_role.as_str(),
|
||||||
|
scope_kind = preview.scope_kind.as_str(),
|
||||||
|
"validated Backfill Desk campaign request without starting a Job"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares one concrete Backfill runtime, installs its control handle atomically and lends execution resources before spawn.
|
||||||
|
pub(crate) fn prepare_backfill_start(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
|
||||||
|
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot start a Backfill run while application shutdown is in progress",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let options = self.backfill_options();
|
||||||
|
let options = match options {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let job_id = self.backfill_runs.next_job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mapped = crate::map_backfill_request(request, &options, job_id.clone());
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let retained_request = mapped.clone();
|
||||||
|
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
|
||||||
|
let runtime = match runtime {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let handle = runtime.handle();
|
||||||
|
let snapshots = handle.snapshots();
|
||||||
|
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
|
||||||
|
if let std::result::Result::Err(error) = installed {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let transport = self.transport_runtime.as_ref();
|
||||||
|
let transport = match transport {
|
||||||
|
std::option::Option::Some(value) => value.pool(),
|
||||||
|
std::option::Option::None => {
|
||||||
|
let rollback = self.backfill_runs.rollback(&job_id);
|
||||||
|
if let std::result::Result::Err(error) = rollback {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot start a Backfill run without a ready HTTP Transport pool",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let store = self.store_startup.take_for_run();
|
||||||
|
let store = match store {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let rollback = self.backfill_runs.rollback(&job_id);
|
||||||
|
if let std::result::Result::Err(rollback_error) = rollback {
|
||||||
|
return std::result::Result::Err(rollback_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
job_id = job_id.as_str(),
|
||||||
|
"admitted Backfill Desk run and installed control handle before async spawn"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares one in-session Resume from the retained Rust-only checkpoint using a new backend Job identity.
|
||||||
|
pub(crate) fn prepare_backfill_resume(&self) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
|
||||||
|
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot resume a Backfill run while application shutdown is in progress",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let options = self.backfill_options();
|
||||||
|
let options = match options {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if !options.composition_ready {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot resume without ready Transport and Store composition",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let job_id = self.backfill_runs.next_job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mapped = self.backfill_runs.resume_request(job_id.clone());
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let store_network_matches = options.store_network.as_deref() == std::option::Option::Some(mapped.network().as_str());
|
||||||
|
let role_available = options.http_routes.iter().any(|route| return route.role == mapped.role().as_str());
|
||||||
|
if !store_network_matches || !role_available {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk retained Resume request no longer matches current Transport/Store composition",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let retained_request = mapped.clone();
|
||||||
|
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
|
||||||
|
let runtime = match runtime {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let handle = runtime.handle();
|
||||||
|
let snapshots = handle.snapshots();
|
||||||
|
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
|
||||||
|
if let std::result::Result::Err(error) = installed {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let transport = self.transport_runtime.as_ref();
|
||||||
|
let transport = match transport {
|
||||||
|
std::option::Option::Some(value) => value.pool(),
|
||||||
|
std::option::Option::None => {
|
||||||
|
let rollback = self.backfill_runs.rollback(&job_id);
|
||||||
|
if let std::result::Result::Err(error) = rollback {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot resume a Backfill run without a ready HTTP Transport pool",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let store = self.store_startup.take_for_run();
|
||||||
|
let store = match store {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let rollback = self.backfill_runs.rollback(&job_id);
|
||||||
|
if let std::result::Result::Err(rollback_error) = rollback {
|
||||||
|
return std::result::Result::Err(rollback_error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
job_id = job_id.as_str(),
|
||||||
|
"admitted Backfill Desk in-session Resume with a reissued Rust-only checkpoint"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current active or retained terminal Backfill status for frontend resynchronization.
|
||||||
|
pub(crate) fn backfill_status(&self) -> ksp_core_lib::Result<std::option::Option<crate::BackfillRunStatusDto>> {
|
||||||
|
let notification = self.backfill_runs.current_notification();
|
||||||
|
let notification = match notification {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return match notification {
|
||||||
|
std::option::Option::Some(value) => crate::project_backfill_status(&value).map(std::option::Option::Some),
|
||||||
|
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests cooperative cancellation of one exact backend-issued Backfill Job identity.
|
||||||
|
pub(crate) fn cancel_backfill(&self, job_id: &str) -> ksp_core_lib::Result<crate::BackfillCancelResponseDto> {
|
||||||
|
let cancelled = self.backfill_runs.cancel(job_id);
|
||||||
|
let cancelled = match cancelled {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
accepted = cancelled.accepted,
|
||||||
|
job_id = cancelled.job_id.as_str(),
|
||||||
|
state = cancelled.state.as_str(),
|
||||||
|
"processed Backfill Desk cooperative cancellation request"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests best-effort cooperative cancellation of the active run during application shutdown.
|
||||||
|
pub(crate) fn cancel_active_backfill_for_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||||
|
return self.backfill_runs.cancel_active_for_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.
|
||||||
|
pub(crate) async fn execute_backfill_run(&self, launch: crate::BackfillRunLaunch) -> ksp_core_lib::Result<()> {
|
||||||
|
let job_id = launch.job_id.clone();
|
||||||
|
let result = launch.runtime.run(&launch.transport, &launch.store).await;
|
||||||
|
let restore = self.store_startup.restore_after_run(launch.store);
|
||||||
|
let finished = self.backfill_runs.finish(&job_id);
|
||||||
|
let cancellation_requested = match finished {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if let std::result::Result::Err(error) = restore {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(snapshot) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
cancellation_requested = cancellation_requested,
|
||||||
|
contiguous_completed = snapshot.contiguous_completed(),
|
||||||
|
job_id = job_id.as_str(),
|
||||||
|
phase = snapshot.phase().code(),
|
||||||
|
"Backfill Desk run reached a terminal snapshot and released the single-run slot"
|
||||||
|
);
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
|
||||||
|
pub(crate) fn begin_shutdown(&self) -> bool {
|
||||||
|
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly closes the retained Store runtime when application shutdown begins.
|
||||||
|
pub(crate) async fn close_store(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return self.store_startup.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
313
crates/ksp-app-backfill-desk/src/backfill_request.rs
Normal file
313
crates/ksp-app-backfill-desk/src/backfill_request.rs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/backfill_request.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Strict application mapping from frontend campaign DTOs to the KSP Backfill request contract.
|
||||||
|
|
||||||
|
/// Maps one app-owned campaign request to a validated KSP Backfill request without starting a Job.
|
||||||
|
pub(crate) fn map_backfill_request(
|
||||||
|
input: crate::BackfillStartRequestDto,
|
||||||
|
options: &crate::BackfillDeskOptionsDto,
|
||||||
|
job_id: ksp_job_api::JobId,
|
||||||
|
) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
|
||||||
|
let composition = validate_composition(options);
|
||||||
|
if let std::result::Result::Err(error) = composition {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let network = request_network(options);
|
||||||
|
let network = match network {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let role = request_role(input.http_role.as_str(), options);
|
||||||
|
let role = match role {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let commitment = request_commitment(input.commitment.as_str());
|
||||||
|
let commitment = match commitment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let min_context_slot = parse_min_context_slot(input.min_context_slot.as_deref());
|
||||||
|
let min_context_slot = match min_context_slot {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let scope = request_scope(&input);
|
||||||
|
let scope = match scope {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let page_size = usize::try_from(input.page_size);
|
||||||
|
let page_size = match page_size {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
|
||||||
|
};
|
||||||
|
let max_pages = usize::try_from(input.max_pages);
|
||||||
|
let max_pages = match max_pages {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
|
||||||
|
};
|
||||||
|
let max_candidates = usize::try_from(input.max_candidates);
|
||||||
|
let max_candidates = match max_candidates {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
|
||||||
|
};
|
||||||
|
let hydration_concurrency = usize::try_from(input.hydration_concurrency);
|
||||||
|
let hydration_concurrency = match hydration_concurrency {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
|
||||||
|
};
|
||||||
|
return ksp_job_backfill_lib::BackfillRequest::new(
|
||||||
|
job_id,
|
||||||
|
network,
|
||||||
|
role,
|
||||||
|
commitment,
|
||||||
|
scope,
|
||||||
|
page_size,
|
||||||
|
max_pages,
|
||||||
|
max_candidates,
|
||||||
|
hydration_concurrency,
|
||||||
|
min_context_slot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a safe request preview from one already validated KSP Backfill request.
|
||||||
|
pub(crate) fn project_backfill_request(request: &ksp_job_backfill_lib::BackfillRequest) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
|
||||||
|
let explicit_signature_count = match request.scope().signatures() {
|
||||||
|
std::option::Option::Some(signatures) => u32::try_from(signatures.len()),
|
||||||
|
std::option::Option::None => std::result::Result::Ok(0),
|
||||||
|
};
|
||||||
|
let explicit_signature_count = match explicit_signature_count {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("explicit_signature_count", error)),
|
||||||
|
};
|
||||||
|
let hydration_concurrency = u32::try_from(request.hydration_concurrency());
|
||||||
|
let hydration_concurrency = match hydration_concurrency {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
|
||||||
|
};
|
||||||
|
let max_candidates = u32::try_from(request.max_candidates());
|
||||||
|
let max_candidates = match max_candidates {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
|
||||||
|
};
|
||||||
|
let max_pages = u32::try_from(request.max_pages());
|
||||||
|
let max_pages = match max_pages {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
|
||||||
|
};
|
||||||
|
let page_size = u32::try_from(request.page_size());
|
||||||
|
let page_size = match page_size {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::BackfillRequestPreviewDto {
|
||||||
|
address_present: request.scope().address().is_some(),
|
||||||
|
anchor_present: request.scope().anchor().is_some(),
|
||||||
|
commitment: request.commitment().code().to_owned(),
|
||||||
|
explicit_signature_count,
|
||||||
|
hydration_concurrency,
|
||||||
|
http_role: request.role().as_str().to_owned(),
|
||||||
|
max_candidates,
|
||||||
|
max_pages,
|
||||||
|
min_context_slot_present: request.min_context_slot().is_some(),
|
||||||
|
network: request.network().as_str().to_owned(),
|
||||||
|
page_size,
|
||||||
|
scope_kind: request.scope().kind().code().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_min_context_slot(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
||||||
|
let value = match value {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||||
|
};
|
||||||
|
if value.is_empty() || value.trim() != value {
|
||||||
|
return std::result::Result::Err(request_field_error("min_context_slot"));
|
||||||
|
}
|
||||||
|
let parsed = value.parse::<u64>();
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("min_context_slot").with_source(error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pubkey(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||||
|
if value.is_empty() || value.trim() != value {
|
||||||
|
return std::result::Result::Err(request_field_error(field));
|
||||||
|
}
|
||||||
|
let parsed = value.parse::<ksp_core_lib::Pubkey>();
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signature(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
|
||||||
|
if value.is_empty() || value.trim() != value {
|
||||||
|
return std::result::Result::Err(request_field_error(field));
|
||||||
|
}
|
||||||
|
let parsed = ksp_job_backfill_lib::BackfillSignature::new(value.to_owned());
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_commitment(value: &str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillCommitment> {
|
||||||
|
return match value {
|
||||||
|
"confirmed" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Confirmed),
|
||||||
|
"finalized" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Finalized),
|
||||||
|
_ => std::result::Result::Err(request_field_error("commitment")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_conversion_error(field: &'static str, source: impl std::error::Error + Send + Sync + 'static) -> ksp_core_lib::Error {
|
||||||
|
return request_field_error(field).with_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_field_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_REQUEST_INVALID, "Backfill Desk campaign request is invalid").with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_network(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_store_lib::RawNetworkId> {
|
||||||
|
let network = options.store_network.as_deref();
|
||||||
|
let network = match network {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(request_field_error("network")),
|
||||||
|
};
|
||||||
|
let parsed = ksp_store_lib::RawNetworkId::new(network.to_owned());
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("network").with_source(error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_role(value: &str, options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
|
||||||
|
let admitted = options.http_routes.iter().any(|route| return route.role == value);
|
||||||
|
if !admitted || value.is_empty() || value.trim() != value {
|
||||||
|
return std::result::Result::Err(request_field_error("http_role"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(value.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
|
||||||
|
return match input.scope_kind.as_str() {
|
||||||
|
"latest_address" => latest_address_scope(input),
|
||||||
|
"before_address" => before_address_scope(input),
|
||||||
|
"after_address" => after_address_scope(input),
|
||||||
|
"explicit_signatures" => explicit_signatures_scope(input),
|
||||||
|
_ => std::result::Result::Err(request_field_error("scope_kind")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latest_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
|
||||||
|
let shape = require_no_anchor_or_signatures(input);
|
||||||
|
if let std::result::Result::Err(error) = shape {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let address = required_address(input.address.as_deref());
|
||||||
|
let address = match address {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::latest_address(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
|
||||||
|
let shape = require_no_explicit_signatures(input);
|
||||||
|
if let std::result::Result::Err(error) = shape {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let address = required_address(input.address.as_deref());
|
||||||
|
let address = match address {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let anchor = required_anchor(input.anchor_signature.as_deref());
|
||||||
|
let anchor = match anchor {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::before_address(address, anchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
|
||||||
|
let shape = require_no_explicit_signatures(input);
|
||||||
|
if let std::result::Result::Err(error) = shape {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let address = required_address(input.address.as_deref());
|
||||||
|
let address = match address {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let anchor = required_anchor(input.anchor_signature.as_deref());
|
||||||
|
let anchor = match anchor {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::after_address(address, anchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn explicit_signatures_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
|
||||||
|
if input.address.is_some() || input.anchor_signature.is_some() || input.min_context_slot.is_some() {
|
||||||
|
return std::result::Result::Err(request_field_error("scope"));
|
||||||
|
}
|
||||||
|
if input.explicit_signatures.is_empty() || input.explicit_signatures.len() > ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES {
|
||||||
|
return std::result::Result::Err(request_field_error("explicit_signatures"));
|
||||||
|
}
|
||||||
|
let mut signatures = std::vec::Vec::with_capacity(input.explicit_signatures.len());
|
||||||
|
for value in &input.explicit_signatures {
|
||||||
|
let signature = parse_signature(value.as_str(), "explicit_signatures");
|
||||||
|
let signature = match signature {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
signatures.push(signature);
|
||||||
|
}
|
||||||
|
return ksp_job_backfill_lib::BackfillScope::explicit_signatures(signatures);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_no_anchor_or_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
|
||||||
|
if input.anchor_signature.is_some() || !input.explicit_signatures.is_empty() {
|
||||||
|
return std::result::Result::Err(request_field_error("scope"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_no_explicit_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
|
||||||
|
if !input.explicit_signatures.is_empty() {
|
||||||
|
return std::result::Result::Err(request_field_error("explicit_signatures"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_address(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||||
|
return match value {
|
||||||
|
std::option::Option::Some(value) => parse_pubkey(value, "address"),
|
||||||
|
std::option::Option::None => std::result::Result::Err(request_field_error("address")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_anchor(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
|
||||||
|
return match value {
|
||||||
|
std::option::Option::Some(value) => parse_signature(value, "anchor_signature"),
|
||||||
|
std::option::Option::None => std::result::Result::Err(request_field_error("anchor_signature")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_composition(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<()> {
|
||||||
|
if options.composition_ready && options.transport_ready && options.store_ready && options.network_coherent {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot admit a campaign before Transport and Store composition is ready",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/backfill_request.rs"]
|
||||||
|
mod tests;
|
||||||
333
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal file
333
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Single-active-run admission and launch ownership for Backfill Desk.
|
||||||
|
|
||||||
|
/// Fully prepared Backfill execution moved into the Tauri async runtime after admission.
|
||||||
|
pub(crate) struct BackfillRunLaunch {
|
||||||
|
/// Backend-generated logical Job identity.
|
||||||
|
pub(crate) job_id: ksp_job_api::JobId,
|
||||||
|
/// Concrete Backfill runtime owning discovery, hydration, persistence and latest-value publication.
|
||||||
|
pub(crate) runtime: ksp_job_backfill_lib::BackfillJobRuntime,
|
||||||
|
/// Independent latest-value source retained by the monitoring bridge.
|
||||||
|
pub(crate) snapshots: ksp_job_backfill_lib::BackfillSnapshotSource,
|
||||||
|
/// Store facade temporarily borrowed from the application Store runtime for the duration of this run.
|
||||||
|
pub(crate) store: ksp_store_lib::Store,
|
||||||
|
/// Shareable HTTP Transport pool selected by the active composite profile.
|
||||||
|
pub(crate) transport: ksp_onchain_transport_lib::HttpTransportPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::BackfillRunLaunch {
|
||||||
|
/// Builds the safe acknowledgement returned immediately after the non-blocking Start admission.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn response(&self) -> crate::BackfillStartResponseDto {
|
||||||
|
return crate::BackfillStartResponseDto { job_id: self.job_id.as_str().to_owned(), state: ksp_job_api::JobState::Created.code().to_owned() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single-run control slot retained by the application while one Backfill runtime is active.
|
||||||
|
pub(crate) struct BackfillRunState {
|
||||||
|
active: std::sync::Mutex<std::option::Option<ActiveBackfillRun>>,
|
||||||
|
last_terminal: std::sync::Mutex<std::option::Option<TerminalBackfillRun>>,
|
||||||
|
next_sequence: std::sync::atomic::AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::BackfillRunState {
|
||||||
|
/// Creates an empty single-run state for one desktop application session.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn new() -> Self {
|
||||||
|
return Self {
|
||||||
|
active: std::sync::Mutex::new(std::option::Option::None),
|
||||||
|
last_terminal: std::sync::Mutex::new(std::option::Option::None),
|
||||||
|
next_sequence: std::sync::atomic::AtomicU64::new(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates one bounded backend-owned Job identity without accepting caller-supplied identity material.
|
||||||
|
pub(crate) fn next_job_id(&self) -> ksp_core_lib::Result<ksp_job_api::JobId> {
|
||||||
|
let sequence = self
|
||||||
|
.next_sequence
|
||||||
|
.fetch_update(std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire, |current| return current.checked_add(1));
|
||||||
|
let sequence = match sequence {
|
||||||
|
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_INVALID,
|
||||||
|
"Backfill Desk Job identity sequence is exhausted",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return ksp_job_api::JobId::new(format!("backfill-desk-{sequence}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs one control handle atomically and rejects concurrent Starts while another run owns the slot.
|
||||||
|
pub(crate) fn install(
|
||||||
|
&self,
|
||||||
|
job_id: ksp_job_api::JobId,
|
||||||
|
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let mut active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk active-run state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if active.is_some() {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
|
||||||
|
"Backfill Desk already has an active Backfill run",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id, request });
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the matching terminal run and reports whether cancellation had been requested on its retained handle.
|
||||||
|
pub(crate) fn finish(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<bool> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let mut active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk active-run state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let current = active.as_ref();
|
||||||
|
let current = match current {
|
||||||
|
std::option::Option::Some(value) if &value.job_id == job_id => value,
|
||||||
|
std::option::Option::Some(_) => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||||
|
"Backfill Desk terminal run does not match the active Job identity",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||||
|
"Backfill Desk terminal run has no active admission slot",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let cancellation_requested = current.handle.is_cancellation_requested();
|
||||||
|
let request = current.request.clone();
|
||||||
|
let source = current.handle.snapshots();
|
||||||
|
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||||
|
if notification.state().is_terminal() {
|
||||||
|
let terminal = self.last_terminal.lock();
|
||||||
|
let mut terminal = match terminal {
|
||||||
|
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,
|
||||||
|
"Backfill Desk terminal monitoring state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
*terminal = std::option::Option::Some(TerminalBackfillRun { notification, request });
|
||||||
|
}
|
||||||
|
*active = std::option::Option::None;
|
||||||
|
return std::result::Result::Ok(cancellation_requested);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests cooperative cancellation for the exact backend-issued active Job identity.
|
||||||
|
pub(crate) fn cancel(&self, job_id: &str) -> ksp_core_lib::Result<crate::BackfillCancelResponseDto> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk active-run cancellation state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let std::option::Option::Some(current) = active.as_ref() {
|
||||||
|
if current.job_id.as_str() != job_id {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
|
||||||
|
"Backfill Desk cancellation target does not match the active Job",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let source = current.handle.snapshots();
|
||||||
|
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||||
|
let state = notification.state();
|
||||||
|
if state.is_terminal() {
|
||||||
|
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
|
||||||
|
accepted: false,
|
||||||
|
job_id: current.job_id.as_str().to_owned(),
|
||||||
|
state: state.code().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let accepted = current.handle.cancel();
|
||||||
|
let state_code = if current.handle.is_cancellation_requested() { "cancelling" } else { state.code() };
|
||||||
|
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
|
||||||
|
accepted,
|
||||||
|
job_id: current.job_id.as_str().to_owned(),
|
||||||
|
state: state_code.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
drop(active);
|
||||||
|
let terminal = self.last_terminal.lock();
|
||||||
|
let terminal = match terminal {
|
||||||
|
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,
|
||||||
|
"Backfill Desk terminal cancellation state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let std::option::Option::Some(terminal) = terminal.as_ref() {
|
||||||
|
let notification = &terminal.notification;
|
||||||
|
if notification.id().as_str() != job_id {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
|
||||||
|
"Backfill Desk cancellation target does not match the retained terminal Job",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
|
||||||
|
accepted: false,
|
||||||
|
job_id: notification.id().as_str().to_owned(),
|
||||||
|
state: notification.state().code().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE,
|
||||||
|
"Backfill Desk has no active Backfill run to cancel",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests best-effort cooperative cancellation of whichever run is active after shutdown wins admission.
|
||||||
|
pub(crate) fn cancel_active_for_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk shutdown cancellation state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return match active.as_ref() {
|
||||||
|
std::option::Option::Some(current) => std::result::Result::Ok(current.handle.cancel()),
|
||||||
|
std::option::Option::None => std::result::Result::Ok(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete current active snapshot or the retained latest terminal snapshot for resynchronization.
|
||||||
|
pub(crate) fn current_notification(
|
||||||
|
&self,
|
||||||
|
) -> ksp_core_lib::Result<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk active-run monitoring state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let std::option::Option::Some(current) = active.as_ref() {
|
||||||
|
let source = current.handle.snapshots();
|
||||||
|
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||||
|
return std::result::Result::Ok(std::option::Option::Some(notification));
|
||||||
|
}
|
||||||
|
drop(active);
|
||||||
|
let terminal = self.last_terminal.lock();
|
||||||
|
let terminal = match terminal {
|
||||||
|
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,
|
||||||
|
"Backfill Desk terminal monitoring state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(terminal.as_ref().map(|terminal| return terminal.notification.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the last terminal request with its opaque checkpoint reissued onto one new Job identity.
|
||||||
|
pub(crate) fn resume_request(&self, job_id: ksp_job_api::JobId) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
|
||||||
|
let active = self.active.lock();
|
||||||
|
let active = match active {
|
||||||
|
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,
|
||||||
|
"Backfill Desk active-run resume state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if active.is_some() {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
|
||||||
|
"Backfill Desk cannot resume while another Backfill run owns the single-run slot",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
drop(active);
|
||||||
|
let terminal = self.last_terminal.lock();
|
||||||
|
let terminal = match terminal {
|
||||||
|
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,
|
||||||
|
"Backfill Desk terminal resume state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let terminal = match terminal.as_ref() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
|
||||||
|
"Backfill Desk has no retained terminal run available for in-session Resume",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let checkpoint = terminal.notification.snapshot().checkpoint();
|
||||||
|
let checkpoint = match checkpoint {
|
||||||
|
std::option::Option::Some(value) => value.clone(),
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
|
||||||
|
"Backfill Desk retained terminal run has no safe checkpoint for in-session Resume",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return terminal.request.resume_for_job(job_id, checkpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
|
||||||
|
pub(crate) fn rollback(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<()> {
|
||||||
|
let finished = self.finish(job_id);
|
||||||
|
return match finished {
|
||||||
|
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActiveBackfillRun {
|
||||||
|
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
||||||
|
job_id: ksp_job_api::JobId,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TerminalBackfillRun {
|
||||||
|
notification: ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/backfill_run.rs"]
|
||||||
|
mod tests;
|
||||||
194
crates/ksp-app-backfill-desk/src/backfill_status.rs
Normal file
194
crates/ksp-app-backfill-desk/src/backfill_status.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/backfill_status.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Safe latest-value monitoring projection for one Backfill Desk run.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Stable Tauri event carrying the latest complete safe Backfill run status.
|
||||||
|
pub(crate) const BACKFILL_STATUS_EVENT_NAME: &str = "ksp-backfill-status";
|
||||||
|
|
||||||
|
/// Frontend-safe latest-value projection of one concrete Backfill runtime snapshot.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts")]
|
||||||
|
pub(crate) struct BackfillRunStatusDto {
|
||||||
|
/// Whether the represented Job is currently non-terminal.
|
||||||
|
pub(crate) active: bool,
|
||||||
|
/// Number of candidates admitted into hydration.
|
||||||
|
pub(crate) candidates_admitted: u32,
|
||||||
|
/// Number of admitted candidates that reached a known coordinator result.
|
||||||
|
pub(crate) candidates_finished: u32,
|
||||||
|
/// Number of candidates selected by discovery.
|
||||||
|
pub(crate) candidates_selected: u32,
|
||||||
|
/// Number of candidates cancelled before Store submission.
|
||||||
|
pub(crate) cancelled_candidates: u32,
|
||||||
|
/// Whether a safe caller-owned checkpoint currently exists.
|
||||||
|
pub(crate) checkpoint_present: bool,
|
||||||
|
/// Normal terminal completion classification, when applicable.
|
||||||
|
pub(crate) completion: std::option::Option<String>,
|
||||||
|
/// Safe contiguous completion prefix retained by the current checkpoint frontier.
|
||||||
|
pub(crate) contiguous_completed: u32,
|
||||||
|
/// Number of Store content conflicts observed by the run.
|
||||||
|
pub(crate) conflicts: u32,
|
||||||
|
/// Safe discovery boundary code after discovery completes.
|
||||||
|
pub(crate) discovery_boundary: std::option::Option<String>,
|
||||||
|
/// Number of identical canonical RAW transactions already present.
|
||||||
|
pub(crate) entities_existing: u32,
|
||||||
|
/// Number of newly inserted canonical RAW transactions.
|
||||||
|
pub(crate) entities_inserted: u32,
|
||||||
|
/// Number of durable purge tombstones respected by normal Backfill persistence.
|
||||||
|
pub(crate) entities_purged: u32,
|
||||||
|
/// Stable fatal error code retained by a failed Job, when applicable.
|
||||||
|
pub(crate) failure_code: std::option::Option<String>,
|
||||||
|
/// Stable fatal error domain retained by a failed Job, when applicable.
|
||||||
|
pub(crate) failure_domain: std::option::Option<String>,
|
||||||
|
/// Number of known candidate outcomes blocking the contiguous frontier.
|
||||||
|
pub(crate) holes: u32,
|
||||||
|
/// Backend-generated logical Job identity.
|
||||||
|
pub(crate) job_id: String,
|
||||||
|
/// Stable concrete Job family code.
|
||||||
|
pub(crate) job_kind: String,
|
||||||
|
/// Greatest observed candidate concurrency.
|
||||||
|
pub(crate) maximum_in_flight: u32,
|
||||||
|
/// Number of `getTransaction = null` candidates.
|
||||||
|
pub(crate) missing: u32,
|
||||||
|
/// Number of acquisition observations already durable.
|
||||||
|
pub(crate) observations_existing: u32,
|
||||||
|
/// Number of newly inserted acquisition observations.
|
||||||
|
pub(crate) observations_inserted: u32,
|
||||||
|
/// Stable concrete Backfill runtime phase.
|
||||||
|
pub(crate) phase: String,
|
||||||
|
/// Monotone latest-value notification sequence encoded as decimal text.
|
||||||
|
pub(crate) sequence: String,
|
||||||
|
/// Stable Backfill scope category without address/signature payloads.
|
||||||
|
pub(crate) scope_kind: String,
|
||||||
|
/// Stable generic Job lifecycle state.
|
||||||
|
pub(crate) state: String,
|
||||||
|
/// Whether this latest value is terminal.
|
||||||
|
pub(crate) terminal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Projects one complete Backfill latest-value notification to the safe desktop monitoring contract.
|
||||||
|
pub(crate) fn project_backfill_status(
|
||||||
|
notification: &ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
|
||||||
|
) -> ksp_core_lib::Result<BackfillRunStatusDto> {
|
||||||
|
let snapshot = notification.snapshot();
|
||||||
|
let state = notification.state();
|
||||||
|
let failure = snapshot.failure_code();
|
||||||
|
let candidates_admitted = usize_to_u32(snapshot.candidates_admitted(), "candidates_admitted");
|
||||||
|
let candidates_admitted = match candidates_admitted {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let candidates_finished = usize_to_u32(snapshot.candidates_finished(), "candidates_finished");
|
||||||
|
let candidates_finished = match candidates_finished {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let candidates_selected = usize_to_u32(snapshot.candidates_selected(), "candidates_selected");
|
||||||
|
let candidates_selected = match candidates_selected {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let cancelled_candidates = usize_to_u32(snapshot.cancelled_candidates(), "cancelled_candidates");
|
||||||
|
let cancelled_candidates = match cancelled_candidates {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let contiguous_completed = usize_to_u32(snapshot.contiguous_completed(), "contiguous_completed");
|
||||||
|
let contiguous_completed = match contiguous_completed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let conflicts = usize_to_u32(snapshot.conflicts(), "conflicts");
|
||||||
|
let conflicts = match conflicts {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let entities_existing = usize_to_u32(snapshot.entities_existing(), "entities_existing");
|
||||||
|
let entities_existing = match entities_existing {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let entities_inserted = usize_to_u32(snapshot.entities_inserted(), "entities_inserted");
|
||||||
|
let entities_inserted = match entities_inserted {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let entities_purged = usize_to_u32(snapshot.entities_purged(), "entities_purged");
|
||||||
|
let entities_purged = match entities_purged {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let holes = usize_to_u32(snapshot.holes(), "holes");
|
||||||
|
let holes = match holes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let maximum_in_flight = usize_to_u32(snapshot.maximum_in_flight(), "maximum_in_flight");
|
||||||
|
let maximum_in_flight = match maximum_in_flight {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let missing = usize_to_u32(snapshot.missing(), "missing");
|
||||||
|
let missing = match missing {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let observations_existing = usize_to_u32(snapshot.observations_existing(), "observations_existing");
|
||||||
|
let observations_existing = match observations_existing {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let observations_inserted = usize_to_u32(snapshot.observations_inserted(), "observations_inserted");
|
||||||
|
let observations_inserted = match observations_inserted {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(BackfillRunStatusDto {
|
||||||
|
active: !state.is_terminal(),
|
||||||
|
candidates_admitted,
|
||||||
|
candidates_finished,
|
||||||
|
candidates_selected,
|
||||||
|
cancelled_candidates,
|
||||||
|
checkpoint_present: snapshot.checkpoint().is_some(),
|
||||||
|
completion: state.completion().map(|completion| return completion.code().to_owned()),
|
||||||
|
contiguous_completed,
|
||||||
|
conflicts,
|
||||||
|
discovery_boundary: snapshot.discovery_boundary().map(|boundary| return boundary.code().to_owned()),
|
||||||
|
entities_existing,
|
||||||
|
entities_inserted,
|
||||||
|
entities_purged,
|
||||||
|
failure_code: failure.map(|code| return code.code().to_owned()),
|
||||||
|
failure_domain: failure.map(|code| return code.domain().to_owned()),
|
||||||
|
holes,
|
||||||
|
job_id: notification.id().as_str().to_owned(),
|
||||||
|
job_kind: notification.kind().as_str().to_owned(),
|
||||||
|
maximum_in_flight,
|
||||||
|
missing,
|
||||||
|
observations_existing,
|
||||||
|
observations_inserted,
|
||||||
|
phase: snapshot.phase().code().to_owned(),
|
||||||
|
sequence: notification.sequence().value().to_string(),
|
||||||
|
scope_kind: snapshot.scope_kind().code().to_owned(),
|
||||||
|
state: state.code().to_owned(),
|
||||||
|
terminal: state.is_terminal(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usize_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, "Backfill Desk cannot project a bounded Job counter to the frontend")
|
||||||
|
.with_context("field", field)
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/backfill_status.rs"]
|
||||||
|
mod tests;
|
||||||
215
crates/ksp-app-backfill-desk/src/bootstrap.rs
Normal file
215
crates/ksp-app-backfill-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/bootstrap.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Config composite and Logging bootstrap for Backfill 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 Backfill Desk Config composite using its registered logical `file_id` and autonomous default profile.
|
||||||
|
pub(crate) fn load_backfill_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_BACKFILL_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 Backfill 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, "Backfill 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, "Backfill 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 Backfill Desk composite, with a trace-level development fallback.
|
||||||
|
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 Backfill 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::Trace,
|
||||||
|
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_backfill_desk_composite(management);
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||||
|
};
|
||||||
|
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||||
|
let logging_profile = match logging_profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||||
|
};
|
||||||
|
let transport_profile =
|
||||||
|
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||||
|
if let std::result::Result::Err(error) = transport_profile {
|
||||||
|
return fallback_startup_plan(error);
|
||||||
|
}
|
||||||
|
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||||
|
if let std::result::Result::Err(error) = store_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 Backfill 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 trace-level development 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn development_fallback_keeps_trace_level_enabled() {
|
||||||
|
let settings = super::fallback_logging_settings();
|
||||||
|
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/ksp-app-backfill-desk/src/constants.rs
Normal file
43
crates/ksp-app-backfill-desk/src/constants.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/constants.rs
|
||||||
|
// version: 6
|
||||||
|
|
||||||
|
//! Application-owned tracing targets and domains.
|
||||||
|
|
||||||
|
/// Composite component identifier for Logging.
|
||||||
|
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
|
||||||
|
/// Composite component identifier for Store.
|
||||||
|
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
|
||||||
|
/// Composite component identifier for Transport.
|
||||||
|
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
|
||||||
|
/// Initial hydration concurrency proposed by the Desk for a new campaign.
|
||||||
|
pub(crate) const DEFAULT_BACKFILL_HYDRATION_CONCURRENCY: u32 = 4;
|
||||||
|
/// Initial candidate cap proposed by the Desk for a new campaign.
|
||||||
|
pub(crate) const DEFAULT_BACKFILL_MAX_CANDIDATES: u32 = 1_000;
|
||||||
|
/// Initial discovery-page cap proposed by the Desk for a new campaign.
|
||||||
|
pub(crate) const DEFAULT_BACKFILL_MAX_PAGES: u32 = 10;
|
||||||
|
/// Initial signature page size proposed by the Desk for a new campaign.
|
||||||
|
pub(crate) const DEFAULT_BACKFILL_PAGE_SIZE: u32 = 100;
|
||||||
|
/// Structured domain used while bootstrapping Config and Logging.
|
||||||
|
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
|
||||||
|
/// Structured domain used by technical frontend events.
|
||||||
|
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||||
|
/// Structured domain used by Backfill campaign admission and request mapping.
|
||||||
|
pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
|
||||||
|
/// Structured domain used by concrete Backfill run admission and execution.
|
||||||
|
pub(crate) const TRACING_DOMAIN_RUN: &str = "backfill.run";
|
||||||
|
/// Structured domain used by the Backfill Desk shell.
|
||||||
|
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
|
||||||
|
/// Structured domain used by Store readiness and shutdown operations.
|
||||||
|
pub(crate) const TRACING_DOMAIN_STORE: &str = "backfill.store";
|
||||||
|
/// Structured domain used by Transport readiness and role inventory operations.
|
||||||
|
pub(crate) const TRACING_DOMAIN_TRANSPORT: &str = "backfill.transport";
|
||||||
|
/// Structured domain used by Tauri window lifecycle operations.
|
||||||
|
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||||
|
/// Owning target for backend events emitted by Backfill Desk.
|
||||||
|
pub(crate) const TRACING_TARGET: &str = "ksp-app-backfill-desk";
|
||||||
|
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-backfill-desk.frontend";
|
||||||
|
/// Owning target for main-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-backfill-desk.frontend.main";
|
||||||
|
/// Owning target for splash-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-backfill-desk.frontend.splash";
|
||||||
233
crates/ksp-app-backfill-desk/src/dto_backfill.rs
Normal file
233
crates/ksp-app-backfill-desk/src/dto_backfill.rs
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Backend-derived limits and application defaults rendered by the Backfill campaign form.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestLimitsDto.ts")]
|
||||||
|
pub(crate) struct BackfillRequestLimitsDto {
|
||||||
|
/// Initial hydration concurrency proposed by this Desk while remaining below the Job-owned maximum.
|
||||||
|
pub(crate) default_hydration_concurrency: u32,
|
||||||
|
/// Initial candidate cap proposed by this Desk while remaining below the Job-owned maximum.
|
||||||
|
pub(crate) default_max_candidates: u32,
|
||||||
|
/// Initial discovery-page cap proposed by this Desk while remaining below the Job-owned maximum.
|
||||||
|
pub(crate) default_max_pages: u32,
|
||||||
|
/// Initial signature page size proposed by this Desk while remaining below the Job-owned maximum.
|
||||||
|
pub(crate) default_page_size: u32,
|
||||||
|
/// Maximum hydration concurrency owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) max_hydration_concurrency: u32,
|
||||||
|
/// Maximum candidate count owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) max_candidates: u32,
|
||||||
|
/// Maximum discovery-page count owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) max_pages: u32,
|
||||||
|
/// Maximum signature page size owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) max_page_size: u32,
|
||||||
|
/// Maximum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) max_signature_text_bytes: u32,
|
||||||
|
/// Minimum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) min_signature_text_bytes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App-owned request received from the Backfill campaign form before a Job is started.
|
||||||
|
///
|
||||||
|
/// Network and physical endpoint information are intentionally absent. The backend derives the
|
||||||
|
/// network from the configured Store and maps `http_role` only after checking the current safe
|
||||||
|
/// Transport inventory.
|
||||||
|
#[derive(serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts")]
|
||||||
|
pub(crate) struct BackfillStartRequestDto {
|
||||||
|
/// Optional address text used only by address-based scopes.
|
||||||
|
pub(crate) address: std::option::Option<String>,
|
||||||
|
/// Optional exclusive anchor signature used only by Before/After scopes.
|
||||||
|
pub(crate) anchor_signature: std::option::Option<String>,
|
||||||
|
/// Stable Backfill commitment code.
|
||||||
|
pub(crate) commitment: String,
|
||||||
|
/// Explicit signature texts used only by the explicit-signatures scope.
|
||||||
|
pub(crate) explicit_signatures: std::vec::Vec<String>,
|
||||||
|
/// Maximum number of concurrent candidate hydrations.
|
||||||
|
pub(crate) hydration_concurrency: u32,
|
||||||
|
/// Logical HTTP role selected from `BackfillDeskOptionsDto::http_routes`.
|
||||||
|
pub(crate) http_role: String,
|
||||||
|
/// Maximum number of candidates admitted by the campaign.
|
||||||
|
pub(crate) max_candidates: u32,
|
||||||
|
/// Maximum number of address-discovery pages admitted by the campaign.
|
||||||
|
pub(crate) max_pages: u32,
|
||||||
|
/// Optional minimum context slot encoded as decimal text to avoid JavaScript integer precision loss.
|
||||||
|
pub(crate) min_context_slot: std::option::Option<String>,
|
||||||
|
/// Signature page size used by address discovery.
|
||||||
|
pub(crate) page_size: u32,
|
||||||
|
/// Stable Backfill scope kind code.
|
||||||
|
pub(crate) scope_kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts")]
|
||||||
|
pub(crate) struct BackfillStartResponseDto {
|
||||||
|
/// Backend-generated bounded Job identifier for this in-session run.
|
||||||
|
pub(crate) job_id: String,
|
||||||
|
/// Initial lifecycle state at the time Start returns to the frontend.
|
||||||
|
pub(crate) state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe acknowledgement of one targeted cooperative cancellation request.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts")]
|
||||||
|
pub(crate) struct BackfillCancelResponseDto {
|
||||||
|
/// Whether this call won the first pre-terminal cancellation request for the targeted run.
|
||||||
|
pub(crate) accepted: bool,
|
||||||
|
/// Backend-generated Job identifier that the cancellation request targeted.
|
||||||
|
pub(crate) job_id: String,
|
||||||
|
/// Safe current or cancellation-intent lifecycle code at command completion.
|
||||||
|
pub(crate) state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe acknowledgement returned when one retained checkpoint is resumed into a new backend Job lifecycle.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts")]
|
||||||
|
pub(crate) struct BackfillResumeResponseDto {
|
||||||
|
/// Whether the retained in-session checkpoint was accepted for a new run.
|
||||||
|
pub(crate) accepted: bool,
|
||||||
|
/// Newly allocated backend Job identifier owning the reissued checkpoint.
|
||||||
|
pub(crate) job_id: String,
|
||||||
|
/// Initial lifecycle state at the time Resume returns to the frontend.
|
||||||
|
pub(crate) state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
|
||||||
|
///
|
||||||
|
/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is
|
||||||
|
/// suitable for request validation feedback before the runtime Start slice exists.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts")]
|
||||||
|
pub(crate) struct BackfillRequestPreviewDto {
|
||||||
|
/// Whether an address participates in the validated scope.
|
||||||
|
pub(crate) address_present: bool,
|
||||||
|
/// Whether an exclusive anchor participates in the validated scope.
|
||||||
|
pub(crate) anchor_present: bool,
|
||||||
|
/// Stable validated commitment code.
|
||||||
|
pub(crate) commitment: String,
|
||||||
|
/// Number of explicit signatures after Backfill-owned stable deduplication.
|
||||||
|
pub(crate) explicit_signature_count: u32,
|
||||||
|
/// Validated hydration concurrency.
|
||||||
|
pub(crate) hydration_concurrency: u32,
|
||||||
|
/// Validated logical HTTP role.
|
||||||
|
pub(crate) http_role: String,
|
||||||
|
/// Validated maximum candidate count.
|
||||||
|
pub(crate) max_candidates: u32,
|
||||||
|
/// Validated maximum discovery-page count.
|
||||||
|
pub(crate) max_pages: u32,
|
||||||
|
/// Whether a minimum context slot participates in the validated request.
|
||||||
|
pub(crate) min_context_slot_present: bool,
|
||||||
|
/// Backend-derived logical Store network.
|
||||||
|
pub(crate) network: String,
|
||||||
|
/// Validated signature page size.
|
||||||
|
pub(crate) page_size: u32,
|
||||||
|
/// Stable validated scope kind code.
|
||||||
|
pub(crate) scope_kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the exact commitment codes currently admitted by the Backfill runtime.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn backfill_commitment_codes() -> std::vec::Vec<String> {
|
||||||
|
return vec![ksp_job_backfill_lib::BackfillCommitment::Finalized.code().to_owned(), ksp_job_backfill_lib::BackfillCommitment::Confirmed.code().to_owned()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the exact HTTP scope codes currently admitted by the Backfill runtime.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn backfill_scope_kind_codes() -> std::vec::Vec<String> {
|
||||||
|
return vec![
|
||||||
|
ksp_job_backfill_lib::BackfillScopeKind::LatestAddress.code().to_owned(),
|
||||||
|
ksp_job_backfill_lib::BackfillScopeKind::BeforeAddress.code().to_owned(),
|
||||||
|
ksp_job_backfill_lib::BackfillScopeKind::AfterAddress.code().to_owned(),
|
||||||
|
ksp_job_backfill_lib::BackfillScopeKind::ExplicitSignatures.code().to_owned(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds frontend-safe request limits directly from the public Backfill constants.
|
||||||
|
pub(crate) fn backfill_request_limits() -> ksp_core_lib::Result<BackfillRequestLimitsDto> {
|
||||||
|
let max_hydration_concurrency = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY, "max_hydration_concurrency");
|
||||||
|
let max_hydration_concurrency = match max_hydration_concurrency {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let max_candidates = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES, "max_candidates");
|
||||||
|
let max_candidates = match max_candidates {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let max_pages = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGES, "max_pages");
|
||||||
|
let max_pages = match max_pages {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let max_page_size = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE, "max_page_size");
|
||||||
|
let max_page_size = match max_page_size {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let max_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, "max_signature_text_bytes");
|
||||||
|
let max_signature_text_bytes = match max_signature_text_bytes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let min_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, "min_signature_text_bytes");
|
||||||
|
let min_signature_text_bytes = match min_signature_text_bytes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY == 0 || crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY > max_hydration_concurrency {
|
||||||
|
return std::result::Result::Err(limit_contract_error("default_hydration_concurrency"));
|
||||||
|
}
|
||||||
|
if crate::DEFAULT_BACKFILL_MAX_CANDIDATES == 0 || crate::DEFAULT_BACKFILL_MAX_CANDIDATES > max_candidates {
|
||||||
|
return std::result::Result::Err(limit_contract_error("default_max_candidates"));
|
||||||
|
}
|
||||||
|
if crate::DEFAULT_BACKFILL_MAX_PAGES == 0 || crate::DEFAULT_BACKFILL_MAX_PAGES > max_pages {
|
||||||
|
return std::result::Result::Err(limit_contract_error("default_max_pages"));
|
||||||
|
}
|
||||||
|
if crate::DEFAULT_BACKFILL_PAGE_SIZE == 0 || crate::DEFAULT_BACKFILL_PAGE_SIZE > max_page_size {
|
||||||
|
return std::result::Result::Err(limit_contract_error("default_page_size"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(BackfillRequestLimitsDto {
|
||||||
|
default_hydration_concurrency: crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY,
|
||||||
|
default_max_candidates: crate::DEFAULT_BACKFILL_MAX_CANDIDATES,
|
||||||
|
default_max_pages: crate::DEFAULT_BACKFILL_MAX_PAGES,
|
||||||
|
default_page_size: crate::DEFAULT_BACKFILL_PAGE_SIZE,
|
||||||
|
max_hydration_concurrency,
|
||||||
|
max_candidates,
|
||||||
|
max_pages,
|
||||||
|
max_page_size,
|
||||||
|
max_signature_text_bytes,
|
||||||
|
min_signature_text_bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limit_contract_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk request-limit projection is internally inconsistent")
|
||||||
|
.with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usize_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, "Backfill Desk cannot project a Job-owned bound to the frontend")
|
||||||
|
.with_context("field", field)
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/dto_backfill.rs"]
|
||||||
|
mod tests;
|
||||||
144
crates/ksp-app-backfill-desk/src/dto_common.rs
Normal file
144
crates/ksp-app-backfill-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/dto_common.rs
|
||||||
|
// version: 7
|
||||||
|
|
||||||
|
//! Common Tauri DTOs shared by the Backfill Desk shell.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Safe HTTP route exposed for operator selection before a Backfill run.
|
||||||
|
///
|
||||||
|
/// The route is identified only by the logical Transport role. Provider labels are safe metadata used for operator choice; endpoint URLs and credentials never
|
||||||
|
/// cross IPC.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts")]
|
||||||
|
pub(crate) struct BackfillHttpRouteOptionDto {
|
||||||
|
/// Whether more than one configured provider participates in this logical route.
|
||||||
|
pub(crate) pooled: bool,
|
||||||
|
/// Safe provider labels participating in the route, sorted and deduplicated.
|
||||||
|
pub(crate) providers: std::vec::Vec<String>,
|
||||||
|
/// Logical Transport role later passed to the Backfill runtime.
|
||||||
|
pub(crate) role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe Program ID entry exposed only to populate the free-form address autocomplete dataset.
|
||||||
|
///
|
||||||
|
/// Every value is derived from the canonical `ksp-core-lib` registry. The frontend remains free to submit any valid Solana address; this DTO is advisory only.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts")]
|
||||||
|
pub(crate) struct ProgramIdAutocompleteOptionDto {
|
||||||
|
/// Stable KSP machine-readable Program ID code.
|
||||||
|
pub(crate) code: String,
|
||||||
|
/// Broad functional domain from the canonical registry.
|
||||||
|
pub(crate) domain: String,
|
||||||
|
/// Functional family from the canonical registry.
|
||||||
|
pub(crate) family: String,
|
||||||
|
/// Human-readable program name.
|
||||||
|
pub(crate) name: String,
|
||||||
|
/// Canonical Base58 Program ID used as the HTML datalist value.
|
||||||
|
pub(crate) program_id: String,
|
||||||
|
/// Owning protocol/project label from the canonical registry.
|
||||||
|
pub(crate) protocol: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Projects the canonical KSP Program ID registry to the safe autocomplete dataset.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn program_id_autocomplete_options() -> std::vec::Vec<crate::ProgramIdAutocompleteOptionDto> {
|
||||||
|
let entries = ksp_core_lib::entries();
|
||||||
|
let mut options = std::vec::Vec::with_capacity(entries.len());
|
||||||
|
for entry in entries {
|
||||||
|
options.push(crate::ProgramIdAutocompleteOptionDto {
|
||||||
|
code: entry.code().to_owned(),
|
||||||
|
domain: entry.domain().to_owned(),
|
||||||
|
family: entry.family().to_owned(),
|
||||||
|
name: entry.name().to_owned(),
|
||||||
|
program_id: entry.program_id().to_owned(),
|
||||||
|
protocol: entry.protocol().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds.
|
||||||
|
///
|
||||||
|
/// Endpoint URLs, credentials and physical routing details are intentionally absent.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts")]
|
||||||
|
pub(crate) struct BackfillDeskOptionsDto {
|
||||||
|
/// Stable commitment codes currently admitted by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) commitments: std::vec::Vec<String>,
|
||||||
|
/// Selectable logical HTTP routes that support both RPC methods required by the Backfill runtime.
|
||||||
|
pub(crate) http_routes: std::vec::Vec<BackfillHttpRouteOptionDto>,
|
||||||
|
/// Backend-owned maxima plus application defaults used by the campaign form.
|
||||||
|
pub(crate) limits: crate::BackfillRequestLimitsDto,
|
||||||
|
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
|
||||||
|
pub(crate) composition_ready: bool,
|
||||||
|
/// Canonical KSP Program IDs exposed as a frontend autocomplete dataset; never an allow-list.
|
||||||
|
pub(crate) program_id_options: std::vec::Vec<crate::ProgramIdAutocompleteOptionDto>,
|
||||||
|
/// Distinct enabled HTTP cluster labels selected by the active Transport profile.
|
||||||
|
pub(crate) configured_networks: std::vec::Vec<String>,
|
||||||
|
/// Whether the selected Store network exactly matches the one coherent Transport network.
|
||||||
|
pub(crate) network_coherent: bool,
|
||||||
|
/// Safe startup diagnostic when Store configuration, opening or readiness could not be proven.
|
||||||
|
pub(crate) store_diagnostic: std::option::Option<CommandErrorDto>,
|
||||||
|
/// Logical network selected by the active Store profile, without backend connection details.
|
||||||
|
pub(crate) store_network: std::option::Option<String>,
|
||||||
|
/// Whether Store opened successfully and its bounded health probe proved readiness.
|
||||||
|
pub(crate) store_ready: bool,
|
||||||
|
/// Safe startup diagnostic when Transport configuration could not be resolved or constructed.
|
||||||
|
pub(crate) transport_diagnostic: std::option::Option<CommandErrorDto>,
|
||||||
|
/// Stable HTTP scope codes currently admitted by `ksp-job-backfill-lib`.
|
||||||
|
pub(crate) scope_kinds: std::vec::Vec<String>,
|
||||||
|
/// Whether Transport currently has one coherent network and at least one compatible available role.
|
||||||
|
pub(crate) transport_ready: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_backfill_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 scaffold/runtime snapshot exposed to the Backfill Desk shell.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts")]
|
||||||
|
pub(crate) struct ShellStatusDto {
|
||||||
|
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
|
||||||
|
pub(crate) active_logging_profile: std::option::Option<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 Backfill Desk had to install its transient in-memory Logging fallback.
|
||||||
|
pub(crate) fallback_logging_active: bool,
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/dto_common.rs"]
|
||||||
|
mod tests;
|
||||||
49
crates/ksp-app-backfill-desk/src/errors.rs
Normal file
49
crates/ksp-app-backfill-desk/src/errors.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/errors.rs
|
||||||
|
// version: 9
|
||||||
|
|
||||||
|
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
|
||||||
|
|
||||||
|
/// Shared Backfill Desk runtime state is internally inconsistent.
|
||||||
|
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid");
|
||||||
|
/// Shared Backfill 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("backfill_desk", "app_state_lock_failed");
|
||||||
|
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
|
||||||
|
/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
|
||||||
|
/// Backfill Desk has no retained safe checkpoint/request pair available for in-session Resume.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_resume_unavailable");
|
||||||
|
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_RUN_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_active");
|
||||||
|
/// Backfill Desk received a stale or mismatched Job identity for an active-run control operation.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_RUN_MISMATCH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_mismatch");
|
||||||
|
/// Backfill Desk has no active or retained matching run for the requested control operation.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_not_active");
|
||||||
|
/// Backfill Desk composite configuration is missing or references an unexpected document.
|
||||||
|
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_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("backfill_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("backfill_desk", "frontend_log_target_invalid");
|
||||||
|
/// Backfill 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("backfill_desk", "logging_bootstrap_failed");
|
||||||
|
/// Splash readiness was invoked from a window other than the splash window.
|
||||||
|
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_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("backfill_desk", "splash_setting_invalid");
|
||||||
|
/// Backfill Desk Store and Transport configuration target different logical networks.
|
||||||
|
pub(crate) const ERROR_CODE_STORE_NETWORK_MISMATCH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_network_mismatch");
|
||||||
|
/// Backfill Desk cannot prove a single Transport network before Store opening.
|
||||||
|
pub(crate) const ERROR_CODE_STORE_NETWORK_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_network_unavailable");
|
||||||
|
/// Backfill Desk cannot complete the bounded Store shutdown lifecycle.
|
||||||
|
pub(crate) const ERROR_CODE_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_shutdown_failed");
|
||||||
|
/// Tauri runtime assembly or execution failed.
|
||||||
|
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_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("backfill_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("backfill_desk", "tauri_window_operation_failed");
|
||||||
|
/// Backfill Desk cannot derive a safe Transport readiness contract from the Transport registry.
|
||||||
|
pub(crate) const ERROR_CODE_TRANSPORT_READINESS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "transport_readiness_invalid");
|
||||||
205
crates/ksp-app-backfill-desk/src/frontend_logging.rs
Normal file
205
crates/ksp-app-backfill-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/frontend_logging.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! KSP-owned bridge for technical log events emitted by Backfill Desk frontend scripts.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Log payload sent by Backfill Desk frontend scripts.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_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;
|
||||||
192
crates/ksp-app-backfill-desk/src/lib.rs
Normal file
192
crates/ksp-app-backfill-desk/src/lib.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
||||||
|
// version: 11
|
||||||
|
|
||||||
|
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
mod app_state;
|
||||||
|
mod backfill_request;
|
||||||
|
mod backfill_run;
|
||||||
|
mod backfill_status;
|
||||||
|
mod bootstrap;
|
||||||
|
mod constants;
|
||||||
|
mod dto_backfill;
|
||||||
|
mod dto_common;
|
||||||
|
mod errors;
|
||||||
|
mod frontend_logging;
|
||||||
|
mod logging_runtime;
|
||||||
|
mod splash;
|
||||||
|
mod store_runtime;
|
||||||
|
mod tauri;
|
||||||
|
mod transport_runtime;
|
||||||
|
mod tw_main;
|
||||||
|
mod tw_splash;
|
||||||
|
|
||||||
|
/// Runs the KSP Backfill desktop application.
|
||||||
|
pub use self::tauri::run;
|
||||||
|
|
||||||
|
/// Shared Backfill Desk application state managed by Tauri.
|
||||||
|
pub(crate) use self::app_state::AppState;
|
||||||
|
/// Maps one app-owned campaign DTO to the validated KSP Backfill request contract.
|
||||||
|
pub(crate) use self::backfill_request::map_backfill_request;
|
||||||
|
/// Projects one validated Backfill request without returning address or signature values.
|
||||||
|
pub(crate) use self::backfill_request::project_backfill_request;
|
||||||
|
/// Fully prepared Backfill launch moved into the async runtime after single-run admission.
|
||||||
|
pub(crate) use self::backfill_run::BackfillRunLaunch;
|
||||||
|
/// Single-active-run admission state retained by the application.
|
||||||
|
pub(crate) use self::backfill_run::BackfillRunState;
|
||||||
|
/// Stable Tauri event carrying coalesced latest-value Backfill status.
|
||||||
|
pub(crate) use self::backfill_status::BACKFILL_STATUS_EVENT_NAME;
|
||||||
|
/// Frontend-safe latest-value Backfill runtime projection.
|
||||||
|
pub(crate) use self::backfill_status::BackfillRunStatusDto;
|
||||||
|
/// Projects one complete Job notification to the safe desktop monitoring contract.
|
||||||
|
pub(crate) use self::backfill_status::project_backfill_status;
|
||||||
|
/// 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 Logging from the Backfill Desk composite, with a trace-level development fallback.
|
||||||
|
pub(crate) use self::bootstrap::initialize_logging;
|
||||||
|
/// Loads the concrete Backfill Desk Config composite.
|
||||||
|
pub(crate) use self::bootstrap::load_backfill_desk_composite;
|
||||||
|
/// Returns one required component profile from the Backfill Desk composite.
|
||||||
|
pub(crate) use self::bootstrap::required_composite_component_profile;
|
||||||
|
/// Composite component identifier for Logging.
|
||||||
|
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
|
||||||
|
/// Composite component identifier for Store.
|
||||||
|
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
|
||||||
|
/// Composite component identifier for Transport.
|
||||||
|
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
|
||||||
|
/// Initial hydration concurrency proposed by the Backfill campaign form.
|
||||||
|
pub(crate) use self::constants::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY;
|
||||||
|
/// Initial candidate cap proposed by the Backfill campaign form.
|
||||||
|
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_CANDIDATES;
|
||||||
|
/// Initial discovery-page cap proposed by the Backfill campaign form.
|
||||||
|
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_PAGES;
|
||||||
|
/// Initial signature page size proposed by the Backfill campaign form.
|
||||||
|
pub(crate) use self::constants::DEFAULT_BACKFILL_PAGE_SIZE;
|
||||||
|
/// 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 Backfill campaign admission and request mapping.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
|
||||||
|
/// Structured domain used by concrete Backfill run admission and execution.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_RUN;
|
||||||
|
/// Structured domain used by the Backfill Desk shell.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||||
|
/// Structured domain used by Store readiness and shutdown operations.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_STORE;
|
||||||
|
/// Structured domain used by Transport readiness and role inventory operations.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT;
|
||||||
|
/// Structured domain used by Tauri window lifecycle operations.
|
||||||
|
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||||
|
/// Owning target for backend events emitted by Backfill 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 acknowledgement returned by targeted cooperative cancellation.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillCancelResponseDto;
|
||||||
|
/// Backend-derived request bounds and Desk defaults for the campaign form.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
|
||||||
|
/// Safe request preview produced after strict backend mapping.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
|
||||||
|
/// Safe acknowledgement returned after in-session checkpoint Resume admission.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillResumeResponseDto;
|
||||||
|
/// App-owned campaign request received from the frontend.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
|
||||||
|
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillStartResponseDto;
|
||||||
|
/// Returns commitment codes admitted by the current Backfill contract.
|
||||||
|
pub(crate) use self::dto_backfill::backfill_commitment_codes;
|
||||||
|
/// Builds frontend-safe campaign limits from Job-owned public constants.
|
||||||
|
pub(crate) use self::dto_backfill::backfill_request_limits;
|
||||||
|
/// Returns scope kind codes admitted by the current Backfill contract.
|
||||||
|
pub(crate) use self::dto_backfill::backfill_scope_kind_codes;
|
||||||
|
/// Safe Backfill Desk options contract combining readiness and campaign metadata.
|
||||||
|
pub(crate) use self::dto_common::BackfillDeskOptionsDto;
|
||||||
|
/// Safe logical HTTP route exposed for operator selection.
|
||||||
|
pub(crate) use self::dto_common::BackfillHttpRouteOptionDto;
|
||||||
|
/// Safe command error projection exposed to Tauri commands.
|
||||||
|
pub(crate) use self::dto_common::CommandErrorDto;
|
||||||
|
/// Safe canonical Program ID entry exposed only for free-form address autocomplete.
|
||||||
|
pub(crate) use self::dto_common::ProgramIdAutocompleteOptionDto;
|
||||||
|
/// Safe scaffold/runtime snapshot exposed to the shell.
|
||||||
|
pub(crate) use self::dto_common::ShellStatusDto;
|
||||||
|
/// Projects the canonical Core Program ID registry to autocomplete entries.
|
||||||
|
pub(crate) use self::dto_common::program_id_autocomplete_options;
|
||||||
|
/// Shared Backfill Desk runtime state is internally inconsistent.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||||
|
/// Shared Backfill Desk runtime state cannot be locked safely.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||||
|
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
|
||||||
|
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
|
||||||
|
/// Backfill Desk has no retained safe checkpoint available for in-session Resume.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE;
|
||||||
|
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_ACTIVE;
|
||||||
|
/// Backfill Desk rejects stale or mismatched Job identity control requests.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_MISMATCH;
|
||||||
|
/// Backfill Desk has no active or retained matching run for the requested control operation.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE;
|
||||||
|
/// Backfill Desk composite configuration is missing or references an unexpected document.
|
||||||
|
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;
|
||||||
|
/// Backfill Desk could not install the managed Logging runtime or its safe fallback.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||||
|
/// Splash readiness was invoked from a window other than the splash window.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||||
|
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||||
|
/// Backfill Desk Store and Transport configuration target different logical networks.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_STORE_NETWORK_MISMATCH;
|
||||||
|
/// Backfill Desk cannot prove a single Transport network before Store opening.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_STORE_NETWORK_UNAVAILABLE;
|
||||||
|
/// Backfill Desk cannot complete the bounded Store shutdown lifecycle.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_STORE_SHUTDOWN_FAILED;
|
||||||
|
/// 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;
|
||||||
|
/// Backfill Desk cannot derive a safe Transport readiness contract.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_TRANSPORT_READINESS_INVALID;
|
||||||
|
/// Log payload sent by Backfill 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 Backfill Desk process launch.
|
||||||
|
pub(crate) use self::logging_runtime::launch_identity;
|
||||||
|
/// Command emitted by Rust to the splash frontend.
|
||||||
|
pub(crate) use self::splash::SplashOrderDto;
|
||||||
|
/// Runtime timings used by the common desk splash lifecycle.
|
||||||
|
pub(crate) use self::splash::SplashSettings;
|
||||||
|
/// Store startup state retained by the application shell.
|
||||||
|
pub(crate) use self::store_runtime::StoreStartup;
|
||||||
|
/// Initializes Store only after composite-selected Transport/Store network coherence is proven.
|
||||||
|
pub(crate) use self::store_runtime::initialize_store;
|
||||||
|
/// Safe and executable Transport runtime retained by application state.
|
||||||
|
pub(crate) use self::transport_runtime::TransportRuntime;
|
||||||
|
/// Initializes the composite-selected HTTP Transport runtime.
|
||||||
|
pub(crate) use self::transport_runtime::initialize_transport;
|
||||||
|
/// Resolves the required main window or returns a typed error.
|
||||||
|
pub(crate) use self::tw_main::require_main_window;
|
||||||
|
/// Shows and focuses the main Backfill 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-backfill-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-backfill-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/logging_runtime.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Backfill Desk Logging runtime identity helpers.
|
||||||
|
|
||||||
|
/// Creates the stable runtime identity for this Backfill 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-backfill-desk/src/main.rs
Normal file
58
crates/ksp-app-backfill-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/main.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Binary entry point for the KSP Backfill 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 Backfill 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_backfill_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-backfill-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_backfill_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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
214
crates/ksp-app-backfill-desk/src/splash.rs
Normal file
214
crates/ksp-app-backfill-desk/src/splash.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Common splash settings and frontend event contracts for Backfill Desk.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
|
||||||
|
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
|
||||||
|
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||||
|
const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
|
||||||
|
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
|
||||||
|
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||||
|
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||||
|
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||||
|
|
||||||
|
/// Runtime timings used by the common desk splash lifecycle.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct SplashSettings {
|
||||||
|
fade_in_ms: u32,
|
||||||
|
minimum_ms: u64,
|
||||||
|
fade_out_ms: u32,
|
||||||
|
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||||
|
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||||
|
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SplashSettings {
|
||||||
|
/// Resolves splash timings through the Config-owned environment snapshot.
|
||||||
|
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||||
|
let environment = match environment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
|
||||||
|
let fade_in = match fade_in {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
|
||||||
|
let minimum = match minimum {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
|
||||||
|
let fade_out = match fade_out {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
|
||||||
|
let fade_in_ms = match fade_in_ms {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
|
||||||
|
let minimum_ms = match minimum_ms {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
|
||||||
|
let fade_out_ms = match fade_out_ms {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(Self {
|
||||||
|
fade_in_ms,
|
||||||
|
minimum_ms,
|
||||||
|
fade_out_ms,
|
||||||
|
fade_in_source: fade_in.source(),
|
||||||
|
minimum_source: minimum.source(),
|
||||||
|
fade_out_source: fade_out.source(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fallback() -> Self {
|
||||||
|
return Self {
|
||||||
|
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
|
||||||
|
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||||
|
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
|
||||||
|
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||||
|
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||||
|
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configured fade-in duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_in_ms(self) -> u32 {
|
||||||
|
return self.fade_in_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum fully-visible splash duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||||
|
return self.minimum_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configured fade-out duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_out_ms(self) -> u32 {
|
||||||
|
return self.fade_out_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe provenance code for the fade-in duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_in_source(self) -> &'static str {
|
||||||
|
return environment_source_code(self.fade_in_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe provenance code for the minimum duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn minimum_source(self) -> &'static str {
|
||||||
|
return environment_source_code(self.minimum_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe provenance code for the fade-out duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_out_source(self) -> &'static str {
|
||||||
|
return environment_source_code(self.fade_out_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the expected additive backend lifecycle from readiness through fade-out.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||||
|
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command emitted by Rust to the splash frontend.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/splash/SplashOrderDto.ts")]
|
||||||
|
pub(crate) struct SplashOrderDto {
|
||||||
|
/// Splash action name.
|
||||||
|
pub(crate) action: String,
|
||||||
|
/// Optional status text.
|
||||||
|
pub(crate) message: std::option::Option<String>,
|
||||||
|
/// Optional message severity used by the bottom message feed.
|
||||||
|
pub(crate) status: std::option::Option<String>,
|
||||||
|
/// Optional animation duration in milliseconds.
|
||||||
|
pub(crate) duration_ms: std::option::Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SplashOrderDto {
|
||||||
|
/// Creates a new `SplashOrderDto` value.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||||
|
return Self {
|
||||||
|
action: action.to_owned(),
|
||||||
|
message: message.map(std::string::ToString::to_string),
|
||||||
|
status: status.map(std::string::ToString::to_string),
|
||||||
|
duration_ms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||||
|
return match source {
|
||||||
|
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||||
|
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||||
|
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||||
|
let parsed = value.parse::<u64>();
|
||||||
|
let parsed = match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||||
|
.with_context("variable_name", variable_name)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if parsed > maximum {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||||
|
.with_context("variable_name", variable_name)
|
||||||
|
.with_context("maximum_ms", maximum.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
|
||||||
|
let parsed = value.parse::<u32>();
|
||||||
|
let parsed = match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||||
|
.with_context("variable_name", variable_name)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if parsed > maximum {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||||
|
.with_context("variable_name", variable_name)
|
||||||
|
.with_context("maximum_ms", maximum.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/splash.rs"]
|
||||||
|
mod tests;
|
||||||
288
crates/ksp-app-backfill-desk/src/store_runtime.rs
Normal file
288
crates/ksp-app-backfill-desk/src/store_runtime.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/store_runtime.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Composite-selected Store readiness and shutdown lifecycle owned by Backfill Desk.
|
||||||
|
|
||||||
|
/// Startup result for the Store layer, including safe diagnostics when the desktop shell remains available without a ready Store.
|
||||||
|
pub(crate) struct StoreStartup {
|
||||||
|
diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||||
|
network_coherent: bool,
|
||||||
|
runtime: std::option::Option<StoreRuntime>,
|
||||||
|
store_network: std::option::Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreStartup {
|
||||||
|
/// Applies Store readiness to the application-owned options DTO without exposing backend connection details.
|
||||||
|
pub(crate) fn apply_to(&self, options: &mut crate::BackfillDeskOptionsDto) {
|
||||||
|
options.network_coherent = self.network_coherent;
|
||||||
|
options.store_diagnostic = self.diagnostic.clone();
|
||||||
|
options.store_network = self.store_network.clone();
|
||||||
|
options.store_ready = self.runtime.as_ref().is_some_and(StoreRuntime::health_ready);
|
||||||
|
options.composition_ready = options.transport_ready && options.network_coherent && options.store_ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily lends the ready Store facade to one admitted Backfill execution.
|
||||||
|
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
|
||||||
|
let runtime = self.runtime.as_ref();
|
||||||
|
let runtime = match runtime {
|
||||||
|
std::option::Option::Some(value) if value.health_ready() => value,
|
||||||
|
_ => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot start a Backfill run without a ready Store",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return runtime.take_for_run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the Store facade after one Backfill execution reaches its terminal result.
|
||||||
|
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
|
||||||
|
let runtime = self.runtime.as_ref();
|
||||||
|
let runtime = match runtime {
|
||||||
|
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,
|
||||||
|
"Backfill Desk cannot restore Store after runtime ownership disappeared",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return runtime.restore_after_run(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
|
||||||
|
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let runtime = self.runtime.as_ref();
|
||||||
|
return match runtime {
|
||||||
|
std::option::Option::Some(value) => value.close().await,
|
||||||
|
std::option::Option::None => std::result::Result::Ok(()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executable Store runtime retained after network coherence and bounded Store health checks.
|
||||||
|
pub(crate) struct StoreRuntime {
|
||||||
|
health_ready: bool,
|
||||||
|
profile_id: String,
|
||||||
|
store: std::sync::Mutex<std::option::Option<ksp_store_lib::Store>>,
|
||||||
|
store_network: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreRuntime {
|
||||||
|
/// Returns whether the startup health probe proved the Store ready.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn health_ready(&self) -> bool {
|
||||||
|
return self.health_ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily removes the Store from the application slot for one single-active Backfill execution.
|
||||||
|
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
|
||||||
|
let store = take_store(&self.store);
|
||||||
|
let store = match store {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return match store {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||||
|
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
|
||||||
|
"Backfill Desk Store is already owned by an active Backfill run",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the Store after one terminal Backfill execution so a later campaign can be admitted.
|
||||||
|
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
|
||||||
|
let locked = self.store.lock();
|
||||||
|
let mut locked = match locked {
|
||||||
|
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,
|
||||||
|
"Backfill Desk Store runtime state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if locked.is_some() {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||||
|
"Backfill Desk cannot restore Store into an occupied runtime slot",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
*locked = std::option::Option::Some(store);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly closes the Store exactly once through its backend-neutral facade.
|
||||||
|
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let store = take_store(&self.store);
|
||||||
|
let store = match store {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let store = match store {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Ok(()),
|
||||||
|
};
|
||||||
|
let closed = store.close().await;
|
||||||
|
return match closed {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_STORE,
|
||||||
|
store_profile = self.profile_id.as_str(),
|
||||||
|
store_network = self.store_network.as_str(),
|
||||||
|
"closed Backfill Desk Store runtime"
|
||||||
|
);
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_SHUTDOWN_FAILED, "Backfill Desk Store shutdown failed").with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the composite Store target, proves Transport/Store network coherence before Store I/O, opens Store and captures one health probe.
|
||||||
|
pub(crate) async fn initialize_store(
|
||||||
|
management: &ksp_config_lib::ConfigManagement,
|
||||||
|
transport_runtime: std::option::Option<&crate::TransportRuntime>,
|
||||||
|
) -> StoreStartup {
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||||
|
let environment = match environment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
|
||||||
|
};
|
||||||
|
let composite = crate::load_backfill_desk_composite(management);
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
|
||||||
|
};
|
||||||
|
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||||
|
let profile = match profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
|
||||||
|
};
|
||||||
|
let resolved = management.engine().resolve_store_config_profile(&profile, &environment);
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
|
||||||
|
};
|
||||||
|
let profile_id = resolved.profile_id().to_owned();
|
||||||
|
let store_network = resolved.settings().network().as_str().to_owned();
|
||||||
|
let transport_network = transport_runtime.and_then(crate::TransportRuntime::coherent_network);
|
||||||
|
let transport_network = match transport_network {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
let error = ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_STORE_NETWORK_UNAVAILABLE,
|
||||||
|
"Backfill Desk cannot open Store before one coherent Transport network is available",
|
||||||
|
);
|
||||||
|
return unavailable_startup(std::option::Option::Some(store_network), false, error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let coherence = validate_network_coherence(store_network.as_str(), transport_network.as_str());
|
||||||
|
if let std::result::Result::Err(error) = coherence {
|
||||||
|
return unavailable_startup(std::option::Option::Some(store_network), false, error);
|
||||||
|
}
|
||||||
|
let store = ksp_store_lib::Store::open(resolved.into_settings()).await;
|
||||||
|
let store = match store {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::Some(store_network), true, error),
|
||||||
|
};
|
||||||
|
let health = store.health().await;
|
||||||
|
let health_ready = store_health_ready(health.state());
|
||||||
|
let diagnostic = if health_ready { std::option::Option::None } else { std::option::Option::Some(non_ready_health_diagnostic(&health)) };
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_STORE,
|
||||||
|
store_profile = profile_id.as_str(),
|
||||||
|
store_network = store_network.as_str(),
|
||||||
|
store_ready = health_ready,
|
||||||
|
pending_migration_count = health.pending_migration_count(),
|
||||||
|
"initialized Backfill Desk Store readiness from composite-managed configuration"
|
||||||
|
);
|
||||||
|
return StoreStartup {
|
||||||
|
diagnostic,
|
||||||
|
network_coherent: true,
|
||||||
|
runtime: std::option::Option::Some(StoreRuntime {
|
||||||
|
health_ready,
|
||||||
|
profile_id,
|
||||||
|
store: std::sync::Mutex::new(std::option::Option::Some(store)),
|
||||||
|
store_network: store_network.clone(),
|
||||||
|
}),
|
||||||
|
store_network: std::option::Option::Some(store_network),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_ready_health_diagnostic(health: &ksp_store_lib::StoreHealthSnapshot) -> crate::CommandErrorDto {
|
||||||
|
let code = health.last_error_code();
|
||||||
|
return match code {
|
||||||
|
std::option::Option::Some(value) => crate::CommandErrorDto {
|
||||||
|
code: value.code().to_owned(),
|
||||||
|
domain: value.domain().to_owned(),
|
||||||
|
message: "Store health probe did not prove readiness".to_owned(),
|
||||||
|
},
|
||||||
|
std::option::Option::None => crate::CommandErrorDto {
|
||||||
|
code: "store_not_ready".to_owned(),
|
||||||
|
domain: "backfill_desk".to_owned(),
|
||||||
|
message: "Store health probe did not prove readiness".to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_health_ready(state: ksp_store_lib::StoreHealthState) -> bool {
|
||||||
|
return match state {
|
||||||
|
ksp_store_lib::StoreHealthState::Ready => true,
|
||||||
|
ksp_store_lib::StoreHealthState::NotReady => false,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_store(store: &std::sync::Mutex<std::option::Option<ksp_store_lib::Store>>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::Store>> {
|
||||||
|
let locked = store.lock();
|
||||||
|
let mut locked = match locked {
|
||||||
|
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,
|
||||||
|
"Backfill Desk Store runtime state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(locked.take());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unavailable_startup(store_network: std::option::Option<String>, network_coherent: bool, error: ksp_core_lib::Error) -> StoreStartup {
|
||||||
|
let diagnostic = crate::CommandErrorDto::from_error(&error);
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_STORE,
|
||||||
|
error_domain = diagnostic.domain.as_str(),
|
||||||
|
error_code = diagnostic.code.as_str(),
|
||||||
|
network_coherent = network_coherent,
|
||||||
|
"Backfill Desk Store readiness is unavailable; keeping desktop shell available"
|
||||||
|
);
|
||||||
|
return StoreStartup {
|
||||||
|
diagnostic: std::option::Option::Some(diagnostic),
|
||||||
|
network_coherent,
|
||||||
|
runtime: std::option::Option::None,
|
||||||
|
store_network,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_network_coherence(store_network: &str, transport_network: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if store_network == transport_network {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_NETWORK_MISMATCH, "Backfill Desk Store and Transport networks do not match")
|
||||||
|
.with_context("store_network", store_network)
|
||||||
|
.with_context("transport_network", transport_network),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/store_runtime.rs"]
|
||||||
|
mod tests;
|
||||||
343
crates/ksp-app-backfill-desk/src/tauri.rs
Normal file
343
crates/ksp-app-backfill-desk/src/tauri.rs
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
||||||
|
// version: 9
|
||||||
|
|
||||||
|
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
||||||
|
|
||||||
|
use tauri::Emitter; // rust-rules: trait-import
|
||||||
|
use tauri::Manager; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Runs the Backfill desktop application.
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||||
|
let context = tauri::generate_context!();
|
||||||
|
let runtime_layout = configure_packaged_runtime(&context);
|
||||||
|
if let std::result::Result::Err(error) = runtime_layout {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let app_state = crate::AppState::initialize(arguments);
|
||||||
|
let app_state = match app_state {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut builder = tauri::Builder::default();
|
||||||
|
builder = configure_state(builder, app_state);
|
||||||
|
builder = configure_plugins(builder);
|
||||||
|
builder = configure_commands(builder);
|
||||||
|
builder = configure_setup(builder);
|
||||||
|
builder = configure_window_events(builder);
|
||||||
|
let run_result = builder.run(context);
|
||||||
|
return match run_result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Backfill Desk Tauri runtime")
|
||||||
|
.with_context("tauri_error", error.to_string()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
|
||||||
|
let resource_root = match resource_root {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
|
||||||
|
.with_context("tauri_error", error.to_string()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
|
||||||
|
let layout = match layout {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let working_directory = std::env::set_current_dir(layout.runtime_root());
|
||||||
|
return match working_directory {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
|
||||||
|
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
|
||||||
|
return builder.manage(app_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
|
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
|
||||||
|
return builder.plugin(tracing_plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||||
|
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
|
return builder.invoke_handler(tauri::generate_handler![
|
||||||
|
backfill_cancel,
|
||||||
|
backfill_options,
|
||||||
|
backfill_resume,
|
||||||
|
backfill_start,
|
||||||
|
backfill_status,
|
||||||
|
backfill_validate_request,
|
||||||
|
emit_frontend_log,
|
||||||
|
get_runtime_status,
|
||||||
|
splash_frontend_ready
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
|
return builder.setup(|app| {
|
||||||
|
let splash = crate::require_splash_window(app);
|
||||||
|
if let std::result::Result::Err(error) = splash {
|
||||||
|
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||||
|
}
|
||||||
|
let main = crate::require_main_window(app);
|
||||||
|
if let std::result::Result::Err(error) = main {
|
||||||
|
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Backfill Desk Tauri shell setup completed");
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
|
return builder.on_window_event(|window, event| {
|
||||||
|
if window.label() != "main" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
api.prevent_close();
|
||||||
|
let app_handle = window.app_handle().clone();
|
||||||
|
let state = app_handle.state::<crate::AppState>();
|
||||||
|
if !state.begin_shutdown() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancellation = state.cancel_active_backfill_for_shutdown();
|
||||||
|
match cancellation {
|
||||||
|
std::result::Result::Ok(accepted) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
cancellation_accepted = accepted,
|
||||||
|
"Backfill Desk main-window close requested; cooperative Backfill cancellation attempted before bounded Store shutdown"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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(),
|
||||||
|
"Backfill Desk could not request cooperative cancellation during shutdown; continuing bounded shutdown"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let state = app_handle.state::<crate::AppState>();
|
||||||
|
let closed = state.close_store().await;
|
||||||
|
match closed {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
"Backfill Desk Store shutdown completed; exiting application"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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(),
|
||||||
|
"Backfill Desk Store shutdown failed; exiting application after bounded close attempt"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app_handle.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = domain,
|
||||||
|
command = command,
|
||||||
|
error_domain = error.code().domain(),
|
||||||
|
error_code = error.code().code(),
|
||||||
|
"Backfill Desk Tauri command failed"
|
||||||
|
);
|
||||||
|
return crate::CommandErrorDto::from_error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_cancel(job_id: String, state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::BackfillCancelResponseDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.cancel_backfill(job_id.as_str());
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_cancel", crate::TRACING_DOMAIN_RUN, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::BackfillDeskOptionsDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.backfill_options();
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_options", crate::TRACING_DOMAIN_TRANSPORT, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_start(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
request: crate::BackfillStartRequestDto,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::BackfillStartResponseDto, crate::CommandErrorDto> {
|
||||||
|
let launch = state.prepare_backfill_start(request);
|
||||||
|
let launch = match launch {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(project_command_error("backfill_start", crate::TRACING_DOMAIN_RUN, &error));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let response = launch.response();
|
||||||
|
spawn_backfill_launch(app, launch);
|
||||||
|
return std::result::Result::Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_resume(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::BackfillResumeResponseDto, crate::CommandErrorDto> {
|
||||||
|
let launch = state.prepare_backfill_resume();
|
||||||
|
let launch = match launch {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(project_command_error("backfill_resume", crate::TRACING_DOMAIN_RUN, &error));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let response = crate::BackfillResumeResponseDto {
|
||||||
|
accepted: true,
|
||||||
|
job_id: launch.job_id.as_str().to_owned(),
|
||||||
|
state: ksp_job_api::JobState::Created.code().to_owned(),
|
||||||
|
};
|
||||||
|
spawn_backfill_launch(app, launch);
|
||||||
|
return std::result::Result::Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_backfill_launch(app: tauri::AppHandle, launch: crate::BackfillRunLaunch) {
|
||||||
|
let monitor_app = app.clone();
|
||||||
|
let monitor_source = launch.snapshots.clone();
|
||||||
|
let _monitor_task = tauri::async_runtime::spawn(async move {
|
||||||
|
monitor_backfill_status(monitor_app, monitor_source).await;
|
||||||
|
});
|
||||||
|
let _run_task = tauri::async_runtime::spawn(async move {
|
||||||
|
let state = app.state::<crate::AppState>();
|
||||||
|
let executed = state.execute_backfill_run(launch).await;
|
||||||
|
if let std::result::Result::Err(error) = executed {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
error_domain = error.code().domain(),
|
||||||
|
error_code = error.code().code(),
|
||||||
|
"Backfill Desk async run terminated with an error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
|
||||||
|
let mut notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||||
|
loop {
|
||||||
|
let projected = crate::project_backfill_status(¬ification);
|
||||||
|
match projected {
|
||||||
|
std::result::Result::Ok(status) => {
|
||||||
|
let main = app.get_webview_window("main");
|
||||||
|
if let std::option::Option::Some(window) = main {
|
||||||
|
let emitted = window.emit(crate::BACKFILL_STATUS_EVENT_NAME, status);
|
||||||
|
if emitted.is_err() {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
"Backfill Desk could not emit latest-value monitoring status to the main window"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_RUN,
|
||||||
|
error_domain = error.code().domain(),
|
||||||
|
error_code = error.code().code(),
|
||||||
|
"Backfill Desk could not project latest-value monitoring status"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if notification.state().is_terminal() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let observed = notification.sequence();
|
||||||
|
notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::wait_for_change(&source, observed).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::option::Option<crate::BackfillRunStatusDto>, crate::CommandErrorDto> {
|
||||||
|
let result = state.backfill_status();
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_status", crate::TRACING_DOMAIN_RUN, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_validate_request(
|
||||||
|
request: crate::BackfillStartRequestDto,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::BackfillRequestPreviewDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.validate_backfill_request(request);
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_validate_request", crate::TRACING_DOMAIN_REQUEST, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||||
|
let result = crate::emit_frontend_log_event(payload);
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.shell_status();
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn splash_frontend_ready(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
webview_window: tauri::WebviewWindow,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||||
|
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
179
crates/ksp-app-backfill-desk/src/transport_runtime.rs
Normal file
179
crates/ksp-app-backfill-desk/src/transport_runtime.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
|
||||||
|
// version: 6
|
||||||
|
|
||||||
|
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
|
||||||
|
|
||||||
|
/// Safe and executable Transport runtime retained by the application state.
|
||||||
|
pub(crate) struct TransportRuntime {
|
||||||
|
pool: ksp_onchain_transport_lib::HttpTransportPool,
|
||||||
|
profile_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportRuntime {
|
||||||
|
/// Returns a shareable clone of the Transport-owned HTTP pool for one admitted Backfill execution.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn pool(&self) -> ksp_onchain_transport_lib::HttpTransportPool {
|
||||||
|
return self.pool.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the composite-selected Transport profile identifier.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn profile_id(&self) -> &str {
|
||||||
|
return self.profile_id.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the one configured Transport network only when the active HTTP pool is logically coherent.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn coherent_network(&self) -> std::option::Option<String> {
|
||||||
|
let networks = configured_networks(&self.pool.snapshot());
|
||||||
|
if networks.len() != 1 {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
return networks.into_iter().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the current safe Transport-only projection for the Backfill options surface.
|
||||||
|
pub(crate) fn options(&self) -> ksp_core_lib::Result<crate::BackfillDeskOptionsDto> {
|
||||||
|
let snapshot = self.pool.snapshot();
|
||||||
|
let configured_networks = configured_networks(&snapshot);
|
||||||
|
let http_routes = compatible_backfill_http_routes(&self.pool, &snapshot);
|
||||||
|
let http_routes = match http_routes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let limits = crate::backfill_request_limits();
|
||||||
|
let limits = match limits {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let transport_ready = configured_networks.len() == 1 && !http_routes.is_empty() && snapshot.available_endpoint_count() > 0;
|
||||||
|
return std::result::Result::Ok(crate::BackfillDeskOptionsDto {
|
||||||
|
commitments: crate::backfill_commitment_codes(),
|
||||||
|
composition_ready: false,
|
||||||
|
configured_networks,
|
||||||
|
http_routes,
|
||||||
|
limits,
|
||||||
|
network_coherent: false,
|
||||||
|
program_id_options: crate::program_id_autocomplete_options(),
|
||||||
|
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||||
|
store_diagnostic: std::option::Option::None,
|
||||||
|
store_network: std::option::Option::None,
|
||||||
|
store_ready: false,
|
||||||
|
transport_diagnostic: std::option::Option::None,
|
||||||
|
transport_ready,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the composite-selected Transport profile and builds the executable HTTP pool.
|
||||||
|
pub(crate) fn initialize_transport(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<TransportRuntime> {
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||||
|
let environment = match environment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let composite = crate::load_backfill_desk_composite(management);
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||||
|
let profile = match profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let resolved = management.engine().resolve_transport_config_profile(&profile, &environment);
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let profile_id = resolved.profile_id().to_owned();
|
||||||
|
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings());
|
||||||
|
let pool = match pool {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let runtime = TransportRuntime { pool, profile_id };
|
||||||
|
let options = runtime.options();
|
||||||
|
let options = match options {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_TRANSPORT,
|
||||||
|
transport_profile = runtime.profile_id(),
|
||||||
|
network_count = options.configured_networks.len(),
|
||||||
|
http_route_count = options.http_routes.len(),
|
||||||
|
transport_ready = options.transport_ready,
|
||||||
|
"initialized Backfill Desk HTTP Transport readiness from composite-managed configuration"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compatible_backfill_http_routes(
|
||||||
|
pool: &ksp_onchain_transport_lib::HttpTransportPool,
|
||||||
|
snapshot: &ksp_onchain_transport_lib::HttpTransportPoolSnapshot,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<crate::BackfillHttpRouteOptionDto>> {
|
||||||
|
let signatures = required_http_rpc_method("getSignaturesForAddress");
|
||||||
|
let signatures = match signatures {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let transaction = required_http_rpc_method("getTransaction");
|
||||||
|
let transaction = match transaction {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut candidates = std::collections::BTreeMap::<String, std::collections::BTreeSet<String>>::new();
|
||||||
|
for endpoint in snapshot.endpoints() {
|
||||||
|
if !endpoint.enabled() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for role in endpoint.roles() {
|
||||||
|
if role.enabled() {
|
||||||
|
candidates.entry(role.role().to_owned()).or_default().insert(endpoint.provider().to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut compatible = std::vec::Vec::new();
|
||||||
|
for (candidate, providers) in candidates {
|
||||||
|
let role = ksp_onchain_transport_lib::HttpRoleName::new(candidate.clone());
|
||||||
|
if pool.select_for_method(&role, signatures).is_ok() && pool.select_for_method(&role, transaction).is_ok() {
|
||||||
|
let providers = providers.into_iter().collect::<std::vec::Vec<_>>();
|
||||||
|
compatible.push(crate::BackfillHttpRouteOptionDto { pooled: providers.len() > 1, providers, role: candidate });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(compatible);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_networks(snapshot: &ksp_onchain_transport_lib::HttpTransportPoolSnapshot) -> std::vec::Vec<String> {
|
||||||
|
let mut values = snapshot
|
||||||
|
.endpoints()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| {
|
||||||
|
if endpoint.enabled() {
|
||||||
|
return std::option::Option::Some(endpoint.cluster().to_owned());
|
||||||
|
}
|
||||||
|
return std::option::Option::None;
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
values.sort();
|
||||||
|
values.dedup();
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_http_rpc_method(method: &'static str) -> ksp_core_lib::Result<&'static ksp_onchain_transport_lib::HttpRpcMethodDescriptor> {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method);
|
||||||
|
return match descriptor {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TRANSPORT_READINESS_INVALID, "Backfill Desk Transport registry is missing a required HTTP RPC method")
|
||||||
|
.with_context("rpc_method", method),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/transport_runtime.rs"]
|
||||||
|
mod tests;
|
||||||
53
crates/ksp-app-backfill-desk/src/tw_main.rs
Normal file
53
crates/ksp-app-backfill-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/tw_main.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri-window helpers for the Backfill Desk main window.
|
||||||
|
|
||||||
|
use tauri::Manager; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
|
||||||
|
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
|
||||||
|
|
||||||
|
/// Resolves the required main window or returns a typed error.
|
||||||
|
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||||
|
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||||
|
return match window {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Backfill Desk main window is missing")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows main window.
|
||||||
|
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||||
|
let window = crate::require_main_window(app);
|
||||||
|
let window = match window {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let shown = window.show();
|
||||||
|
if let std::result::Result::Err(error) = shown {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Backfill Desk main window")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let focused = window.set_focus();
|
||||||
|
if let std::result::Result::Err(error) = focused {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Backfill Desk main window")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
window_label = WINDOW_LABEL_MAIN,
|
||||||
|
"main window shown and focused"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
173
crates/ksp-app-backfill-desk/src/tw_splash.rs
Normal file
173
crates/ksp-app-backfill-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/src/tw_splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri-window lifecycle for the Backfill Desk splash window.
|
||||||
|
|
||||||
|
use tauri::Emitter; // rust-rules: trait-import
|
||||||
|
use tauri::Manager; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Crate-internal splash window label.
|
||||||
|
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||||
|
|
||||||
|
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||||
|
|
||||||
|
/// Resolves the required splash window or returns a typed error.
|
||||||
|
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||||
|
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||||
|
return match window {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Backfill Desk splash window is missing")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_SPLASH),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||||
|
pub(crate) async fn splash_frontend_ready_service(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
invoking_window: tauri::WebviewWindow,
|
||||||
|
state: &crate::AppState,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||||
|
.with_context("window_label", invoking_window.label()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !state.begin_splash_sequence() {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
let settings = state.splash_settings();
|
||||||
|
let lifecycle_started = std::time::Instant::now();
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
window_label = WINDOW_LABEL_SPLASH,
|
||||||
|
fade_in_ms = settings.fade_in_ms(),
|
||||||
|
fade_in_source = settings.fade_in_source(),
|
||||||
|
minimum_ms = settings.minimum_ms(),
|
||||||
|
minimum_source = settings.minimum_source(),
|
||||||
|
fade_out_ms = settings.fade_out_ms(),
|
||||||
|
fade_out_source = settings.fade_out_source(),
|
||||||
|
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||||
|
"splash frontend readiness accepted; starting splash to main lifecycle"
|
||||||
|
);
|
||||||
|
let emitted = emit_order(
|
||||||
|
&invoking_window,
|
||||||
|
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_message(&invoking_window, "Initialisation de Backfill Desk...", "info");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
|
||||||
|
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
|
||||||
|
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let emitted = emit_order(
|
||||||
|
&invoking_window,
|
||||||
|
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = emitted {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
|
||||||
|
let show_main = crate::show_main_window(&app);
|
||||||
|
if let std::result::Result::Err(error) = show_main {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let destroyed = invoking_window.destroy();
|
||||||
|
if let std::result::Result::Err(error) = destroyed {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Backfill Desk splash window")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
window_label = WINDOW_LABEL_SPLASH,
|
||||||
|
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||||
|
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
"splash window destroyed after main activation"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
return emit_order(
|
||||||
|
window,
|
||||||
|
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
return emit_order(
|
||||||
|
window,
|
||||||
|
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
|
||||||
|
let wait_started = std::time::Instant::now();
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
window_label = window.label(),
|
||||||
|
phase,
|
||||||
|
configured_wait_ms = duration_ms,
|
||||||
|
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
"splash lifecycle wait completed"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
|
||||||
|
let action = order.action.clone();
|
||||||
|
let emitted = window.emit(SPLASH_EVENT_NAME, order);
|
||||||
|
return match emitted {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Backfill Desk splash order")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||||
|
.with_context("action", action)
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
72
crates/ksp-app-backfill-desk/tauri.conf.json
Normal file
72
crates/ksp-app-backfill-desk/tauri.conf.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "KSP Backfill Desk",
|
||||||
|
"identifier": "com.sasedev.ksp-app-backfill-desk",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": {
|
||||||
|
"script": "npm run dev",
|
||||||
|
"cwd": "."
|
||||||
|
},
|
||||||
|
"devUrl": "http://localhost:1436",
|
||||||
|
"beforeBuildCommand": {
|
||||||
|
"script": "npm run build",
|
||||||
|
"cwd": "."
|
||||||
|
},
|
||||||
|
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "splash",
|
||||||
|
"url": "splash.html",
|
||||||
|
"title": "Chargement — KSP Backfill Desk",
|
||||||
|
"width": 960,
|
||||||
|
"height": 637,
|
||||||
|
"resizable": false,
|
||||||
|
"decorations": false,
|
||||||
|
"transparent": true,
|
||||||
|
"center": true,
|
||||||
|
"alwaysOnTop": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"url": "main.html",
|
||||||
|
"title": "KSP Backfill Desk",
|
||||||
|
"width": 1500,
|
||||||
|
"height": 960,
|
||||||
|
"minWidth": 1024,
|
||||||
|
"minHeight": 768,
|
||||||
|
"center": true,
|
||||||
|
"visible": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/favicon.png",
|
||||||
|
"icons/favicon.ico"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
|
||||||
|
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
|
||||||
|
"../../config/composite.ksp-app-store-desk.json": "config/composite.ksp-app-store-desk.json",
|
||||||
|
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.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.store.schema.json": "config/schemas/std.store.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",
|
||||||
|
"../../config/std.logging.json": "config/std.logging.json",
|
||||||
|
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||||
|
"../../config/std.store.json": "config/std.store.json",
|
||||||
|
"../../config/std.transport.json": "config/std.transport.json",
|
||||||
|
"../../config/std.wallet.json": "config/std.wallet.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
crates/ksp-app-backfill-desk/tests/config_composition.rs
Normal file
308
crates/ksp-app-backfill-desk/tests/config_composition.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/config_composition.rs
|
||||||
|
// version: 7
|
||||||
|
|
||||||
|
//! Config composite, Transport readiness and Store-network composition contracts for Backfill Desk through pre.006.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn workspace_engine() -> ksp_core_lib::Result<ksp_config_lib::ConfigDocumentEngine> {
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||||
|
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::defaults();
|
||||||
|
return match registry {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, value)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_component(composite: &ksp_config_lib::ResolvedConfigComposite, component_id: &str, file_id: &str, profile_id: &str) {
|
||||||
|
let component = composite.component(component_id);
|
||||||
|
assert!(component.is_some(), "missing component {component_id}");
|
||||||
|
if let std::option::Option::Some(component) = component {
|
||||||
|
assert_eq!(component.resolved().file_id().as_str(), file_id);
|
||||||
|
assert_eq!(component.resolved().profile_id(), profile_id);
|
||||||
|
assert_eq!(component.resolved().selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_devnet_profile_selects_trace_logging_transport_and_store() {
|
||||||
|
let engine = workspace_engine();
|
||||||
|
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||||
|
assert!(file_id.is_ok(), "Backfill Desk composite file_id should be valid: {file_id:?}");
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some("devnet"));
|
||||||
|
assert!(composite.is_ok(), "committed Backfill Desk devnet profile should resolve: {composite:?}");
|
||||||
|
if let std::result::Result::Ok(composite) = composite {
|
||||||
|
assert_eq!(composite.profile_id(), "devnet");
|
||||||
|
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "supertrace");
|
||||||
|
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "devnet_public");
|
||||||
|
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, "devnet");
|
||||||
|
let logging = composite.component("logging");
|
||||||
|
assert!(logging.is_some(), "Backfill Desk composite should expose Logging");
|
||||||
|
if let std::option::Option::Some(logging) = logging {
|
||||||
|
assert_eq!(logging.resolved().profile().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("trace"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_named_profiles_keep_transport_and_store_network_selection_paired() {
|
||||||
|
let engine = workspace_engine();
|
||||||
|
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for (profile_id, logging_profile, transport_profile, store_profile) in
|
||||||
|
[("mainnet", "supertrace", "mainnet_backfill_pool", "mainnet"), ("testnet", "console_info", "publicnode_testnet", "testnet")]
|
||||||
|
{
|
||||||
|
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
|
||||||
|
assert!(composite.is_ok(), "Backfill Desk composite profile {profile_id} should resolve: {composite:?}");
|
||||||
|
if let std::result::Result::Ok(composite) = composite {
|
||||||
|
assert_eq!(composite.profile_id(), profile_id);
|
||||||
|
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, logging_profile);
|
||||||
|
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, transport_profile);
|
||||||
|
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, store_profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_default_composite_builds_http_pool_for_both_backfill_rpc_methods() {
|
||||||
|
let engine = workspace_engine();
|
||||||
|
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some("devnet"));
|
||||||
|
assert!(composite.is_ok(), "committed Backfill Desk devnet profile should resolve: {composite:?}");
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let transport = composite.component("transport");
|
||||||
|
assert!(transport.is_some(), "Backfill Desk composite should expose Transport");
|
||||||
|
let transport = match transport {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||||
|
assert!(environment.is_ok(), "Config environment should load for committed public devnet profile");
|
||||||
|
let environment = match environment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let resolved = engine.resolve_transport_config_profile(transport.resolved(), &environment);
|
||||||
|
assert!(resolved.is_ok(), "composite-selected Transport should map: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resolved.profile_id(), "devnet_public");
|
||||||
|
assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
|
||||||
|
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings());
|
||||||
|
assert!(pool.is_ok(), "composite-selected Transport settings should construct an HTTP pool");
|
||||||
|
let pool = match pool {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let snapshot = pool.snapshot();
|
||||||
|
let mut clusters = snapshot
|
||||||
|
.endpoints()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| {
|
||||||
|
if endpoint.enabled() {
|
||||||
|
return std::option::Option::Some(endpoint.cluster().to_owned());
|
||||||
|
}
|
||||||
|
return std::option::Option::None;
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
clusters.sort();
|
||||||
|
clusters.dedup();
|
||||||
|
assert_eq!(clusters, vec!["devnet".to_owned()]);
|
||||||
|
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||||
|
for method in ["getSignaturesForAddress", "getTransaction"] {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method);
|
||||||
|
assert!(descriptor.is_some(), "required Transport method {method} should exist");
|
||||||
|
if let std::option::Option::Some(descriptor) = descriptor {
|
||||||
|
let selection = pool.select_for_method(&role, descriptor);
|
||||||
|
assert!(selection.is_ok(), "role default should route {method}: {selection:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declared_http_networks(profile: &ksp_config_lib::ResolvedConfigProfile) -> std::vec::Vec<String> {
|
||||||
|
let endpoints = profile.profile().get("endpoints").and_then(serde_json::Value::as_array);
|
||||||
|
let endpoints = match endpoints {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
let mut networks = endpoints
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| {
|
||||||
|
let enabled = endpoint.get("enabled").and_then(serde_json::Value::as_bool);
|
||||||
|
if enabled != std::option::Option::Some(true) {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
return endpoint.get("cluster").and_then(serde_json::Value::as_str).map(str::to_owned);
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
networks.sort();
|
||||||
|
networks.dedup();
|
||||||
|
return networks;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_all_composite_profiles_keep_declared_http_and_store_networks_identical_without_external_secrets() {
|
||||||
|
let engine = workspace_engine();
|
||||||
|
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for profile_id in ["devnet", "mainnet", "testnet"] {
|
||||||
|
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
|
||||||
|
assert!(composite.is_ok(), "Backfill Desk composite profile {profile_id} should resolve: {composite:?}");
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let transport = composite.component("transport");
|
||||||
|
let store = composite.component("store");
|
||||||
|
assert!(transport.is_some(), "profile {profile_id} should expose Transport");
|
||||||
|
assert!(store.is_some(), "profile {profile_id} should expose Store");
|
||||||
|
let (transport, store) = match (transport, store) {
|
||||||
|
(std::option::Option::Some(transport), std::option::Option::Some(store)) => (transport, store),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
let networks = declared_http_networks(transport.resolved());
|
||||||
|
assert_eq!(networks.len(), 1, "profile {profile_id} must declare exactly one enabled HTTP network");
|
||||||
|
let store_network = store.resolved().profile().get("network").and_then(serde_json::Value::as_str);
|
||||||
|
assert!(store_network.is_some(), "profile {profile_id} Store should declare one logical network");
|
||||||
|
if let (std::option::Option::Some(transport_network), std::option::Option::Some(store_network)) = (networks.first(), store_network) {
|
||||||
|
assert_eq!(transport_network.as_str(), store_network, "profile {profile_id} Store/Transport network mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_default_mainnet_profile_exposes_pool_and_targeted_http_routes_without_secret_provider_data() {
|
||||||
|
let engine = workspace_engine();
|
||||||
|
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let composite = engine.load_resolved_composite(&file_id, std::option::Option::None);
|
||||||
|
assert!(composite.is_ok(), "default Backfill Desk composite should resolve: {composite:?}");
|
||||||
|
let composite = match composite {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(composite.profile_id(), "mainnet");
|
||||||
|
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "supertrace");
|
||||||
|
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "mainnet_backfill_pool");
|
||||||
|
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, "mainnet");
|
||||||
|
let transport = match composite.component("transport") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let profile = transport.resolved().profile();
|
||||||
|
let endpoints = profile.get("endpoints").and_then(serde_json::Value::as_array);
|
||||||
|
assert!(endpoints.is_some(), "mainnet Backfill Transport profile should expose HTTP endpoints");
|
||||||
|
let endpoints = match endpoints {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let providers = endpoints
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| return endpoint.get("provider").and_then(serde_json::Value::as_str))
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(providers, std::collections::BTreeSet::from(["publicnode", "solana-public"]));
|
||||||
|
let roles = endpoints
|
||||||
|
.iter()
|
||||||
|
.flat_map(|endpoint| return endpoint.get("roles").and_then(serde_json::Value::as_array).into_iter().flatten())
|
||||||
|
.filter_map(|role| return role.get("role").and_then(serde_json::Value::as_str))
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert!(roles.contains("backfill_pool"));
|
||||||
|
assert!(roles.contains("backfill_publicnode"));
|
||||||
|
assert!(roles.contains("backfill_solana_public"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_composite_profile_and_component_inventory_is_exact() {
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
let source = std::fs::read_to_string(workspace.join("config/composite.ksp-app-backfill-desk.json"));
|
||||||
|
assert!(source.is_ok(), "Backfill Desk composite should be readable");
|
||||||
|
let source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let document = serde_json::from_str::<serde_json::Value>(source.as_str());
|
||||||
|
assert!(document.is_ok(), "Backfill Desk composite should remain valid JSON");
|
||||||
|
let document = match document {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(document.get("default_profile").and_then(serde_json::Value::as_str), std::option::Option::Some("mainnet"));
|
||||||
|
let profiles = document.get("profiles").and_then(serde_json::Value::as_array);
|
||||||
|
assert!(profiles.is_some(), "Backfill Desk composite should expose profiles");
|
||||||
|
let profiles = match profiles {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let profile_ids = profiles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str))
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(profile_ids, std::collections::BTreeSet::from(["devnet", "mainnet", "testnet"]));
|
||||||
|
for profile in profiles {
|
||||||
|
let documents = profile.get("documents").and_then(serde_json::Value::as_array);
|
||||||
|
assert!(documents.is_some(), "every Backfill Desk profile should expose component documents");
|
||||||
|
let documents = match documents {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
let component_ids = documents
|
||||||
|
.iter()
|
||||||
|
.filter_map(|document| return document.get("component_id").and_then(serde_json::Value::as_str))
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(component_ids, std::collections::BTreeSet::from(["logging", "store", "transport"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
78
crates/ksp-app-backfill-desk/tests/dependency_boundary.rs
Normal file
78
crates/ksp-app-backfill-desk/tests/dependency_boundary.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/dependency_boundary.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Final dependency-boundary checks for the KSP Backfill desktop application.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_manifest_ksp_dependency_surface_is_exact_and_backend_neutral() {
|
||||||
|
let manifest = read_text(app_root().join("Cargo.toml").as_path());
|
||||||
|
let dependency_section = manifest.split("[dependencies]").nth(1).and_then(|tail| return tail.split("[dev-dependencies]").next());
|
||||||
|
assert!(dependency_section.is_some(), "Backfill Desk manifest should expose a dependency section");
|
||||||
|
let dependency_section = match dependency_section {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let ksp_dependencies = dependency_section
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let name = line.split('=').next().map(str::trim);
|
||||||
|
return match name {
|
||||||
|
std::option::Option::Some(value) if value.starts_with("ksp-") => std::option::Option::Some(value),
|
||||||
|
_ => std::option::Option::None,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
ksp_dependencies,
|
||||||
|
std::collections::BTreeSet::from([
|
||||||
|
"ksp-config-lib",
|
||||||
|
"ksp-core-lib",
|
||||||
|
"ksp-job-api",
|
||||||
|
"ksp-job-backfill-lib",
|
||||||
|
"ksp-logging-lib",
|
||||||
|
"ksp-onchain-transport-lib",
|
||||||
|
"ksp-store-lib",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "tokio-postgres", "reqwest", "tonic", "yellowstone-grpc-proto", "mpl-token-metadata"] {
|
||||||
|
assert!(!dependency_section.contains(forbidden), "Backfill Desk manifest owns forbidden dependency {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_production_sources_do_not_cross_into_physical_store_or_provider_clients() {
|
||||||
|
let source_root = app_root().join("src");
|
||||||
|
let entries = std::fs::read_dir(source_root.as_path());
|
||||||
|
assert!(entries.is_ok(), "Backfill Desk source directory should be readable");
|
||||||
|
let entries = match entries {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = read_text(path.as_path());
|
||||||
|
for forbidden in ["ksp_store_api", "ksp_store_postgres_lib", "tokio_postgres", "reqwest::", "tonic::", "yellowstone_grpc_proto"] {
|
||||||
|
assert!(!source.contains(forbidden), "{} crosses forbidden dependency boundary via {forbidden}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
465
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
Normal file
465
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||||
|
// version: 17
|
||||||
|
|
||||||
|
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> std::path::PathBuf {
|
||||||
|
return app_root().join("../..");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_json(path: &std::path::Path) -> serde_json::Value {
|
||||||
|
let source = read_text(path);
|
||||||
|
let parsed = serde_json::from_str(source.as_str());
|
||||||
|
assert!(parsed.is_ok(), "unable to parse {}", path.display());
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => serde_json::Value::Null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bytes(path: &std::path::Path) -> std::vec::Vec<u8> {
|
||||||
|
let source = std::fs::read(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_rust_package_is_mixed_lib_bin_and_workspace_member() {
|
||||||
|
let root = app_root();
|
||||||
|
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||||
|
assert!(manifest.contains("name = \"ksp-app-backfill-desk\""));
|
||||||
|
assert!(manifest.contains("name = \"ksp_app_backfill_desk_lib\""));
|
||||||
|
assert!(manifest.contains("[[bin]]"));
|
||||||
|
let workspace = read_text(workspace_root().join("Cargo.toml").as_path());
|
||||||
|
assert!(workspace.contains("crates/ksp-app-backfill-desk"));
|
||||||
|
let launcher = read_text(root.join("src/main.rs").as_path());
|
||||||
|
assert!(launcher.contains("ksp_app_backfill_desk_lib::run"));
|
||||||
|
assert!(launcher.contains("try_lock_exclusive"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() {
|
||||||
|
let root = app_root();
|
||||||
|
let vite = read_text(root.join("vite.config.ts").as_path());
|
||||||
|
assert!(vite.contains("port: 1436"));
|
||||||
|
assert!(vite.contains("port: 1437"));
|
||||||
|
assert!(vite.contains("strictPort: true"));
|
||||||
|
let tauri = read_text(root.join("tauri.conf.json").as_path());
|
||||||
|
assert!(tauri.contains("KSP Backfill Desk"));
|
||||||
|
assert!(tauri.contains("com.sasedev.ksp-app-backfill-desk"));
|
||||||
|
assert!(tauri.contains("http://localhost:1436"));
|
||||||
|
assert!(tauri.contains("npm run dev"));
|
||||||
|
assert!(tauri.contains("npm run build"));
|
||||||
|
assert!(tauri.contains("../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_package_dependencies_match_desk_baseline() {
|
||||||
|
let package = read_text(app_root().join("package.json").as_path());
|
||||||
|
for required in [
|
||||||
|
"@fltsci/tauri-plugin-tracing",
|
||||||
|
"@fortawesome/fontawesome-free",
|
||||||
|
"@tauri-apps/api",
|
||||||
|
"bootstrap",
|
||||||
|
"resize-observer-polyfill",
|
||||||
|
"simplebar",
|
||||||
|
"@tauri-apps/cli",
|
||||||
|
"@types/bootstrap",
|
||||||
|
"@types/node",
|
||||||
|
"sass-embedded",
|
||||||
|
"typescript",
|
||||||
|
"vite",
|
||||||
|
] {
|
||||||
|
assert!(package.contains(required), "missing Desk package baseline dependency {required}");
|
||||||
|
}
|
||||||
|
assert!(!package.contains("package-lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_splash_assets_and_styles_match_existing_desk_template() {
|
||||||
|
let root = app_root();
|
||||||
|
let reference = workspace_root().join("crates/ksp-app-solprices-desk");
|
||||||
|
for relative in ["frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png", "icons/favicon.png", "icons/favicon.ico"] {
|
||||||
|
assert_eq!(read_bytes(root.join(relative).as_path()), read_bytes(reference.join(relative).as_path()), "Desk asset differs: {relative}");
|
||||||
|
}
|
||||||
|
let splash = read_text(root.join("frontend/splash.html").as_path());
|
||||||
|
assert!(splash.contains("imgs/splash.png"));
|
||||||
|
assert!(splash.contains("Backfill Desk"));
|
||||||
|
let splash_style = read_text(root.join("frontend/sass/splash.scss").as_path());
|
||||||
|
let reference_style = read_text(reference.join("frontend/sass/splash.scss").as_path());
|
||||||
|
let normalized = splash_style.replace("crates/ksp-app-backfill-desk", "crates/ksp-app-solprices-desk").replace("// version: 1", "// version: 2");
|
||||||
|
assert_eq!(normalized, reference_style);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_shell_and_splash_use_shared_frontend_tooling() {
|
||||||
|
let root = app_root();
|
||||||
|
let shell = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
for required in ["app-header", "app-sidebar", "app-sidebar-scroll", "app-content", "app-footer", "data-simplebar"] {
|
||||||
|
assert!(shell.contains(required), "Backfill Desk shell is missing template marker {required}");
|
||||||
|
}
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(main.contains("resize-observer-polyfill"));
|
||||||
|
assert!(main.contains("simplebar"));
|
||||||
|
assert!(main.contains("bootstrap"));
|
||||||
|
assert!(main.contains("Backfill Desk frontend control clicked"));
|
||||||
|
assert!(main.contains("Backfill Desk navigation tab clicked"));
|
||||||
|
assert!(main.contains("Backfill Desk runtime status refresh button clicked"));
|
||||||
|
let splash = read_text(root.join("frontend/ts/splash.ts").as_path());
|
||||||
|
assert!(splash.contains("Splash opacity animation started"));
|
||||||
|
assert!(splash.contains("Backfill Desk splash frontend loaded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
|
||||||
|
let root = app_root();
|
||||||
|
let config = read_json(root.join("tauri.conf.json").as_path());
|
||||||
|
let resources = config
|
||||||
|
.get("bundle")
|
||||||
|
.and_then(|value| {
|
||||||
|
return value.get("resources");
|
||||||
|
})
|
||||||
|
.and_then(serde_json::Value::as_object);
|
||||||
|
assert!(resources.is_some());
|
||||||
|
let resources = match resources {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resources.len(), 15);
|
||||||
|
for required in [
|
||||||
|
"../../config/composite.ksp-app-backfill-desk.json",
|
||||||
|
"../../config/composite.ksp-app-solprices-desk.json",
|
||||||
|
"../../config/composite.ksp-app-store-desk.json",
|
||||||
|
"../../config/composite.ksp-app-wallet-desk.json",
|
||||||
|
"../../config/std.logging.json",
|
||||||
|
"../../config/std.offchain_transport.json",
|
||||||
|
"../../config/std.store.json",
|
||||||
|
"../../config/std.transport.json",
|
||||||
|
"../../config/std.wallet.json",
|
||||||
|
"../../config/schemas/composite.schema.json",
|
||||||
|
"../../config/schemas/std.logging.schema.json",
|
||||||
|
"../../config/schemas/std.offchain_transport.schema.json",
|
||||||
|
"../../config/schemas/std.store.schema.json",
|
||||||
|
"../../config/schemas/std.transport.schema.json",
|
||||||
|
"../../config/schemas/std.wallet.schema.json",
|
||||||
|
] {
|
||||||
|
assert!(resources.contains_key(required), "missing packaged Config resource {required}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("prepare_packaged_runtime"));
|
||||||
|
assert!(tauri.contains("resource_dir"));
|
||||||
|
assert!(tauri.contains("set_current_dir"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_bootstrap_keeps_logging_composition_separate_from_transport_runtime() {
|
||||||
|
let root = app_root();
|
||||||
|
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
|
||||||
|
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK"));
|
||||||
|
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_LOGGING"));
|
||||||
|
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_TRANSPORT"));
|
||||||
|
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_STORE"));
|
||||||
|
assert!(bootstrap.contains("LogFilterLevel::Trace"));
|
||||||
|
for forbidden in ["HttpTransportPool", "Store::open", "BackfillJobRuntime", "BackfillRequest"] {
|
||||||
|
assert!(!bootstrap.contains(forbidden), "bootstrap.rs absorbs another runtime responsibility: {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_transport_runtime_builds_pool_and_validates_both_required_rpc_methods() {
|
||||||
|
let root = app_root();
|
||||||
|
let transport = read_text(root.join("src/transport_runtime.rs").as_path());
|
||||||
|
assert!(transport.contains("HttpTransportPool::new"));
|
||||||
|
assert!(transport.contains("getSignaturesForAddress"));
|
||||||
|
assert!(transport.contains("getTransaction"));
|
||||||
|
assert!(transport.contains("select_for_method"));
|
||||||
|
assert!(transport.contains("configured_networks"));
|
||||||
|
assert!(transport.contains("compatible_backfill_http_routes"));
|
||||||
|
assert!(transport.contains("endpoint.provider()"));
|
||||||
|
for forbidden in ["url()", "Store::open", "BackfillJobRuntime", "BackfillRequest"] {
|
||||||
|
assert!(!transport.contains(forbidden), "pre.004 opens or projects forbidden surface {forbidden}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("backfill_options"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_store_readiness_proves_network_before_open_and_closes_on_main_window_exit() {
|
||||||
|
let root = app_root();
|
||||||
|
let store = read_text(root.join("src/store_runtime.rs").as_path());
|
||||||
|
let coherence = store.find("validate_network_coherence(store_network.as_str(), transport_network.as_str())");
|
||||||
|
let open = store.find("ksp_store_lib::Store::open");
|
||||||
|
assert!(coherence.is_some());
|
||||||
|
assert!(open.is_some());
|
||||||
|
if let (std::option::Option::Some(coherence), std::option::Option::Some(open)) = (coherence, open) {
|
||||||
|
assert!(coherence < open, "Store network coherence must be proven before physical Store open");
|
||||||
|
}
|
||||||
|
assert!(store.contains("store.health().await"));
|
||||||
|
assert!(store.contains("store.close().await"));
|
||||||
|
assert!(store.contains("StoreHealthState::Ready"));
|
||||||
|
assert!(store.contains("StoreHealthState::NotReady"));
|
||||||
|
assert!(!store.contains("BackfillJobRuntime"));
|
||||||
|
assert!(!store.contains("BackfillRequest"));
|
||||||
|
let app_state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
assert!(app_state.contains("initialize_store"));
|
||||||
|
assert!(app_state.contains("composition_ready"));
|
||||||
|
assert!(app_state.contains("close_store"));
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
for required in ["on_window_event", "WindowEvent::CloseRequested", "prevent_close", "async_runtime::spawn", "close_store", "app_handle.exit(0)"] {
|
||||||
|
assert!(tauri.contains(required), "missing bounded Store shutdown lifecycle marker {required}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_runtime() {
|
||||||
|
let root = app_root();
|
||||||
|
let workspace = root.join("../..");
|
||||||
|
let composite = read_text(workspace.join("config/composite.ksp-app-backfill-desk.json").as_path());
|
||||||
|
assert!(composite.contains("\"default_profile\": \"mainnet\""));
|
||||||
|
assert!(composite.contains("\"profile_id\": \"mainnet_backfill_pool\""));
|
||||||
|
let transport = read_text(workspace.join("config/std.transport.json").as_path());
|
||||||
|
for required in ["mainnet_backfill_pool", "backfill_pool", "backfill_publicnode", "backfill_solana_public", "solana-rpc.publicnode.com"] {
|
||||||
|
assert!(transport.contains(required), "missing Backfill HTTP route marker {required}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(frontend.contains("Backfill Desk HTTP route selection changed"));
|
||||||
|
assert!(frontend.contains("backfill_options"));
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
assert!(html.contains("backfillHttpRoute"));
|
||||||
|
for forbidden in ["grpc", "websocket"] {
|
||||||
|
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_007_campaign_dto_and_backend_mapping_remain_the_authoritative_start_input() {
|
||||||
|
let root = app_root();
|
||||||
|
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||||
|
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
|
||||||
|
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
|
||||||
|
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
for required in [
|
||||||
|
"BackfillStartRequestDto",
|
||||||
|
"BackfillRequestPreviewDto",
|
||||||
|
"MAX_BACKFILL_CANDIDATES",
|
||||||
|
"MAX_BACKFILL_HYDRATION_CONCURRENCY",
|
||||||
|
"MAX_BACKFILL_PAGES",
|
||||||
|
"MAX_BACKFILL_PAGE_SIZE",
|
||||||
|
] {
|
||||||
|
assert!(dto.contains(required), "missing pre.007 DTO/request bound marker {required}");
|
||||||
|
}
|
||||||
|
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
|
||||||
|
for required in ["BackfillRequest::new", "HttpRoleName::new", "latest_address", "before_address", "after_address", "explicit_signatures"] {
|
||||||
|
assert!(mapping.contains(required), "missing strict Backfill request mapping marker {required}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("backfill_validate_request"));
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
for required in [
|
||||||
|
"backfillCampaignForm",
|
||||||
|
"backfillScopeKind",
|
||||||
|
"backfillCommitment",
|
||||||
|
"backfillAddress",
|
||||||
|
"backfillAnchorSignature",
|
||||||
|
"backfillExplicitSignatures",
|
||||||
|
"backfillMinContextSlot",
|
||||||
|
"validateBackfillRequest",
|
||||||
|
"Valider la requête",
|
||||||
|
] {
|
||||||
|
assert!(html.contains(required), "missing campaign form marker {required}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(frontend.contains("backfill_validate_request"));
|
||||||
|
assert!(frontend.contains("Backfill Desk campaign validation requested"));
|
||||||
|
assert!(frontend.contains("Aucun job n'a été démarré"));
|
||||||
|
assert!(frontend.contains("backfill_validate_request"));
|
||||||
|
assert!(tauri.contains("backfill_validate_request"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_backend_owned() {
|
||||||
|
let root = app_root();
|
||||||
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
|
||||||
|
let handle_index = state.find("let handle = runtime.handle();");
|
||||||
|
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle, retained_request)");
|
||||||
|
assert!(handle_index.is_some());
|
||||||
|
assert!(install_index.is_some());
|
||||||
|
if let (std::option::Option::Some(handle_index), std::option::Option::Some(install_index)) = (handle_index, install_index) {
|
||||||
|
assert!(handle_index < install_index);
|
||||||
|
}
|
||||||
|
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
|
||||||
|
assert!(run.contains("std::sync::Mutex<std::option::Option<ActiveBackfillRun>>"));
|
||||||
|
assert!(tauri.contains("backfill_start"));
|
||||||
|
let prepare_index = tauri.find("state.prepare_backfill_start(request)");
|
||||||
|
assert!(prepare_index.is_some());
|
||||||
|
if let std::option::Option::Some(prepare_index) = prepare_index {
|
||||||
|
let start_tail = &tauri[prepare_index..];
|
||||||
|
let spawn_index = start_tail.find("tauri::async_runtime::spawn(async move");
|
||||||
|
assert!(spawn_index.is_some());
|
||||||
|
}
|
||||||
|
assert!(html.contains("startBackfillRequest"));
|
||||||
|
assert!(html.contains("Démarrer le backfill"));
|
||||||
|
assert!(frontend.contains("Backfill Desk campaign Start requested"));
|
||||||
|
assert!(frontend.contains("backfill_start"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_009_latest_value_monitoring_uses_snapshot_source_event_bridge_and_resynchronization_command() {
|
||||||
|
let root = app_root();
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
let status = read_text(root.join("src/backfill_status.rs").as_path());
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(run.contains("last_terminal"));
|
||||||
|
assert!(run.contains("current_notification"));
|
||||||
|
assert!(status.contains("BackfillRunStatusDto"));
|
||||||
|
assert!(status.contains("checkpoint_present"));
|
||||||
|
assert!(status.contains("contiguous_completed"));
|
||||||
|
assert!(tauri.contains("backfill_status"));
|
||||||
|
assert!(tauri.contains("wait_for_change"));
|
||||||
|
assert!(tauri.contains("ksp-backfill-status") || tauri.contains("BACKFILL_STATUS_EVENT_NAME"));
|
||||||
|
assert!(frontend.contains("listen<BackfillRunStatusDto>"));
|
||||||
|
assert!(frontend.contains("syncBackfillStatus"));
|
||||||
|
assert!(!frontend.contains("parseBackfillLog"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_010_cancel_is_targeted_idempotent_and_shutdown_requests_cooperative_cancellation_before_exit() {
|
||||||
|
let root = app_root();
|
||||||
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(run.contains("current.job_id.as_str() != job_id"));
|
||||||
|
assert!(run.contains("current.handle.cancel()"));
|
||||||
|
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_MISMATCH"));
|
||||||
|
assert!(state.contains("cancel_backfill"));
|
||||||
|
assert!(state.contains("cancel_active_backfill_for_shutdown"));
|
||||||
|
assert!(tauri.contains("backfill_cancel"));
|
||||||
|
assert!(tauri.contains("cancel_active_backfill_for_shutdown"));
|
||||||
|
let shutdown_cancel = tauri.find("cancel_active_backfill_for_shutdown");
|
||||||
|
let shutdown_exit = tauri.find("app_handle.exit(0)");
|
||||||
|
assert!(shutdown_cancel.is_some());
|
||||||
|
assert!(shutdown_exit.is_some());
|
||||||
|
if let (std::option::Option::Some(cancel), std::option::Option::Some(exit)) = (shutdown_cancel, shutdown_exit) {
|
||||||
|
assert!(cancel < exit);
|
||||||
|
}
|
||||||
|
assert!(html.contains("cancelBackfillRun"));
|
||||||
|
assert!(html.contains("Annuler"));
|
||||||
|
assert!(frontend.contains("backfill_cancel"));
|
||||||
|
assert!(frontend.contains("{ jobId: status.jobId }"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_011_resume_reissues_rust_only_checkpoint_for_new_backend_job_without_ipc_checkpoint_material() {
|
||||||
|
let root = app_root();
|
||||||
|
let workspace = root.join("../..");
|
||||||
|
let checkpoint = read_text(workspace.join("crates/ksp-job-backfill-lib/src/checkpoint.rs").as_path());
|
||||||
|
assert!(checkpoint.contains("reissue_for_job"));
|
||||||
|
let request = read_text(workspace.join("crates/ksp-job-backfill-lib/src/request.rs").as_path());
|
||||||
|
assert!(request.contains("pub fn resume_for_job"));
|
||||||
|
assert!(request.contains("validate_request_checkpoint"));
|
||||||
|
assert!(request.contains("checkpoint.reissue_for_job(job_id.clone())"));
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
for required in ["TerminalBackfillRun", "request: ksp_job_backfill_lib::BackfillRequest", "snapshot().checkpoint()", "resume_for_job(job_id, checkpoint)"] {
|
||||||
|
assert!(run.contains(required), "missing Rust-only Resume ownership marker {required}");
|
||||||
|
}
|
||||||
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
for required in ["prepare_backfill_resume", "options.composition_ready", "store_network_matches", "role_available"] {
|
||||||
|
assert!(state.contains(required), "missing Resume admission marker {required}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("backfill_resume"));
|
||||||
|
assert!(tauri.contains("spawn_backfill_launch"));
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(frontend.contains("resumeBackfillRun"));
|
||||||
|
assert!(frontend.contains("checkpointPresent"));
|
||||||
|
assert!(frontend.contains("backfill_resume"));
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
assert!(html.contains("resumeBackfillRun"));
|
||||||
|
assert!(html.contains("Reprendre"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_free_address_autocomplete_is_html_datalist_derived_from_core_registry() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto = read_text(root.join("src/dto_common.rs").as_path());
|
||||||
|
assert!(dto.contains("ksp_core_lib::entries()"));
|
||||||
|
assert!(dto.contains("ProgramIdAutocompleteOptionDto"));
|
||||||
|
assert!(dto.contains("program_id_autocomplete_options"));
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
assert!(html.contains("list=\"backfillProgramIds\""));
|
||||||
|
assert!(html.contains("<datalist id=\"backfillProgramIds\"></datalist>"));
|
||||||
|
assert!(html.contains("toute adresse Solana valide reste acceptée"));
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
for required in [
|
||||||
|
"renderProgramIdAutocomplete(options.programIdOptions)",
|
||||||
|
"option.value = program.programId",
|
||||||
|
"option.dataset.code = program.code",
|
||||||
|
"option.dataset.domain = program.domain",
|
||||||
|
"registryMatch: selected !== null",
|
||||||
|
"Backfill Desk Program ID autocomplete dataset rendered",
|
||||||
|
] {
|
||||||
|
assert!(frontend.contains(required), "missing pre.012 autocomplete marker {required}");
|
||||||
|
}
|
||||||
|
for forbidden in
|
||||||
|
["AddressLookupTab1e1111111111111111111111111", "ComputeBudget111111111111111111111111111111", "Vote111111111111111111111111111111111111111"]
|
||||||
|
{
|
||||||
|
assert!(!frontend.contains(forbidden), "frontend hardcodes canonical Program ID {forbidden}");
|
||||||
|
assert!(!html.contains(forbidden), "HTML hardcodes canonical Program ID {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_final_v1_command_and_frontend_control_inventory_is_exact() {
|
||||||
|
let root = app_root();
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let commands = [
|
||||||
|
"backfill_cancel",
|
||||||
|
"backfill_options",
|
||||||
|
"backfill_resume",
|
||||||
|
"backfill_start",
|
||||||
|
"backfill_status",
|
||||||
|
"backfill_validate_request",
|
||||||
|
"emit_frontend_log",
|
||||||
|
"get_runtime_status",
|
||||||
|
"splash_frontend_ready",
|
||||||
|
];
|
||||||
|
for command in commands {
|
||||||
|
assert!(tauri.contains(format!("fn {command}").as_str()), "missing final Tauri command {command}");
|
||||||
|
}
|
||||||
|
assert_eq!(tauri.matches("#[tauri::command]").count(), commands.len());
|
||||||
|
for forbidden in ["backfill_reset", "backfill_delete", "backfill_retry", "backfill_schedule", "backfill_history"] {
|
||||||
|
assert!(!tauri.contains(forbidden), "unexpected V1 Tauri command marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
for required in ["startBackfillRequest", "cancelBackfillRun", "resumeBackfillRun", "refreshBackfillStatus", "backfillProgramIds"] {
|
||||||
|
assert!(frontend.contains(required), "missing final frontend control {required}");
|
||||||
|
}
|
||||||
|
}
|
||||||
395
crates/ksp-app-backfill-desk/tests/desktop_security.rs
Normal file
395
crates/ksp-app-backfill-desk/tests/desktop_security.rs
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||||
|
// version: 16
|
||||||
|
|
||||||
|
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn struct_source<'a>(source: &'a str, marker: &str) -> &'a str {
|
||||||
|
let start = match source.find(marker) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return "",
|
||||||
|
};
|
||||||
|
let tail = &source[start..];
|
||||||
|
let end = match tail.find("\n}") {
|
||||||
|
std::option::Option::Some(value) => value + 2,
|
||||||
|
std::option::Option::None => return "",
|
||||||
|
};
|
||||||
|
return &tail[..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_files(directory: &std::path::Path, extension: &str, files: &mut std::vec::Vec<std::path::PathBuf>) {
|
||||||
|
let entries = std::fs::read_dir(directory);
|
||||||
|
let entries = match entries {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = match entry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_files(path.as_path(), extension, files);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some(extension) {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_surface_remains_core_plus_tracing_while_backfill_runtime_stays_backend_only() {
|
||||||
|
let root = app_root();
|
||||||
|
let capability = read_text(root.join("capabilities/default.json").as_path());
|
||||||
|
assert!(capability.contains("\"core:default\""));
|
||||||
|
assert!(capability.contains("\"tracing:default\""));
|
||||||
|
for forbidden in ["dialog:", "fs:", "http:", "shell:"] {
|
||||||
|
assert!(!capability.contains(forbidden));
|
||||||
|
}
|
||||||
|
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||||
|
assert!(manifest.contains("tauri-plugin-tracing.workspace = true"));
|
||||||
|
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
|
||||||
|
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
|
||||||
|
assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
|
||||||
|
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
|
||||||
|
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
|
||||||
|
assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden direct dependency: {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() {
|
||||||
|
let root = app_root();
|
||||||
|
let mut typescript = std::vec::Vec::new();
|
||||||
|
collect_files(root.join("frontend/ts").as_path(), "ts", &mut typescript);
|
||||||
|
assert!(!typescript.is_empty());
|
||||||
|
for path in typescript {
|
||||||
|
let source = read_text(path.as_path());
|
||||||
|
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert(", "window.confirm(", "window.prompt("] {
|
||||||
|
assert!(!source.contains(forbidden), "{} contains forbidden scaffold surface {forbidden}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_tauri_commands_remain_centralized() {
|
||||||
|
let root = app_root();
|
||||||
|
let mut rust_files = std::vec::Vec::new();
|
||||||
|
collect_files(root.join("src").as_path(), "rs", &mut rust_files);
|
||||||
|
let mut command_count = 0usize;
|
||||||
|
for path in rust_files {
|
||||||
|
let source = read_text(path.as_path());
|
||||||
|
let count = source.matches("#[tauri::command]").count();
|
||||||
|
if path.file_name().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("tauri.rs") {
|
||||||
|
command_count += count;
|
||||||
|
} else {
|
||||||
|
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(command_count, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_frontend_instrumentation_avoids_business_or_secret_payloads() {
|
||||||
|
let root = app_root();
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
for required in [
|
||||||
|
"Backfill Desk frontend control clicked",
|
||||||
|
"Backfill Desk navigation tab clicked",
|
||||||
|
"Backfill Desk runtime status refresh button clicked",
|
||||||
|
"Frontend IPC command requested",
|
||||||
|
] {
|
||||||
|
if required == "Frontend IPC command requested" {
|
||||||
|
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
|
||||||
|
assert!(invoke.contains(required));
|
||||||
|
} else {
|
||||||
|
assert!(main.contains(required));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"JSON.stringify(status)",
|
||||||
|
"apiKey",
|
||||||
|
"authorization",
|
||||||
|
"database_url",
|
||||||
|
"campaign validation requested\", {\n address",
|
||||||
|
"campaign validation requested\", {\n anchorSignature",
|
||||||
|
"campaign validation requested\", {\n explicitSignatures",
|
||||||
|
"campaign validation requested\", {\n minContextSlot",
|
||||||
|
] {
|
||||||
|
assert!(!main.contains(forbidden), "frontend tracing includes forbidden request payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_store_runtime_remains_backend_neutral_after_job_mapping_opens() {
|
||||||
|
let root = app_root();
|
||||||
|
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
|
||||||
|
assert!(bootstrap.contains("LogFilterLevel::Trace"));
|
||||||
|
assert!(!bootstrap.contains("HttpTransportPool"));
|
||||||
|
assert!(!bootstrap.contains("Store::open"));
|
||||||
|
let store = read_text(root.join("src/store_runtime.rs").as_path());
|
||||||
|
assert!(store.contains("ksp_store_lib::Store::open"));
|
||||||
|
assert!(store.contains("store.health().await"));
|
||||||
|
assert!(store.contains("store.close().await"));
|
||||||
|
for forbidden in ["ksp_job_", "BackfillRequest", "connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
|
||||||
|
assert!(!store.contains(forbidden), "Store runtime leaks or absorbs another responsibility: {forbidden}");
|
||||||
|
}
|
||||||
|
let dto = read_text(root.join("src/dto_common.rs").as_path());
|
||||||
|
assert!(dto.contains("BackfillDeskOptionsDto"));
|
||||||
|
for forbidden in ["endpoint_url", "pub(crate) url", "pub(crate) credential", "pub(crate) token", "connection_uri"] {
|
||||||
|
assert!(!dto.contains(forbidden), "readiness options DTO source contains forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_frontend_receives_only_safe_http_route_metadata_and_no_endpoint_material() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto = read_text(root.join("src/dto_common.rs").as_path());
|
||||||
|
assert!(dto.contains("BackfillHttpRouteOptionDto"));
|
||||||
|
assert!(dto.contains("pub(crate) providers"));
|
||||||
|
assert!(dto.contains("pub(crate) role"));
|
||||||
|
for forbidden in ["endpoint_url", "pub(crate) url", "pub(crate) credential", "pub(crate) api_key", "pub(crate) authorization"] {
|
||||||
|
assert!(!dto.contains(forbidden), "route DTO leaks forbidden endpoint material marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
for forbidden in ["http://", "https://", "apiKey", "authorization", "token"] {
|
||||||
|
assert!(!frontend.contains(forbidden), "frontend embeds forbidden provider material marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_owned() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
let request_source = struct_source(dto.as_str(), "pub(crate) struct BackfillStartRequestDto");
|
||||||
|
assert!(!request_source.is_empty());
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) network:",
|
||||||
|
"pub(crate) provider",
|
||||||
|
"pub(crate) endpoint",
|
||||||
|
"pub(crate) url",
|
||||||
|
"pub(crate) job_id",
|
||||||
|
"pub(crate) credential",
|
||||||
|
"pub(crate) token",
|
||||||
|
] {
|
||||||
|
assert!(!request_source.contains(forbidden), "frontend request DTO owns forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
|
||||||
|
assert!(mapping.contains("options.store_network.as_deref()"));
|
||||||
|
assert!(mapping.contains("options.http_routes.iter().any"));
|
||||||
|
assert!(mapping.contains("BackfillRequest::new"));
|
||||||
|
assert!(!mapping.contains("HttpTransportPool::new"));
|
||||||
|
assert!(!mapping.contains("Store::open"));
|
||||||
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
assert!(state.contains("backfill-desk-validation"));
|
||||||
|
assert!(state.contains("validated Backfill Desk campaign request without starting a Job"));
|
||||||
|
for forbidden in [
|
||||||
|
"preview.address",
|
||||||
|
"preview.anchor_signature",
|
||||||
|
"preview.explicit_signatures",
|
||||||
|
"request.address.as_str()",
|
||||||
|
"request.anchor_signature.as_str()",
|
||||||
|
] {
|
||||||
|
assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_008_start_surface_keeps_payload_and_physical_resources_backend_owned() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
let response_source = struct_source(dto.as_str(), "pub(crate) struct BackfillStartResponseDto");
|
||||||
|
assert!(!response_source.is_empty());
|
||||||
|
assert!(response_source.contains("pub(crate) job_id: String"));
|
||||||
|
assert!(response_source.contains("pub(crate) state: String"));
|
||||||
|
for forbidden in ["address", "signature", "provider", "endpoint", "url", "credential", "token", "network"] {
|
||||||
|
assert!(!response_source.contains(forbidden), "Start acknowledgement leaks forbidden marker {forbidden}");
|
||||||
|
}
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
assert!(run.contains("BackfillJobHandle"));
|
||||||
|
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
|
||||||
|
assert!(!run.contains("reqwest"));
|
||||||
|
assert!(!run.contains("tokio_postgres"));
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
let start_marker = frontend.find("Backfill Desk campaign Start requested");
|
||||||
|
assert!(start_marker.is_some());
|
||||||
|
let start_source = match start_marker {
|
||||||
|
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 420, frontend.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
|
||||||
|
assert!(!start_source.contains(forbidden), "Start tracing includes forbidden request payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_009_monitoring_projection_exposes_counters_and_codes_without_checkpoint_or_business_payloads() {
|
||||||
|
let root = app_root();
|
||||||
|
let status = read_text(root.join("src/backfill_status.rs").as_path());
|
||||||
|
let dto = struct_source(status.as_str(), "BackfillRunStatusDto");
|
||||||
|
for required in ["checkpoint_present", "contiguous_completed", "failure_code", "failure_domain", "sequence", "scope_kind"] {
|
||||||
|
assert!(dto.contains(required), "monitoring DTO missing required safe marker {required}");
|
||||||
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) address:",
|
||||||
|
"pub(crate) signature:",
|
||||||
|
"pub(crate) endpoint:",
|
||||||
|
"pub(crate) provider:",
|
||||||
|
"pub(crate) credential:",
|
||||||
|
"pub(crate) token:",
|
||||||
|
"pub(crate) checkpoint:",
|
||||||
|
"pub(crate) payload:",
|
||||||
|
"pub(crate) raw_transaction:",
|
||||||
|
] {
|
||||||
|
assert!(!dto.contains(forbidden), "monitoring DTO leaks forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_010_cancel_surface_targets_backend_job_identity_without_business_payloads() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto_source = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
let dto = struct_source(dto_source.as_str(), "BackfillCancelResponseDto");
|
||||||
|
for required in ["accepted", "job_id", "state"] {
|
||||||
|
assert!(dto.contains(required), "Cancel acknowledgement missing safe field {required}");
|
||||||
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) address:",
|
||||||
|
"pub(crate) signature:",
|
||||||
|
"pub(crate) endpoint:",
|
||||||
|
"pub(crate) provider:",
|
||||||
|
"pub(crate) credential:",
|
||||||
|
"pub(crate) token:",
|
||||||
|
"pub(crate) checkpoint:",
|
||||||
|
"pub(crate) payload:",
|
||||||
|
] {
|
||||||
|
assert!(!dto.contains(forbidden), "Cancel acknowledgement leaks forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
let cancel_marker = frontend.find("Backfill Desk cooperative cancellation requested");
|
||||||
|
assert!(cancel_marker.is_some());
|
||||||
|
let cancel_source = match cancel_marker {
|
||||||
|
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 700, frontend.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
assert!(cancel_source.contains("jobId: status.jobId"));
|
||||||
|
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
|
||||||
|
assert!(!cancel_source.contains(forbidden), "Cancel tracing includes forbidden campaign payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_011_resume_surface_exposes_only_new_job_acknowledgement_and_never_checkpoint_payload() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto_source = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
let dto = struct_source(dto_source.as_str(), "BackfillResumeResponseDto");
|
||||||
|
for required in ["accepted", "job_id", "state"] {
|
||||||
|
assert!(dto.contains(required), "Resume acknowledgement missing safe field {required}");
|
||||||
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) address:",
|
||||||
|
"pub(crate) signature:",
|
||||||
|
"pub(crate) endpoint:",
|
||||||
|
"pub(crate) provider:",
|
||||||
|
"pub(crate) credential:",
|
||||||
|
"pub(crate) token:",
|
||||||
|
"pub(crate) checkpoint:",
|
||||||
|
"pub(crate) payload:",
|
||||||
|
"pub(crate) network:",
|
||||||
|
] {
|
||||||
|
assert!(!dto.contains(forbidden), "Resume acknowledgement leaks forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let resume_marker = tauri.find("fn backfill_resume");
|
||||||
|
assert!(resume_marker.is_some());
|
||||||
|
let resume_source = match resume_marker {
|
||||||
|
std::option::Option::Some(index) => &tauri[index..std::cmp::min(index + 1300, tauri.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
for forbidden in ["BackfillCheckpoint", "checkpoint:", "address:", "signature:", "provider:", "endpoint:"] {
|
||||||
|
assert!(!resume_source.contains(forbidden), "Resume IPC command leaks forbidden marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
let resume_marker = frontend.find("Backfill Desk in-session Resume requested");
|
||||||
|
assert!(resume_marker.is_some());
|
||||||
|
let resume_source = match resume_marker {
|
||||||
|
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 1100, frontend.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
assert!(resume_source.contains("backfill_resume"));
|
||||||
|
for forbidden in ["checkpoint:", "address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
|
||||||
|
assert!(!resume_source.contains(forbidden), "Resume frontend sends or logs forbidden payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_program_id_autocomplete_exposes_only_public_registry_metadata_and_no_browser_storage() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto_source = read_text(root.join("src/dto_common.rs").as_path());
|
||||||
|
let dto = struct_source(dto_source.as_str(), "ProgramIdAutocompleteOptionDto");
|
||||||
|
for required in ["code", "domain", "family", "name", "program_id", "protocol"] {
|
||||||
|
assert!(dto.contains(required), "Program ID autocomplete DTO missing safe registry field {required}");
|
||||||
|
}
|
||||||
|
for forbidden in ["url", "credential", "token", "secret", "endpoint", "connection", "private_key"] {
|
||||||
|
assert!(!dto.contains(forbidden), "Program ID autocomplete DTO leaks forbidden marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
for forbidden in ["localStorage", "sessionStorage", "indexedDB", "document.cookie"] {
|
||||||
|
assert!(!frontend.contains(forbidden), "frontend persists campaign/autocomplete material via {forbidden}");
|
||||||
|
}
|
||||||
|
let selection_marker = frontend.find("Backfill Desk address autocomplete selection evaluated");
|
||||||
|
assert!(selection_marker.is_some());
|
||||||
|
let selection_source = match selection_marker {
|
||||||
|
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 320, frontend.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
assert!(selection_source.contains("programCode"));
|
||||||
|
assert!(selection_source.contains("registryMatch"));
|
||||||
|
assert!(!selection_source.contains("address.value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_final_frontend_and_ipc_surface_keeps_physical_and_checkpoint_material_backend_owned() {
|
||||||
|
let root = app_root();
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
for forbidden in ["http://", "https://", "localStorage", "sessionStorage", "indexedDB", "document.cookie", "privateKey", "secretKey"] {
|
||||||
|
assert!(!frontend.contains(forbidden), "final frontend contains forbidden marker {forbidden}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
for forbidden in ["BackfillCheckpoint", "HttpTransportPool", "ksp_store_postgres_lib", "tokio_postgres", "reqwest", "tonic"] {
|
||||||
|
assert!(!tauri.contains(forbidden), "Tauri IPC shell owns forbidden physical/runtime marker {forbidden}");
|
||||||
|
}
|
||||||
|
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
for marker in ["BackfillStartResponseDto", "BackfillCancelResponseDto", "BackfillResumeResponseDto"] {
|
||||||
|
let source = struct_source(dto.as_str(), marker);
|
||||||
|
assert!(!source.is_empty(), "missing DTO {marker}");
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) checkpoint:",
|
||||||
|
"pub(crate) endpoint:",
|
||||||
|
"pub(crate) provider:",
|
||||||
|
"pub(crate) credential:",
|
||||||
|
"pub(crate) token:",
|
||||||
|
"pub(crate) payload:",
|
||||||
|
] {
|
||||||
|
assert!(!source.contains(forbidden), "DTO {marker} leaks forbidden field {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crates/ksp-app-backfill-desk/tests/public_api.rs
Normal file
14
crates/ksp-app-backfill-desk/tests/public_api.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/public_api.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Public API checks for the KSP Backfill desktop application library.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_entry_point_is_available_from_crate_root() {
|
||||||
|
let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_backfill_desk_lib::run;
|
||||||
|
assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of::<fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()>>());
|
||||||
|
}
|
||||||
121
crates/ksp-app-backfill-desk/tests/release_completeness.rs
Normal file
121
crates/ksp-app-backfill-desk/tests/release_completeness.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/tests/release_completeness.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Final V1 completeness canaries for the KSP Backfill desktop application.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_production_module_inventory_is_exact() {
|
||||||
|
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||||
|
let modules = lib
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let line = line.trim();
|
||||||
|
if !line.starts_with("mod ") || !line.ends_with(';') {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
return std::option::Option::Some(line.trim_start_matches("mod ").trim_end_matches(';'));
|
||||||
|
})
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
modules,
|
||||||
|
std::collections::BTreeSet::from([
|
||||||
|
"app_state",
|
||||||
|
"backfill_request",
|
||||||
|
"backfill_run",
|
||||||
|
"backfill_status",
|
||||||
|
"bootstrap",
|
||||||
|
"constants",
|
||||||
|
"dto_backfill",
|
||||||
|
"dto_common",
|
||||||
|
"errors",
|
||||||
|
"frontend_logging",
|
||||||
|
"logging_runtime",
|
||||||
|
"splash",
|
||||||
|
"store_runtime",
|
||||||
|
"tauri",
|
||||||
|
"transport_runtime",
|
||||||
|
"tw_main",
|
||||||
|
"tw_splash",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_integration_suite_inventory_closes_v1_contracts() {
|
||||||
|
let tests_root = app_root().join("tests");
|
||||||
|
let entries = std::fs::read_dir(tests_root.as_path());
|
||||||
|
assert!(entries.is_ok(), "Backfill Desk tests directory should be readable");
|
||||||
|
let entries = match entries {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let files = entries
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
return path.file_name().and_then(std::ffi::OsStr::to_str).map(str::to_owned);
|
||||||
|
})
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
files,
|
||||||
|
std::collections::BTreeSet::from([
|
||||||
|
"config_composition.rs".to_owned(),
|
||||||
|
"dependency_boundary.rs".to_owned(),
|
||||||
|
"desktop_contract.rs".to_owned(),
|
||||||
|
"desktop_security.rs".to_owned(),
|
||||||
|
"public_api.rs".to_owned(),
|
||||||
|
"release_completeness.rs".to_owned(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_final_surface_contains_every_planned_v1_capability_without_deferred_extensions() {
|
||||||
|
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||||
|
for required in ["backfill_start", "backfill_status", "backfill_cancel", "backfill_resume"] {
|
||||||
|
assert!(tauri.contains(required), "missing final V1 capability marker {required}");
|
||||||
|
}
|
||||||
|
let options = read_text(app_root().join("src/dto_common.rs").as_path());
|
||||||
|
assert!(options.contains("program_id_options"));
|
||||||
|
assert!(options.contains("program_id_autocomplete_options"));
|
||||||
|
let status = read_text(app_root().join("src/backfill_status.rs").as_path());
|
||||||
|
for required in ["checkpoint_present", "contiguous_completed", "failure_code", "failure_domain"] {
|
||||||
|
assert!(status.contains(required), "missing final monitoring marker {required}");
|
||||||
|
}
|
||||||
|
for forbidden in ["Grpc", "Yellowstone", "WebSocketBackfill", "scheduler", "job_history"] {
|
||||||
|
assert!(!tauri.contains(forbidden), "deferred capability leaked into V1 Tauri surface via {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_013_shared_autocomplete_dto_remains_exported_and_consumed_through_crate_root() {
|
||||||
|
let root = app_root();
|
||||||
|
let lib = read_text(root.join("src/lib.rs").as_path());
|
||||||
|
assert!(lib.contains("pub(crate) use self::dto_common::ProgramIdAutocompleteOptionDto;"));
|
||||||
|
let dto = read_text(root.join("src/dto_common.rs").as_path());
|
||||||
|
assert!(dto.contains("pub(crate) struct ProgramIdAutocompleteOptionDto"));
|
||||||
|
assert!(dto.contains("std::vec::Vec<crate::ProgramIdAutocompleteOptionDto>"));
|
||||||
|
assert!(dto.contains("crate::ProgramIdAutocompleteOptionDto {"));
|
||||||
|
let bare_construction = dto.matches("\n ProgramIdAutocompleteOptionDto {").count();
|
||||||
|
assert_eq!(bare_construction, 0, "shared autocomplete DTO must not bypass the crate-root facade");
|
||||||
|
}
|
||||||
31
crates/ksp-app-backfill-desk/tsconfig.json
Normal file
31
crates/ksp-app-backfill-desk/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"frontend",
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
199
crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
Normal file
199
crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
fn ready_options() -> crate::BackfillDeskOptionsDto {
|
||||||
|
let limits = crate::backfill_request_limits();
|
||||||
|
assert!(limits.is_ok());
|
||||||
|
let limits = match limits {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => crate::BackfillRequestLimitsDto {
|
||||||
|
default_hydration_concurrency: 1,
|
||||||
|
default_max_candidates: 1,
|
||||||
|
default_max_pages: 1,
|
||||||
|
default_page_size: 1,
|
||||||
|
max_hydration_concurrency: 1,
|
||||||
|
max_candidates: 1,
|
||||||
|
max_pages: 1,
|
||||||
|
max_page_size: 1,
|
||||||
|
max_signature_text_bytes: 88,
|
||||||
|
min_signature_text_bytes: 64,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return crate::BackfillDeskOptionsDto {
|
||||||
|
commitments: crate::backfill_commitment_codes(),
|
||||||
|
composition_ready: true,
|
||||||
|
configured_networks: vec!["mainnet".to_owned()],
|
||||||
|
http_routes: vec![
|
||||||
|
crate::BackfillHttpRouteOptionDto {
|
||||||
|
pooled: true,
|
||||||
|
providers: vec!["publicnode".to_owned(), "solana-public".to_owned()],
|
||||||
|
role: "backfill_pool".to_owned(),
|
||||||
|
},
|
||||||
|
crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["publicnode".to_owned()], role: "backfill_publicnode".to_owned() },
|
||||||
|
],
|
||||||
|
limits,
|
||||||
|
network_coherent: true,
|
||||||
|
program_id_options: crate::program_id_autocomplete_options(),
|
||||||
|
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||||
|
store_diagnostic: std::option::Option::None,
|
||||||
|
store_network: std::option::Option::Some("mainnet".to_owned()),
|
||||||
|
store_ready: true,
|
||||||
|
transport_diagnostic: std::option::Option::None,
|
||||||
|
transport_ready: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(scope_kind: &str) -> crate::BackfillStartRequestDto {
|
||||||
|
return crate::BackfillStartRequestDto {
|
||||||
|
address: std::option::Option::Some("11111111111111111111111111111111".to_owned()),
|
||||||
|
anchor_signature: std::option::Option::None,
|
||||||
|
commitment: "finalized".to_owned(),
|
||||||
|
explicit_signatures: std::vec::Vec::new(),
|
||||||
|
hydration_concurrency: 4,
|
||||||
|
http_role: "backfill_pool".to_owned(),
|
||||||
|
max_candidates: 100,
|
||||||
|
max_pages: 10,
|
||||||
|
min_context_slot: std::option::Option::Some("123456".to_owned()),
|
||||||
|
page_size: 100,
|
||||||
|
scope_kind: scope_kind.to_owned(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn job_id() -> std::option::Option<ksp_job_api::JobId> {
|
||||||
|
let job_id = ksp_job_api::JobId::new("request-mapping-test");
|
||||||
|
assert!(job_id.is_ok());
|
||||||
|
return match job_id {
|
||||||
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||||
|
std::result::Result::Err(_) => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
|
||||||
|
let job_id = job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
|
||||||
|
assert!(mapped.is_ok());
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(mapped.network().as_str(), "mainnet");
|
||||||
|
assert_eq!(mapped.role().as_str(), "backfill_pool");
|
||||||
|
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
|
||||||
|
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
|
||||||
|
assert_eq!(mapped.page_size(), 100);
|
||||||
|
assert_eq!(mapped.max_pages(), 10);
|
||||||
|
assert_eq!(mapped.max_candidates(), 100);
|
||||||
|
assert_eq!(mapped.hydration_concurrency(), 4);
|
||||||
|
assert_eq!(mapped.min_context_slot(), std::option::Option::Some(123456));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn before_and_after_scopes_require_an_exact_anchor_shape() {
|
||||||
|
for scope_kind in ["before_address", "after_address"] {
|
||||||
|
let job_id = job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let mut input = request(scope_kind);
|
||||||
|
input.anchor_signature = std::option::Option::Some("1".repeat(64));
|
||||||
|
let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
|
||||||
|
assert!(mapped.is_ok(), "scope {scope_kind} should map with a bounded anchor");
|
||||||
|
}
|
||||||
|
let job_id = job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let missing_anchor = crate::map_backfill_request(request("before_address"), &ready_options(), job_id);
|
||||||
|
assert!(missing_anchor.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_signatures_reject_address_context_and_deduplicate_in_job_contract() {
|
||||||
|
let request_job_id = job_id();
|
||||||
|
let request_job_id = match request_job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let mut input = request("explicit_signatures");
|
||||||
|
input.address = std::option::Option::None;
|
||||||
|
input.min_context_slot = std::option::Option::None;
|
||||||
|
input.explicit_signatures = vec!["1".repeat(64), "1".repeat(64), "2".repeat(64)];
|
||||||
|
let mapped = crate::map_backfill_request(input, &ready_options(), request_job_id);
|
||||||
|
assert!(mapped.is_ok());
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let signatures = mapped.scope().signatures();
|
||||||
|
assert!(signatures.is_some());
|
||||||
|
if let std::option::Option::Some(signatures) = signatures {
|
||||||
|
assert_eq!(signatures.len(), 2);
|
||||||
|
}
|
||||||
|
let mut hostile = request("explicit_signatures");
|
||||||
|
hostile.address = std::option::Option::None;
|
||||||
|
hostile.min_context_slot = std::option::Option::None;
|
||||||
|
hostile.explicit_signatures = vec![" 1".to_owned()];
|
||||||
|
let hostile_job_id = job_id();
|
||||||
|
let hostile_job_id = match hostile_job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
assert!(crate::map_backfill_request(hostile, &ready_options(), hostile_job_id).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn route_inventory_and_composition_are_revalidated_before_mapping() {
|
||||||
|
let first_job_id = job_id();
|
||||||
|
let first_job_id = match first_job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let mut unknown_route = request("latest_address");
|
||||||
|
unknown_route.http_role = "provider-from-frontend".to_owned();
|
||||||
|
let rejected = crate::map_backfill_request(unknown_route, &ready_options(), first_job_id);
|
||||||
|
assert!(rejected.is_err());
|
||||||
|
let mut unavailable = ready_options();
|
||||||
|
unavailable.composition_ready = false;
|
||||||
|
let second_job_id = job_id();
|
||||||
|
let second_job_id = match second_job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
assert!(crate::map_backfill_request(request("latest_address"), &unavailable, second_job_id).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_preview_never_contains_address_or_signature_values() {
|
||||||
|
let job_id = job_id();
|
||||||
|
let job_id = match job_id {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
|
||||||
|
assert!(mapped.is_ok());
|
||||||
|
let mapped = match mapped {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let preview = crate::project_backfill_request(&mapped);
|
||||||
|
assert!(preview.is_ok());
|
||||||
|
let preview = match preview {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&preview);
|
||||||
|
assert!(serialized.is_ok());
|
||||||
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
|
assert!(!serialized.contains("11111111111111111111111111111111"));
|
||||||
|
assert!(!serialized.contains(&"1".repeat(64)));
|
||||||
|
assert!(serialized.contains("mainnet"));
|
||||||
|
assert!(serialized.contains("backfill_pool"));
|
||||||
|
}
|
||||||
|
}
|
||||||
117
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal file
117
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
||||||
|
let state = crate::BackfillRunState::new();
|
||||||
|
let first = state.next_job_id();
|
||||||
|
let second = state.next_job_id();
|
||||||
|
assert!(first.is_ok());
|
||||||
|
assert!(second.is_ok());
|
||||||
|
let first = match first {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let second = match second {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_ne!(first, second);
|
||||||
|
assert!(first.as_str().starts_with("backfill-desk-"));
|
||||||
|
assert!(second.as_str().starts_with("backfill-desk-"));
|
||||||
|
assert!(first.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
||||||
|
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancellable_runtime(
|
||||||
|
job_id: &str,
|
||||||
|
) -> std::option::Option<(ksp_job_api::JobId, ksp_job_backfill_lib::BackfillRequest, ksp_job_backfill_lib::BackfillJobRuntime)> {
|
||||||
|
let job_id = match ksp_job_api::JobId::new(job_id) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let request = ksp_job_backfill_lib::BackfillRequest::new(
|
||||||
|
job_id.clone(),
|
||||||
|
network,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
|
||||||
|
ksp_job_backfill_lib::BackfillCommitment::Finalized,
|
||||||
|
ksp_job_backfill_lib::BackfillScope::latest_address(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let request = match request {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let retained_request = request.clone();
|
||||||
|
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return std::option::Option::Some((job_id, retained_request, runtime));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn targeted_cancel_is_idempotent_and_rejects_stale_job_identity() {
|
||||||
|
let state = crate::BackfillRunState::new();
|
||||||
|
let runtime = cancellable_runtime("backfill-desk-41");
|
||||||
|
assert!(runtime.is_some());
|
||||||
|
let (job_id, request, runtime) = match runtime {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let installed = state.install(job_id.clone(), runtime.handle(), request);
|
||||||
|
assert!(installed.is_ok());
|
||||||
|
let first = state.cancel(job_id.as_str());
|
||||||
|
assert!(first.is_ok());
|
||||||
|
if let std::result::Result::Ok(first) = first {
|
||||||
|
assert!(first.accepted);
|
||||||
|
assert_eq!(first.state, "cancelling");
|
||||||
|
}
|
||||||
|
let repeated = state.cancel(job_id.as_str());
|
||||||
|
assert!(repeated.is_ok());
|
||||||
|
if let std::result::Result::Ok(repeated) = repeated {
|
||||||
|
assert!(!repeated.accepted);
|
||||||
|
assert_eq!(repeated.state, "cancelling");
|
||||||
|
}
|
||||||
|
let stale = state.cancel("backfill-desk-40");
|
||||||
|
assert!(stale.is_err());
|
||||||
|
if let std::result::Result::Err(error) = stale {
|
||||||
|
assert_eq!(error.code().domain(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.domain());
|
||||||
|
assert_eq!(error.code().code(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_requires_terminal_checkpoint_and_rejects_active_run() {
|
||||||
|
let state = crate::BackfillRunState::new();
|
||||||
|
let next = match ksp_job_api::JobId::new("backfill-desk-50") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let unavailable = state.resume_request(next.clone());
|
||||||
|
assert!(unavailable.is_err());
|
||||||
|
if let std::result::Result::Err(error) = unavailable {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
let runtime = cancellable_runtime("backfill-desk-49");
|
||||||
|
assert!(runtime.is_some());
|
||||||
|
let (job_id, request, runtime) = match runtime {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let installed = state.install(job_id, runtime.handle(), request);
|
||||||
|
assert!(installed.is_ok());
|
||||||
|
let active = state.resume_request(next);
|
||||||
|
assert!(active.is_err());
|
||||||
|
if let std::result::Result::Err(error) = active {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RUN_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
Normal file
76
crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
|
||||||
|
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let scope = match ksp_job_backfill_lib::BackfillScope::explicit_signatures(vec![signature]) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let job_id = match ksp_job_api::JobId::new("backfill-desk-monitor-test") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let request = ksp_job_backfill_lib::BackfillRequest::new(
|
||||||
|
job_id,
|
||||||
|
network,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
|
||||||
|
ksp_job_backfill_lib::BackfillCommitment::Confirmed,
|
||||||
|
scope,
|
||||||
|
100,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let request = match request {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let source = runtime.handle().snapshots();
|
||||||
|
return std::option::Option::Some(<ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_value_projection_is_safe_complete_and_uses_decimal_sequence_text() {
|
||||||
|
let notification = match initial_notification() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let status = crate::project_backfill_status(¬ification);
|
||||||
|
assert!(status.is_ok());
|
||||||
|
let status = match status {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert!(status.active);
|
||||||
|
assert!(!status.terminal);
|
||||||
|
assert_eq!(status.job_id, "backfill-desk-monitor-test");
|
||||||
|
assert_eq!(status.job_kind, ksp_job_backfill_lib::BACKFILL_JOB_KIND_CODE);
|
||||||
|
assert_eq!(status.sequence, "0");
|
||||||
|
assert_eq!(status.state, "created");
|
||||||
|
assert_eq!(status.phase, "created");
|
||||||
|
assert_eq!(status.scope_kind, "explicit_signatures");
|
||||||
|
assert!(!status.checkpoint_present);
|
||||||
|
assert_eq!(status.candidates_selected, 0);
|
||||||
|
assert_eq!(status.failure_code, std::option::Option::None);
|
||||||
|
assert_eq!(status.failure_domain, std::option::Option::None);
|
||||||
|
let serialized = serde_json::to_string(&status);
|
||||||
|
assert!(serialized.is_ok());
|
||||||
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
|
for forbidden in ["address", "signature\"", "endpoint", "credential", "token", "postgres"] {
|
||||||
|
assert!(!serialized.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
Normal file
75
crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_limits_are_derived_from_job_owned_public_bounds() {
|
||||||
|
let limits = crate::backfill_request_limits();
|
||||||
|
assert!(limits.is_ok());
|
||||||
|
let limits = match limits {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(usize::try_from(limits.max_page_size), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE));
|
||||||
|
assert_eq!(usize::try_from(limits.max_pages), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGES));
|
||||||
|
assert_eq!(usize::try_from(limits.max_candidates), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES));
|
||||||
|
assert_eq!(usize::try_from(limits.max_hydration_concurrency), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY));
|
||||||
|
assert_eq!(usize::try_from(limits.min_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES));
|
||||||
|
assert_eq!(usize::try_from(limits.max_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES));
|
||||||
|
assert!(limits.default_page_size <= limits.max_page_size);
|
||||||
|
assert!(limits.default_max_pages <= limits.max_pages);
|
||||||
|
assert!(limits.default_max_candidates <= limits.max_candidates);
|
||||||
|
assert!(limits.default_hydration_concurrency <= limits.max_hydration_concurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_options_expose_exact_current_commitments_and_http_scopes() {
|
||||||
|
assert_eq!(crate::backfill_commitment_codes(), vec!["finalized".to_owned(), "confirmed".to_owned()]);
|
||||||
|
assert_eq!(
|
||||||
|
crate::backfill_scope_kind_codes(),
|
||||||
|
vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_acknowledgement_contains_only_backend_job_identity_and_initial_state() {
|
||||||
|
let response = crate::BackfillStartResponseDto { job_id: "backfill-desk-1".to_owned(), state: "created".to_owned() };
|
||||||
|
let serialized = serde_json::to_string(&response);
|
||||||
|
assert!(serialized.is_ok());
|
||||||
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
|
assert!(serialized.contains("backfill-desk-1"));
|
||||||
|
assert!(serialized.contains("created"));
|
||||||
|
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token"] {
|
||||||
|
assert!(!serialized.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cancel_acknowledgement_contains_only_backend_job_identity_acceptance_and_state() {
|
||||||
|
let response = crate::BackfillCancelResponseDto { accepted: true, job_id: "backfill-desk-1".to_owned(), state: "cancelling".to_owned() };
|
||||||
|
let serialized = serde_json::to_string(&response);
|
||||||
|
assert!(serialized.is_ok());
|
||||||
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
|
assert!(serialized.contains("backfill-desk-1"));
|
||||||
|
assert!(serialized.contains("cancelling"));
|
||||||
|
assert!(serialized.contains("accepted"));
|
||||||
|
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token", "checkpoint", "payload"] {
|
||||||
|
assert!(!serialized.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_acknowledgement_contains_only_new_backend_job_identity_acceptance_and_state() {
|
||||||
|
let response = crate::BackfillResumeResponseDto { accepted: true, job_id: "backfill-desk-2".to_owned(), state: "created".to_owned() };
|
||||||
|
let serialized = serde_json::to_string(&response);
|
||||||
|
assert!(serialized.is_ok());
|
||||||
|
if let std::result::Result::Ok(serialized) = serialized {
|
||||||
|
assert!(serialized.contains("backfill-desk-2"));
|
||||||
|
assert!(serialized.contains("created"));
|
||||||
|
assert!(serialized.contains("accepted"));
|
||||||
|
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token", "checkpoint", "payload"] {
|
||||||
|
assert!(!serialized.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
Normal file
67
crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
|
||||||
|
// version: 6
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
|
||||||
|
let source = std::io::Error::other("secret-source");
|
||||||
|
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failed"), "safe message")
|
||||||
|
.with_context("secret", "hidden")
|
||||||
|
.with_source(source);
|
||||||
|
let dto = crate::CommandErrorDto::from_error(&error);
|
||||||
|
assert_eq!(dto.domain, "test");
|
||||||
|
assert_eq!(dto.code, "failed");
|
||||||
|
assert_eq!(dto.message, "safe message");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_options_projection_contains_only_safe_transport_metadata() {
|
||||||
|
let limits = crate::backfill_request_limits();
|
||||||
|
assert!(limits.is_ok());
|
||||||
|
let limits = match limits {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let dto = crate::BackfillDeskOptionsDto {
|
||||||
|
commitments: crate::backfill_commitment_codes(),
|
||||||
|
http_routes: vec![crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["solana-public".to_owned()], role: "default".to_owned() }],
|
||||||
|
limits,
|
||||||
|
composition_ready: true,
|
||||||
|
configured_networks: vec!["devnet".to_owned()],
|
||||||
|
network_coherent: true,
|
||||||
|
program_id_options: crate::program_id_autocomplete_options(),
|
||||||
|
scope_kinds: crate::backfill_scope_kind_codes(),
|
||||||
|
store_diagnostic: std::option::Option::None,
|
||||||
|
store_network: std::option::Option::Some("devnet".to_owned()),
|
||||||
|
store_ready: true,
|
||||||
|
transport_diagnostic: std::option::Option::None,
|
||||||
|
transport_ready: true,
|
||||||
|
};
|
||||||
|
let value = serde_json::to_value(dto);
|
||||||
|
assert!(value.is_ok());
|
||||||
|
if let std::result::Result::Ok(value) = value {
|
||||||
|
let serialized = value.to_string();
|
||||||
|
assert!(serialized.contains("default"));
|
||||||
|
assert!(serialized.contains("devnet"));
|
||||||
|
assert!(serialized.contains("solana-public"));
|
||||||
|
assert!(!serialized.contains("url"));
|
||||||
|
assert!(!serialized.contains("endpoint"));
|
||||||
|
assert!(!serialized.contains("postgres"));
|
||||||
|
assert!(!serialized.contains("connection"));
|
||||||
|
assert!(!serialized.contains("database"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn program_id_autocomplete_dataset_is_derived_exactly_from_core_registry() {
|
||||||
|
let options = crate::program_id_autocomplete_options();
|
||||||
|
let entries = ksp_core_lib::entries();
|
||||||
|
assert_eq!(options.len(), entries.len());
|
||||||
|
for (option, entry) in options.iter().zip(entries.iter()) {
|
||||||
|
assert_eq!(option.code, entry.code());
|
||||||
|
assert_eq!(option.name, entry.name());
|
||||||
|
assert_eq!(option.program_id, entry.program_id());
|
||||||
|
assert_eq!(option.domain, entry.domain());
|
||||||
|
assert_eq!(option.family, entry.family());
|
||||||
|
assert_eq!(option.protocol, entry.protocol());
|
||||||
|
}
|
||||||
|
}
|
||||||
40
crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs
Normal file
40
crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_log_levels_are_explicit_and_case_insensitive() {
|
||||||
|
assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
|
||||||
|
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
|
||||||
|
assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
|
||||||
|
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
|
||||||
|
assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_frontend_log_level_is_rejected() {
|
||||||
|
let result = super::parse_level("notice");
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = match result {
|
||||||
|
std::result::Result::Ok(_) => return,
|
||||||
|
std::result::Result::Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_log_targets_are_whitelisted() {
|
||||||
|
assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
|
||||||
|
assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
|
||||||
|
assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arbitrary_frontend_log_target_is_rejected() {
|
||||||
|
let result = super::parse_target("ksp-app-backfill-desk.test.logging");
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = match result {
|
||||||
|
std::result::Result::Ok(_) => return,
|
||||||
|
std::result::Result::Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
|
||||||
|
}
|
||||||
36
crates/ksp-app-backfill-desk/unit_tests/splash.rs
Normal file
36
crates/ksp-app-backfill-desk/unit_tests/splash.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fallback_splash_settings_are_short_and_ordered() {
|
||||||
|
let settings = crate::SplashSettings::fallback();
|
||||||
|
assert_eq!(settings.fade_in_ms(), 300);
|
||||||
|
assert_eq!(settings.minimum_ms(), 1200);
|
||||||
|
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.fade_out_source(), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splash_order_contract_keeps_action_message_status_and_duration() {
|
||||||
|
let order =
|
||||||
|
crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
|
||||||
|
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
|
||||||
|
let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
|
||||||
|
let oversized_fade_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!(oversized_minimum.is_err());
|
||||||
|
assert!(oversized_fade_in.is_err());
|
||||||
|
assert!(oversized_fade_out.is_err());
|
||||||
|
}
|
||||||
18
crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
Normal file
18
crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_health_mapping_is_explicit_and_conservative() {
|
||||||
|
assert!(super::store_health_ready(ksp_store_lib::StoreHealthState::Ready));
|
||||||
|
assert!(!super::store_health_ready(ksp_store_lib::StoreHealthState::NotReady));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_transport_network_coherence_requires_exact_logical_identity() {
|
||||||
|
assert!(super::validate_network_coherence("devnet", "devnet").is_ok());
|
||||||
|
let mismatch = super::validate_network_coherence("mainnet", "devnet");
|
||||||
|
assert!(mismatch.is_err());
|
||||||
|
if let std::result::Result::Err(error) = mismatch {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
crates/ksp-app-backfill-desk/unit_tests/transport_runtime.rs
Normal file
133
crates/ksp-app-backfill-desk/unit_tests/transport_runtime.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/unit_tests/transport_runtime.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
fn endpoint(
|
||||||
|
name: &str,
|
||||||
|
cluster: &str,
|
||||||
|
role_name: &str,
|
||||||
|
request_kinds: std::vec::Vec<ksp_onchain_transport_lib::HttpRequestKind>,
|
||||||
|
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpEndpointSettings> {
|
||||||
|
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(format!("https://{name}.example.invalid"));
|
||||||
|
let url = match url {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new(role_name),
|
||||||
|
true,
|
||||||
|
request_kinds,
|
||||||
|
0,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
name,
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new("fixture"),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new(cluster),
|
||||||
|
url,
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
vec![role],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pool(endpoints: std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointSettings>) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||||
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||||
|
endpoints,
|
||||||
|
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(10), std::time::Duration::from_millis(10)),
|
||||||
|
);
|
||||||
|
return ksp_onchain_transport_lib::HttpTransportPool::new(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_routes_require_both_backfill_rpc_methods() {
|
||||||
|
let signatures = ksp_onchain_transport_lib::find_http_rpc_method("getSignaturesForAddress");
|
||||||
|
let transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction");
|
||||||
|
assert!(signatures.is_some());
|
||||||
|
assert!(transaction.is_some());
|
||||||
|
let signatures = match signatures {
|
||||||
|
std::option::Option::Some(value) => ksp_onchain_transport_lib::HttpRequestKind::new(value.request_kind()),
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let transaction = match transaction {
|
||||||
|
std::option::Option::Some(value) => ksp_onchain_transport_lib::HttpRequestKind::new(value.request_kind()),
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let complete = endpoint("complete", "devnet", "complete", vec![signatures.clone(), transaction]);
|
||||||
|
let complete = match complete {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let partial = endpoint("partial", "devnet", "partial", vec![signatures]);
|
||||||
|
let partial = match partial {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let runtime_pool = pool(vec![complete, partial]);
|
||||||
|
assert!(runtime_pool.is_ok());
|
||||||
|
let runtime_pool = match runtime_pool {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let snapshot = runtime_pool.snapshot();
|
||||||
|
let roles = super::compatible_backfill_http_routes(&runtime_pool, &snapshot);
|
||||||
|
assert!(roles.is_ok());
|
||||||
|
if let std::result::Result::Ok(roles) = roles {
|
||||||
|
assert_eq!(roles.len(), 1);
|
||||||
|
if let std::option::Option::Some(route) = roles.first() {
|
||||||
|
assert_eq!(route.role, "complete");
|
||||||
|
assert_eq!(route.providers, vec!["fixture".to_owned()]);
|
||||||
|
assert!(!route.pooled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_networks_are_safe_sorted_and_deduplicated() {
|
||||||
|
let one = endpoint("one", "testnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let two = endpoint("two", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let three = endpoint("three", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let (one, two, three) = match (one, two, three) {
|
||||||
|
(std::result::Result::Ok(one), std::result::Result::Ok(two), std::result::Result::Ok(three)) => (one, two, three),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let runtime_pool = pool(vec![one, two, three]);
|
||||||
|
assert!(runtime_pool.is_ok());
|
||||||
|
if let std::result::Result::Ok(runtime_pool) = runtime_pool {
|
||||||
|
let values = super::configured_networks(&runtime_pool.snapshot());
|
||||||
|
assert_eq!(values, vec!["devnet".to_owned(), "testnet".to_owned()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coherent_network_requires_exactly_one_enabled_http_cluster() {
|
||||||
|
let one = endpoint("one", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let one = match one {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let one_pool = pool(vec![one]);
|
||||||
|
assert!(one_pool.is_ok());
|
||||||
|
if let std::result::Result::Ok(one_pool) = one_pool {
|
||||||
|
let runtime = crate::TransportRuntime { pool: one_pool, profile_id: "fixture".to_owned() };
|
||||||
|
assert_eq!(runtime.coherent_network(), std::option::Option::Some("devnet".to_owned()));
|
||||||
|
}
|
||||||
|
let two = endpoint("two", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let three = endpoint("three", "testnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
|
||||||
|
let (two, three) = match (two, three) {
|
||||||
|
(std::result::Result::Ok(two), std::result::Result::Ok(three)) => (two, three),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let multi_pool = pool(vec![two, three]);
|
||||||
|
assert!(multi_pool.is_ok());
|
||||||
|
if let std::result::Result::Ok(multi_pool) = multi_pool {
|
||||||
|
let runtime = crate::TransportRuntime { pool: multi_pool, profile_id: "fixture".to_owned() };
|
||||||
|
assert_eq!(runtime.coherent_network(), std::option::Option::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
crates/ksp-app-backfill-desk/vite.config.ts
Normal file
72
crates/ksp-app-backfill-desk/vite.config.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// file: crates/ksp-app-backfill-desk/vite.config.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import { NodePackageImporter } from "sass-embedded";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { defineConfig, normalizePath } from "vite";
|
||||||
|
|
||||||
|
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
||||||
|
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"));
|
||||||
|
const devHost = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
clearScreen: false,
|
||||||
|
root: frontendRoot,
|
||||||
|
publicDir: false,
|
||||||
|
input: {
|
||||||
|
main: normalizePath(resolve(frontendRoot, "main.html")),
|
||||||
|
splash: normalizePath(resolve(frontendRoot, "splash.html")),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: frontendDist,
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
cssCodeSplit: true,
|
||||||
|
rolldownOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: "js/[name]-[hash].js",
|
||||||
|
chunkFileNames: "js/chunks/[name]-[hash].js",
|
||||||
|
assetFileNames: assetInfo => {
|
||||||
|
const originalName = assetInfo.names[0] ?? "";
|
||||||
|
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
|
||||||
|
if (extension === "css") {
|
||||||
|
return "css/[name]-[hash][extname]";
|
||||||
|
}
|
||||||
|
if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
|
||||||
|
return "fonts/[name]-[hash][extname]";
|
||||||
|
}
|
||||||
|
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
|
||||||
|
return "imgs/[name][extname]";
|
||||||
|
}
|
||||||
|
return "otherassets/[name][extname]";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
quietDeps: true,
|
||||||
|
silenceDeprecations: ["import", "color-functions", "global-builtin"],
|
||||||
|
verbose: false,
|
||||||
|
importers: [new NodePackageImporter()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 1436,
|
||||||
|
strictPort: true,
|
||||||
|
host: devHost || false,
|
||||||
|
ws: {
|
||||||
|
protocol: "ws",
|
||||||
|
host: devHost || "localhost",
|
||||||
|
port: 1437,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
ignored: ["**/src/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-app-config-desk/Cargo.toml
|
# file: crates/ksp-app-config-desk/Cargo.toml
|
||||||
# version: 8
|
# version: 9
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-app-config-desk"
|
name = "ksp-app-config-desk"
|
||||||
@@ -22,6 +22,7 @@ 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" }
|
||||||
@@ -30,7 +31,6 @@ 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, features = ["std", "now"] }
|
|
||||||
tokio = { workspace = true, features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
ts-rs.workspace = true
|
ts-rs.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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/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: 4 -->
|
||||||
<!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</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)]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user