212 Commits

Author SHA1 Message Date
7328be6997 v0.3.15-rel.001 2026-09-19 12:40:28 +02:00
d32299ea18 v0.3.15-pre.018 2026-09-19 12:40:04 +02:00
cd5c79c253 v0.3.15-pre.017 2026-09-19 08:36:52 +02:00
5f19786ce9 0.3.15-pre.016 2026-09-19 05:08:05 +02:00
b1b0e91616 0.3.15-pre.015-fix.001 2026-09-18 23:33:08 +02:00
582a286baa 0.3.15-pre.015 2026-09-18 21:06:58 +02:00
b266019ea8 0.3.15-pre.014-fix.003 2026-09-18 13:36:51 +02:00
463c82bfcf 0.3.15-pre.014-fix.002 2026-09-18 00:42:56 +02:00
6d820bf0d7 0.3.15-pre.014-fix.001 2026-09-17 23:09:21 +02:00
ad24cb36fa 0.3.15-pre.014 2026-09-17 13:02:10 +02:00
6ee4e3bdf4 0.3.15-pre.013 2026-09-17 09:34:11 +02:00
1573273d27 0.3.15-pre.012-fix.004 2026-09-16 23:49:07 +02:00
d56de4ace9 0.3.15-pre.012-fix.003 2026-09-16 22:22:48 +02:00
888ce3f35f 0.3.15-pre.012-fix.001 2026-09-16 17:02:28 +02:00
1c28f3155f 0.3.15-pre.012-fix.001 2026-09-16 11:38:02 +02:00
31ae38853d 0.3.15-pre.012 2026-09-16 10:58:29 +02:00
2f587e22be 0.3.15-pre.011-fix.001 2026-09-16 09:38:30 +02:00
a248be04ca 0.3.15-pre.011 2026-09-16 09:07:31 +02:00
e919945d1e 0.3.15-pre.010-fix.001 2026-09-15 05:53:36 +02:00
9fd51c5a41 0.3.15-pre.009-fix.003 2026-09-14 21:06:32 +02:00
8ae4d0effd v0.3.15-pre.009-fix.002 2026-09-14 15:19:55 +02:00
d7c2931c10 v0.3.15-pre.009-fix.001 2026-09-14 14:47:04 +02:00
11105fac28 v0.3.15-pre.009 2026-09-14 11:38:55 +02:00
d3eea1aa12 v0.3.15-pre.008-fix.004 2026-09-14 09:24:27 +02:00
11e5247fbf v0.3.15-pre.008-fix.003 2026-09-13 23:05:18 +02:00
ee658eb862 v0.3.15-pre.008-fix.002 2026-09-13 22:40:15 +02:00
fe512637d4 v0.3.15-pre.008-fix.001 2026-09-13 22:34:26 +02:00
c71fb2d8fe v0.3.15-pre.008 2026-09-13 22:28:21 +02:00
35bd1bb226 v0.3.15-pre.007-fix.002 2026-09-13 21:59:35 +02:00
320699e61a v0.3.15-pre.007-fix.001 2026-09-13 17:51:44 +02:00
0d9ec5c490 v0.3.15-pre.007 2026-09-13 17:36:06 +02:00
487bcbf0c1 v0.3.15-pre.006-fix.003 2026-09-13 17:11:21 +02:00
f12e67bdc7 v0.3.15-pre.006-fix.002 2026-09-13 16:34:18 +02:00
f3ed86e54c v0.3.15-pre.006-fix.001 2026-09-13 15:47:32 +02:00
0a8ce6e5f9 v0.3.15-pre.006 2026-09-13 11:30:56 +02:00
9ee3cd4a53 v0.3.15-pre.005-fix.002 2026-09-13 11:29:38 +02:00
b8d46fbb2b v0.3.15-pre.005-fix.001 2026-09-13 10:10:20 +02:00
b5554a9f9c v0.3.15-pre.005 2026-09-13 09:57:39 +02:00
8adf1bda3f v0.3.15-pre.004 2026-09-13 08:47:43 +02:00
dc8863a46d v0.3.15-pre.004 2026-09-13 08:47:31 +02:00
bd376dc479 v0.3.15-pre.003 2026-09-13 08:11:35 +02:00
dc126497db v0.3.15-pre.002-fix.001 2026-09-13 08:09:39 +02:00
8ea261d3a4 v0.3.15-pre.002 2026-09-13 06:07:42 +02:00
84ef8bb1f1 v0.3.15-pre.001-fix.001 2026-09-13 00:33:44 +02:00
4ec80889c2 v0.3.15-pre.001 2026-09-13 00:27:37 +02:00
002d08ba5f v0.3.14-rel.001 2026-09-12 23:46:51 +02:00
a7ac30f39b v0.3.14-pre.016 2026-09-12 23:31:19 +02:00
cd7cf2c4c6 v0.3.14-pre.015 2026-09-12 23:07:58 +02:00
8d8b6b9bcb v0.3.14-pre.014 2026-09-12 21:29:00 +02:00
02012f105a v0.3.14-pre.013-fix.001 2026-09-12 20:55:57 +02:00
09c91172ae v0.3.14-pre.013 2026-09-12 20:43:43 +02:00
8a0717c839 v0.3.14-pre.012 2026-09-12 20:28:00 +02:00
f3c4169752 v0.3.14-pre.011-fix.002 2026-09-12 19:29:26 +02:00
eedad4b81e v0.3.14-pre.011-fix.001 2026-09-12 19:07:27 +02:00
4e39ddd5d0 v0.3.14-pre.011 2026-09-12 19:05:43 +02:00
c99cb048bf v0.3.14-pre.010-fix.001 2026-09-12 18:11:16 +02:00
8c4b458724 v0.3.14-pre.010 2026-09-12 14:20:02 +02:00
792560191c v0.3.14-pre.009-fix.002 2026-09-12 13:04:49 +02:00
c884ce88ba v0.3.14-pre.009-fix.001 2026-09-12 12:30:27 +02:00
4bcd942928 v0.3.14-pre.009 2026-09-12 11:22:53 +02:00
bf394e0e4b v0.3.14-pre.008-fix.002 2026-09-12 09:45:58 +02:00
ec94001ab8 v0.3.14-pre.008-fix.001 2026-09-12 09:44:36 +02:00
505cc8c3f6 v0.3.14-pre.008 2026-09-12 09:00:36 +02:00
9fc30d3ab9 v0.3.14-pre.007-fix.001 2026-09-12 08:04:19 +02:00
887664f3c0 v0.3.14-pre.007 2026-09-12 07:47:16 +02:00
ae712eae64 v0.3.14-pre.006 2026-09-11 23:48:53 +02:00
a3df8f6ef5 v0.3.14-pre.005 2026-09-11 23:15:00 +02:00
1006079654 v0.3.14-pre.004 2026-09-11 22:22:51 +02:00
6721a4142a v0.3.14-pre.003 2026-09-11 21:54:19 +02:00
7bf938c19f v0.3.14-pre.002-fix.002 2026-09-11 20:55:53 +02:00
5932250314 v0.3.14-pre.002-fix.001 2026-09-11 20:00:56 +02:00
b2068e9ef2 v0.3.14-pre.002 2026-09-11 19:58:32 +02:00
2ca56dbaf4 v0.3.14-pre.001 2026-09-11 19:37:36 +02:00
9785cda39d v0.3.13-rel.001 2026-09-11 00:41:22 +02:00
0cea99cd5e v0.3.13-pre.015 2026-09-11 00:39:51 +02:00
71bc754feb v0.3.13-pre.014 2026-09-10 23:25:05 +02:00
cabbfed730 v0.3.13-pre.013 2026-09-10 22:15:21 +02:00
8b6ef7778a v0.3.13-pre.012 2026-09-10 21:55:02 +02:00
06319655b0 v0.3.13-pre.011 2026-09-10 21:27:18 +02:00
f636169783 v0.3.13-pre.010 2026-09-10 21:02:29 +02:00
0f8f38ff52 v0.3.13-pre.009-fix.002 2026-09-10 20:24:12 +02:00
142998afb3 v0.3.13-pre.009-fix.001 2026-09-10 20:13:01 +02:00
665d7d2bb6 v0.3.13-pre.009 2026-09-10 19:56:16 +02:00
1746d47412 v0.3.13-pre.008-fix.002 2026-09-10 18:25:54 +02:00
9bb2f7bcae v0.3.13-pre.008-fix.001 2026-09-10 17:55:46 +02:00
1f2720463b v0.3.13-pre.008 2026-09-10 17:52:35 +02:00
8bb530e9b4 v0.3.13-pre.007 2026-09-10 17:33:02 +02:00
72df965a9c v0.3.13-pre.006-fix.001 2026-09-10 16:17:50 +02:00
f688325170 v0.3.13-pre.006 2026-09-10 16:05:52 +02:00
c711213dcc v0.3.13-pre.005-fix.001 2026-09-10 15:23:09 +02:00
11ecdccd65 v0.3.13-pre.005 2026-09-10 15:01:00 +02:00
27ab84d198 v0.3.13-pre.004-fix.001 2026-09-10 14:51:28 +02:00
28e4da3879 v0.3.13-pre.004 2026-09-10 14:25:30 +02:00
31c0a83e51 v0.3.13-pre.003-fix.003 2026-09-10 13:44:17 +02:00
34fddc3bf5 v0.3.13-pre.003-fix.002 2026-09-10 13:20:09 +02:00
13c3dac554 v0.3.13-pre.003-fix.001 2026-09-10 13:12:25 +02:00
f7ed053e8e v0.3.13-pre.003 2026-09-10 11:38:34 +02:00
2e8feb56dd v0.3.13-pre.002-fix.001 2026-09-10 10:00:42 +02:00
793178b345 v0.3.13-pre.002 2026-09-10 09:50:42 +02:00
ee0359efd5 v0.3.13-pre.001 2026-09-10 09:01:00 +02:00
1dc57a85c2 v0.3.12-rel.001 2026-09-10 00:09:44 +02:00
e7e8960fec v0.3.12-pre.013 2026-09-10 00:00:12 +02:00
79e278064d v0.3.12-pre.012 2026-09-09 23:24:20 +02:00
474b894d1c v0.3.12-pre.011 2026-09-09 23:07:52 +02:00
0c76b74afe v0.3.12-pre.010 2026-09-09 22:35:30 +02:00
b608ed2c50 v0.3.12-pre.009-fix.001 2026-09-09 22:14:29 +02:00
f9d4e6bc74 v0.3.12-pre.009 2026-09-09 22:06:18 +02:00
223f11b989 v0.3.12-pre.008-fix.002 2026-09-09 20:57:54 +02:00
5095be0b0d v0.3.12-pre.008-fix.001 2026-09-09 20:46:44 +02:00
2aa64cf0a4 v0.3.12-pre.008 2026-09-09 20:42:32 +02:00
90c266d68a v0.3.12-pre.007-fix.002 2026-09-09 17:14:47 +02:00
8410b2f4ef v0.3.12-pre.007-fix.001 2026-09-09 16:22:09 +02:00
ba69a5ad20 v0.3.12-pre.007 2026-09-09 16:21:23 +02:00
07b9b20eb5 v0.3.12-pre.006-fix.001 2026-09-09 15:00:27 +02:00
901a0515b7 v0.3.12-pre.006 2026-09-09 13:30:15 +02:00
50c01a19c9 v0.3.12-pre.005-fix.001 2026-09-09 12:59:51 +02:00
3a8fa87acb v0.3.12-pre.005 2026-09-09 12:45:29 +02:00
df1dbbb8fb v0.3.12-pre.004-fix.001 2026-09-09 11:55:36 +02:00
531ca851c4 v0.3.12-pre.004 2026-09-09 11:44:36 +02:00
b3fd74529d v0.3.12-pre.003-fix.002 2026-09-09 11:43:19 +02:00
00b376d234 v0.3.12-pre.003-fix.001 2026-09-09 08:14:45 +02:00
fa720d0054 v0.3.12-pre.003 2026-09-09 07:39:42 +02:00
91438e8214 v0.3.12-pre.002 2026-09-09 07:12:37 +02:00
08e9a34183 v0.3.12-pre.001 2026-09-08 23:44:19 +02:00
85937b8cbb v0.3.11-rel.001 2026-09-08 22:02:24 +02:00
107986bf85 v0.3.11-pre.013 2026-09-08 21:11:52 +02:00
a8cabbebd5 v0.3.11-pre.012 2026-09-08 18:58:34 +02:00
636a0c5f43 v0.3.11-pre.011 2026-09-08 18:16:46 +02:00
bf46782db3 v0.3.11-pre.010-fix.001 2026-09-08 17:45:37 +02:00
02919e959e v0.3.11-pre.010 2026-09-08 17:28:37 +02:00
d231eba8be v0.3.11-pre.009-fix.003 2026-09-08 16:58:15 +02:00
637382f364 v0.3.11-pre.009-fix.002 2026-09-08 16:52:06 +02:00
bf6ef5b426 v0.3.11-pre.009-fix.001 2026-09-08 16:48:22 +02:00
822e8fcd86 v0.3.11-pre.009 2026-09-08 16:22:40 +02:00
63e1a866a3 v0.3.11-pre.008-fix.001 2026-09-08 12:51:16 +02:00
22c88c449d v0.3.11-pre.008 2026-09-08 12:28:51 +02:00
ed6c0ac10c v0.3.11-pre.007 2026-09-08 11:32:41 +02:00
355c5d1e9a v0.3.11-pre.006-fix.001 2026-09-08 11:03:31 +02:00
61d8b1dee2 v0.3.11-pre.006 2026-09-08 10:54:40 +02:00
0db32a865c v0.3.11-pre.005 2026-09-08 10:24:04 +02:00
a4061ee1e1 v0.3.11-pre.004 2026-09-08 10:11:07 +02:00
ec64be340e v0.3.11-pre.003-fix.001 2026-09-08 09:35:26 +02:00
f1c383c392 v0.3.11-pre.003 2026-09-08 09:31:23 +02:00
984c327162 v0.3.11-pre.002 2026-09-08 09:10:57 +02:00
e2689d1b04 v0.3.11-pre.001 2026-09-08 08:57:07 +02:00
b5c84f3df8 v0.3.10-rel.001 2026-09-08 08:17:31 +02:00
36c683df9e v0.3.10-pre.009 2026-09-08 08:07:06 +02:00
510adcb49b v0.3.10-pre.008 2026-09-08 06:43:02 +02:00
ab87fd23cd v0.3.10-pre.007 2026-09-08 00:24:58 +02:00
1a352aa8d0 v0.3.10-pre.006-fix.002 2026-09-08 00:17:25 +02:00
919b4ed46d v0.3.10-pre.006-fix.001 2026-09-07 23:56:48 +02:00
f7c57f21c5 v0.3.10-pre.006 2026-09-07 18:58:48 +02:00
d4f4237723 v0.3.10-pre.005-fix.002 2026-09-06 23:00:44 +02:00
ba574f3052 v0.3.10-pre.005-fix.001 2026-09-06 22:30:27 +02:00
0683b70abe v0.3.10-pre.005 2026-09-06 21:59:15 +02:00
eaf6fae94c v0.3.10-pre.004-fix.001 2026-09-06 18:02:01 +02:00
e19a202565 v0.3.10-pre.004 2026-09-06 17:15:51 +02:00
8309d829cc v0.3.10-pre.003 2026-09-06 10:41:47 +02:00
aabe8dedb2 v0.3.10-pre.002-fix.001 2026-09-06 09:59:28 +02:00
d0da907c00 v0.3.10-pre.002 2026-09-06 09:58:31 +02:00
6b434750c5 v0.3.10-pre.001 2026-09-06 09:05:33 +02:00
c9047af626 v0.3.9-rel.001 2026-09-05 20:47:42 +02:00
52d692e8ec v0.3.9-pre.009 2026-09-05 20:30:45 +02:00
5c3c8b3fef v0.3.9-pre.008 2026-09-05 17:39:50 +02:00
b0079fc3ee v0.3.9-pre.007-fix.002 2026-09-05 15:27:43 +02:00
ecfa123a79 v0.3.9-pre.007-fix.001 2026-09-05 14:54:01 +02:00
9da372e4a2 v0.3.9-pre.007 2026-09-05 14:36:12 +02:00
9a998e7b52 v0.3.9-pre.006-fix.003 2026-09-05 13:53:36 +02:00
c3c9a80310 v0.3.9-pre.006-fix.002 2026-09-05 12:32:20 +02:00
116eabba7d v0.3.9-pre.006-fix.001 2026-09-05 02:24:47 +02:00
e6c2649410 v0.3.9-pre.006 2026-09-05 00:10:15 +02:00
e85d6f6963 v0.3.9-pre.005 2026-09-04 21:53:36 +02:00
eb39aaa904 v0.3.9-pre.004 2026-09-04 21:23:46 +02:00
bb7ee3cc3a v0.3.9-pre.003 2026-09-04 14:45:05 +02:00
84647c12ea v0.3.9-pre.002-fix.002 2026-09-04 14:29:24 +02:00
156d237460 v0.3.9-pre.002-fix.001 2026-09-04 14:23:54 +02:00
7d0710a1e6 v0.3.9-pre.002 2026-09-04 14:21:05 +02:00
5c797827f7 v0.3.9-pre.001-fix.002 2026-09-04 14:11:21 +02:00
b87b7f4505 v0.3.9-pre.001-fix.001 2026-09-04 13:16:08 +02:00
92f08ca5a8 v0.3.9-pre.001 2026-09-04 12:47:53 +02:00
85f292a36d v0.3.8-rel.001 2026-09-04 10:51:31 +02:00
877411c6e5 v0.3.8-pre.014 2026-09-04 10:48:25 +02:00
081a5352c0 v0.3.8-pre.013-fix.001 2026-09-04 10:44:45 +02:00
c0b3190256 v0.3.8-pre.013 2026-09-04 09:58:20 +02:00
eaefec2259 v0.3.8-pre.012-fix.001 2026-09-04 07:23:33 +02:00
cfbf849e2a v0.3.8-pre.012 2026-09-03 23:21:23 +02:00
e01fe88594 v0.3.8-pre.011-fix.001 2026-09-03 22:34:02 +02:00
80fe2fd9e2 v0.3.8-pre.011 2026-09-03 22:31:28 +02:00
516c52ece9 v0.3.8-pre.010-fix.002 2026-09-03 22:05:45 +02:00
2b79818848 v0.3.8-pre.010-fix.001 2026-09-03 21:47:02 +02:00
e2e603ca6d v0.3.8-pre.010 2026-09-03 21:36:34 +02:00
01502678bf v0.3.8-pre.009-fix.001 2026-09-03 20:52:31 +02:00
32c4b67541 v0.3.8-pre.009 2026-09-03 20:44:21 +02:00
8c23625fee v0.3.8-pre.008-fix.001 2026-09-03 19:59:25 +02:00
489d686023 v0.3.8-pre.008 2026-09-03 19:53:25 +02:00
10515d1562 v0.3.8-pre.007-fix.003 2026-09-03 18:59:39 +02:00
c9e52d1926 v0.3.8-pre.007-fix.002 2026-09-03 18:51:37 +02:00
32577ea48d v0.3.8-pre.00-fix.001 2026-09-03 18:45:53 +02:00
d5b4aef194 v0.3.8-pre.007 2026-09-03 17:54:50 +02:00
8130e01b70 v0.3.8-pre.006-fix.001 2026-09-03 16:28:13 +02:00
3a38914be7 v0.3.8-pre.006 2026-09-03 16:26:18 +02:00
62d6cf9dac v0.3.8-pre.005-fix.003 2026-09-03 14:21:47 +02:00
4e2647cce1 v0.3.8-pre.005-fix.002 2026-09-03 14:19:01 +02:00
9b92900dd7 v0.3.8-pre.005-fix.001 2026-09-03 13:45:12 +02:00
bf73481180 v0.3.8-pre.005 2026-09-03 13:09:25 +02:00
3bb73e81e4 v0.3.8-pre.004-fix.001 2026-09-03 12:33:16 +02:00
c4198ac656 v0.3.8-pre.004 2026-09-03 12:32:53 +02:00
009b957523 v0.3.8-pre.003-fix.001 2026-09-03 11:45:43 +02:00
9300dccbe2 v0.3.8-pre.003 2026-09-03 11:39:09 +02:00
74890a6268 v0.3.8-pre.002-fix.001 2026-09-03 11:31:41 +02:00
6a1892792c v0.3.8-pre.002 2026-09-03 10:44:49 +02:00
3df8fc74c1 v0.3.8-pre.001 2026-09-03 09:56:41 +02:00
538 changed files with 107971 additions and 989 deletions

View File

@@ -1,5 +1,5 @@
# file: .env.example # file: .env.example
# version: 13 # 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.
@@ -28,16 +28,16 @@ KSP_WALLETS_DIRECTORY=wallets
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. # 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. # 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 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. # Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent. # 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-beta.solana.com 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.

View File

@@ -1,8 +1,116 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 27 --> <!-- version: 35 -->
# Changelog KSP # Changelog KSP
## 0.3.15 — Raw Transaction Ingest Desk multi-route et fermeture live Mainnet — 2026-09-19
`0.3.15` livre `ksp-app-raw-transaction-ingest-desk` comme Desk Tauri KSP de composition et supervision des routes live `RawTransaction`. La Desk raisonne en routes complètes plutôt quen endpoints : une route nest composable que lorsque toutes ses capabilities Config/Transport requises existent sur le même réseau, puis le Start reconstruit les ressources backend et lance une instance `ksp-worker-raw-transaction-ingest-lib` indépendante par route sélectionnée. Plusieurs routes peuvent partager le même Store sans partager implicitement lifecycle, gaps, `TargetCoverage` ou réparation. Lapplication expose inventaire, Start/Stop ciblé, lifecycle, health, activité, backpressure, reconnect/replay, gaps/repair et compteurs sûrs sans déplacer Transport, Store ou logique dingestion dans le frontend.
Les cinq familles live du Worker sont composables selon les capacités réellement déclarées : Yellowstone hydraté, Standard Logs hydraté, Standard Block direct lorsquun `blockSubscribe` compatible existe, Helius Transaction hydraté et HTTP Block Polling. Sur Mainnet, Yellowstone/PublicNode est traité comme capability du réseau canonique `mainnet`, pas comme réseau séparé. Le chemin Yellowstone Block final utilise le stream gRPC comme déclencheur de bloc puis un HTTP `getBlock` `Full/Base64` avec `maxSupportedTransactionVersion = 1`, au lieu dun fan-out `getTransaction` par transaction. La queue dupdates Yellowstone reste bornée mais applique désormais une backpressure asynchrone vers le stream au lieu de transformer une saturation locale prolongée en `grpc_backpressure_overflow`; le Stop explicite accepte aussi comme fermeture coopérative un `Status` provider reçu après le half-close local, sans masquer un `grpc_status` sur session active.
Le live multi-provider a également révélé un cas de convergence RAW où deux acquisitions de la même transaction, du même slot et du même bloc différaient uniquement par `meta.logMessages`, PublicNode renvoyant un marqueur explicite `Log truncated`. Store journalise désormais des diagnostics bornés et sans matériau sensible, puis accepte le cas strictement prouvé **canonique complet déjà stocké + observation entrante tronquée compatible** comme observation supplémentaire sans remplacer le canonique ni arrêter le Worker. Cette exception reste volontairement asymétrique en `0.3.15` : un canonique tronqué nest pas encore promu automatiquement vers une version complète et toute autre divergence non prouvée conserve la sémantique `content_conflict`.
Le gate technique final `pre.016` passe format, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, les suites Store PostgreSQL, Transport, Worker, Raw Transaction Ingest Desk et le rejeu `cargo test --workspace --all-targets --all-features`, ainsi que les graphes Cargo et `cargo tree --duplicates`. `cargo tauri build` produit les trois bundles Linux `.deb`, `.rpm` et `.AppImage`. Le live de référence immédiatement antérieur fait tourner Yellowstone Mainnet et HTTP Block Polling environ dix-neuf minutes en parallèle sans `grpc_backpressure_overflow`, sans terminal `content_conflict` et sans route `Faulted`; les deux routes terminent ensuite `Stopped/Healthy`. La réconciliation documentaire `pre.017` repasse ensuite format, audits Rust/Markdown et `cargo check --workspace` sans réouvrir le runtime.
`prompts/035-V0_3_16_START_PROMPT.md` ouvre `0.3.16` exclusivement depuis le futur tag stable `v0.3.15`. Cette release est dédiée à la résilience RAW et à la gestion durable des conflits : variantes conservées, comparaison `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, promotions réversibles, quarantaine non terminale des conflits, historique de résolution, Store retry/backpressure, reconnexion Transport configurable et nouvelles vues de `ksp-app-store-desk`. `0.3.17` reste réservé au Backfill multi-route/multi-stratégie et `0.3.18` à ladaptation de `ksp-app-backfill-desk`.
## 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 dhydration, 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, navance jamais au travers dun 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` lorsquune hausse du gap Transport prouve que la rétention ne permet plus la reprise demandée. Aucun Backfill ou repair automatique nest 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` daudit/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 lacquisition 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 dun Job, ni dun backend Store physique, ni encore de `ksp-onchain-transport-lib`. Son identité stable est `raw_transaction_ingest`, ses settings bornent la capacité dadmission, 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 nest 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é. À lexpiration 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 derreur publics, les huit modules de production, les vingt-quatre exports crate-root, la redaction, le dependency firewall et labsence 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 larchitecture sur létat matériel : la fondation `0.3.11` na aucun adapter HTTP/WS/Yellowstone productif, aucune API publique denqueue/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, linjection Transport, lhydration, 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 `N1N4` 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 — 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. `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.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 444 # version: 637
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-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-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-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.3.7" version = "0.3.15"
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"
@@ -25,7 +25,7 @@ fs2 = { version = "^0.4" }
futures-util = { version = "^0.3", default-features = false } futures-util = { version = "^0.3", default-features = false }
getrandom = { version = "^0.4", default-features = false } getrandom = { version = "^0.4", default-features = false }
http = { version = "^1.5", default-features = false } http = { version = "^1.5", default-features = false }
jsonschema = { version = "^0.52", default-features = false } jsonschema = { version = "^0.53", default-features = false }
reqwest = { version = "^0.13", default-features = false } reqwest = { version = "^0.13", default-features = false }
rustls = { version = "^0.23", default-features = false } rustls = { version = "^0.23", default-features = false }
rustls-native-certs = { version = "^0.8", default-features = false } rustls-native-certs = { version = "^0.8", default-features = false }
@@ -49,7 +49,7 @@ tokio-tungstenite = { version = "^0.30", default-features = false }
tonic = { version = "^0.14", default-features = false } tonic = { version = "^0.14", default-features = false }
tonic-prost = { version = "^0.14", default-features = false } tonic-prost = { version = "^0.14", default-features = false }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
yellowstone-grpc-proto = { version = "^12.6", default-features = false } yellowstone-grpc-proto = { version = "^12.7", default-features = false }
zeroize = { version = "^1.9" } zeroize = { version = "^1.9" }
[workspace.lints.rust] [workspace.lints.rust]

View File

@@ -1,5 +1,5 @@
<!-- file: README.md --> <!-- file: README.md -->
<!-- version: 9 --> <!-- version: 15 -->
# Khadhroony Solana Project # Khadhroony Solana Project
@@ -53,7 +53,13 @@ La couche RAW dispose d'une façade Store backend-neutral et d'un premier job hi
`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-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.
Ces contrats restent distincts des futurs workers continus : un job borné n'est ni un service worker ni un pipeline générique imposé aux autres couches. `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 cinq familles productives couvrent Yellowstone, Standard WS `logsSubscribe`, Standard WS `blockSubscribe`, Helius `transactionSubscribe` et Solana HTTP live block polling. Les signaux transactionnels Yellowstone/Standard Logs/Helius utilisent l'hydration HTTP `getTransaction`, tandis que le mode Yellowstone Block utilisé par la route Mainnet du Desk emploie le bloc gRPC comme trigger puis réconcilie le slot par HTTP `getBlock` Full/Base64 avec concurrence bornée. Standard Block et HTTP Block Polling qualifient directement leurs transactions Legacy/V0/V1 vers Common RAW. Les queues restent bornées ; une saturation durable de la livraison Yellowstone applique désormais une backpressure asynchrone au stream gRPC au lieu de produire un overflow local. Le Store accepte aussi l'exception étroite déjà prouvée où un canonique complet reçoit ensuite une observation identique hors `logMessages` explicitement tronqués ; le canonique complet est conservé et les autres divergences restent fail-closed en `0.3.15`. 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.
`ksp-app-raw-transaction-ingest-desk` compose et supervise ces routes live sans déplacer leurs responsabilités dans l'UI. Les profils applicatifs sont réseau-centriques, plusieurs routes d'un même réseau peuvent partager un Store tout en gardant des Workers indépendants, et le monitoring latest-value expose lifecycle, health, activité, persistence, reconnect/replay, continuité, gaps et repair sans URL, secret, payload RAW ou handle physique. Le gate `0.3.15` valide notamment le fonctionnement parallèle Mainnet de Yellowstone Block + HTTP `getBlock` et HTTP Block Polling, la fermeture coopérative de Yellowstone après half-close et le build Tauri Linux de la Desk.
## Points d'entrée ## Points d'entrée

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 102 --> <!-- version: 114 -->
# Roadmap KSP # Roadmap KSP
@@ -40,7 +40,7 @@ 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
@@ -87,32 +87,42 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
``` ```
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program. - **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en N1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe. - 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.
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante. - À 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. - À 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
- [X] `0.3.1``ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN. - [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.
- [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. - [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.
- [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. - [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.
- [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. - [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.
- [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.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.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 nest exposé. - [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 nest exposé.
- [ ] `0.3.8` Introduire `ksp-app-store-desk` V1 sur le **gabarit KSP courant** (shell/splash/styles/assets/logging et dépendances npm de base des Desk KSP), avec DataTables selon le pattern déjà utilisé par Config/Wallet Desk. La V1 reste backend-agnostique et consulte le Store uniquement via `ksp-store-lib` + Config : health/runtime sûr, tableaux `RawTransaction` et `RawAccountState`, observations lorsque la surface de listing backend-neutral est disponible/ajoutée proprement, rétention/tombstones en lecture et pagination Store opaque distincte du filtrage/paging local DataTables. kbot3 sert uniquement de référence fonctionnelle/UX des anciens tableaux RAW, jamais de gabarit, source de code, SQL, DTO, commandes ou versions npm. Lapplication évoluera ensuite avec STRUCTURAL, DECODED, processing/materialization et DOMAIN réellement persistés. - [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.
- [ ] `0.3.9` Introduire `ksp-worker-api` comme API générique de lifecycle/health/progression pour services continus, en reprenant le pattern latest-value stabili par Job tout en gardant les sémantiques Worker distinctes des jobs terminables et des wake-ups Store post-commit. - [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.
- [ ] `0.3.10`Introduire `ksp-worker-raw-transaction-ingest-lib` pour l'acquisition continue de `RawTransaction` via les surfaces live de `ksp-onchain-transport-lib`, persistance atomique par `ksp-store-lib`, reprise/backpressure/idempotence et notifications `ksp-worker-api`, sans decode Program ni dépendance backend/provider directe. - [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`.
- [ ] `0.3.11`Introduire `ksp-app-raw-transaction-ingest-desk`, application Tauri spécialisée de contrôle et monitoring du worker live : lifecycle, health, rates, backpressure, reconnect/recovery et compteurs sûrs ; la consultation détaillée des données persistées reste la responsabilité de `ksp-app-store-desk`. - [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 dun 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 nest 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.
- [X] `0.3.15``ksp-app-raw-transaction-ingest-desk` livrée comme Desk Tauri KSP multi-route : composition capability-driven par réseau, une instance Worker indépendante par route sélectionnée, Store partagé, Start/Stop ciblé et supervision lifecycle/health/backpressure/reconnect/replay/gaps/repair sans réimplémenter Transport ni persistence. Les cinq familles live sont projetées selon les capacités réellement configurées ; Yellowstone Mainnet utilise le trigger gRPC Block + HTTP `getBlock` et applique une backpressure bornée sans overflow local prolongé. Le Stop Yellowstone post-half-close est coopératif, et Store accepte le cas strict `canonique complet + incoming logMessages explicitement tronqué compatible` sans remplacer le canonique ni arrêter le Worker. Gate workspace/Clippy/tests/graphes/bundles Linux vert et live Mainnet Yellowstone + HTTP polling denviron 19 minutes fermé `Stopped/Healthy`. La généralisation variantes/conflits/promotions/retry/reconnect reste explicitement `0.3.16`.
- [ ] `0.3.16`**RAW resilience / conflict management** : faire converger Store API/façade/PostgreSQL, Worker live et Store Desk autour de variantes RAW durables, résolution automatique `Exact / CompatibleLessComplete / CompatibleMoreComplete / Conflict`, quarantaine des conflits non résolus sans arrêt du Worker, historique réversible des promotions/résolutions, retry Store sans perte silencieuse et reconnexion Transport configurable/bornée. Une provenance provider n'est jamais une priorité canonique en soi ; la représentation prouvée la plus complète gagne.
- [ ] `0.3.17` — Étendre `ksp-job-backfill-lib` au **backfill multi-route/multi-stratégie** à partir de la matrice d'acquisition auditée en `0.3.9` et de la convergence Store `0.3.16`. Conserver `getSignaturesForAddress + getTransaction` comme première voie valide puis ajouter les stratégies historiques/catch-up réellement pertinentes sans edge Job ↔ Worker.
- [ ] `0.3.18` — Adapter `ksp-app-backfill-desk` au Job multi-route `0.3.17` : inventaire/composition de routes, sélection/supervision et progression sur le modèle structurel de `ksp-app-raw-transaction-ingest-desk`, sans réimplémenter la logique du Job.
### TODO/IDEAS — applications spécialisées et control plane ### TODO/IDEAS — applications spécialisées et control plane
- [ ] **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`. - [ ] **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`.
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'un niveau N3/D3 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. - [ ] **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.
### TODO/IDEAS — taxonomie N1, processing et rétention ### TODO/IDEAS — taxonomie D1, processing et rétention
- [ ] **TODO** — maintenir la matrice dadmission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte. - [ ] **TODO** — maintenir la matrice dadmission 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` résilience RAW / `0.3.17``0.3.18` Backfill multi-route.
- [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 na été nécessaire. Le payload riche reste Transport-owned et la voie Worker hydrate via `getTransaction observed` tant que le RAW complet nest 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. - [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 dexécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsquun consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup. - [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif dexécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsquun 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 dun 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-*`. - [ ] **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 dun 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-*`.
@@ -125,13 +135,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant quaucune représentation compactée réelle nexiste. - [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant quaucune représentation compactée réelle nexiste.
- [ ] **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 quun RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée quavec un besoin réel. - [ ] **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 quun RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée quavec 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. - [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 louverture de N2/N3 ; conserver lisolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique. - [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de louverture de D2/D3 ; conserver lisolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
## Série STRUCTURAL suivante ## Série STRUCTURAL suivante
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables. - [ ] 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. - [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
- [ ] Vérifier avant extension si d'autres familles N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention. - [ ] 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 replay/backfill RAW -> STRUCTURAL avec processing versionné.
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret. - [ ] 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. - [ ] 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.

View File

@@ -0,0 +1,91 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_devnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "orbitflare_devnet"
},
{
"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_public"
},
{
"component_id": "transport.helius",
"file_id": "cfg.std.transport",
"profile_id": "helius_mainnet"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "publicnode_mainnet"
},
{
"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": "transport",
"file_id": "cfg.std.transport",
"profile_id": "testnet_public"
},
{
"component_id": "transport.yellowstone",
"file_id": "cfg.std.transport",
"profile_id": "publicnode_testnet"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "testnet"
}
]
}
]
}

View 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"
}
]
}
]
}

View File

@@ -29,7 +29,7 @@
}, },
{ {
"profile_id": "mainnet", "profile_id": "mainnet",
"network": "mainnet-beta", "network": "mainnet",
"backend": "postgres", "backend": "postgres",
"postgres": { "postgres": {
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",

View File

@@ -31,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,
@@ -57,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,
@@ -85,21 +85,39 @@
"name": "mainnet_public_ws", "name": "mainnet_public_ws",
"enabled": true, "enabled": true,
"provider": "solana-public", "provider": "solana-public",
"cluster": "mainnet-beta", "cluster": "mainnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot"
]
}, },
{ {
"name": "mainnet_helius_ws", "name": "mainnet_helius_ws",
"enabled": true, "enabled": true,
"provider": "helius", "provider": "helius",
"cluster": "mainnet-beta", "cluster": "mainnet",
"kind": "helius_laserstream", "kind": "helius_laserstream",
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}", "url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
"session": { "session": {
"notification_queue_capacity": 512, "notification_queue_capacity": 512,
"max_active_subscriptions": 2048 "max_active_subscriptions": 2048
} },
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
} }
] ]
}, },
@@ -140,7 +158,17 @@
"provider": "helius", "provider": "helius",
"cluster": "devnet", "cluster": "devnet",
"kind": "helius_laserstream", "kind": "helius_laserstream",
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}" "url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
} }
] ]
} }

View File

@@ -348,6 +348,26 @@
}, },
"session": { "session": {
"$ref": "#/$defs/wsSessionOverride" "$ref": "#/$defs/wsSessionOverride"
},
"capabilities": {
"type": "array",
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
"items": {
"enum": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"vote",
"helius_transaction"
]
}
} }
} }
}, },

View File

@@ -29,7 +29,7 @@
}, },
{ {
"profile_id": "mainnet", "profile_id": "mainnet",
"network": "mainnet-beta", "network": "mainnet",
"backend": "postgres", "backend": "postgres",
"postgres": { "postgres": {
"connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}", "connection_uri": "${KSP_SECRET_STORE_MAINNET_POSTGRES_URI:-postgresql://localhost/ksp_mainnet}",

View File

@@ -75,7 +75,67 @@
"provider": "solana-public", "provider": "solana-public",
"cluster": "devnet", "cluster": "devnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}" "url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{
"profile_id": "helius_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": "helius_devnet_ws",
"enabled": true,
"provider": "helius",
"cluster": "devnet",
"kind": "helius_laserstream",
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
} }
] ]
}, },
@@ -116,7 +176,16 @@
"provider": "solana-public", "provider": "solana-public",
"cluster": "devnet", "cluster": "devnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}" "url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
} }
], ],
"grpc_endpoints": [ "grpc_endpoints": [
@@ -144,8 +213,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,
@@ -172,9 +241,69 @@
"name": "solana_mainnet_public_ws", "name": "solana_mainnet_public_ws",
"enabled": true, "enabled": true,
"provider": "solana-public", "provider": "solana-public",
"cluster": "mainnet-beta", "cluster": "mainnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{
"profile_id": "helius_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": "helius_mainnet_ws",
"enabled": true,
"provider": "helius",
"cluster": "mainnet",
"kind": "helius_laserstream",
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY}",
"capabilities": [
"account",
"logs",
"program",
"root",
"signature",
"slot",
"slots_updates",
"helius_transaction"
]
} }
] ]
}, },
@@ -185,8 +314,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,
@@ -225,7 +354,7 @@
"name": "publicnode_solana_mainnet_http", "name": "publicnode_solana_mainnet_http",
"enabled": true, "enabled": true,
"provider": "publicnode", "provider": "publicnode",
"cluster": "mainnet-beta", "cluster": "mainnet",
"url": "https://solana-rpc.publicnode.com", "url": "https://solana-rpc.publicnode.com",
"connect_timeout_ms": 5000, "connect_timeout_ms": 5000,
"request_timeout_ms": 15000, "request_timeout_ms": 15000,
@@ -267,9 +396,18 @@
"name": "solana_mainnet_public_ws", "name": "solana_mainnet_public_ws",
"enabled": true, "enabled": true,
"provider": "solana-public", "provider": "solana-public",
"cluster": "mainnet-beta", "cluster": "mainnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
} }
] ]
}, },
@@ -277,11 +415,35 @@
"profile_id": "publicnode_mainnet", "profile_id": "publicnode_mainnet",
"endpoints": [ "endpoints": [
{ {
"name": "solana_mainnet_public", "name": "publicnode_solana_mainnet_rpc",
"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": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "solana_mainnet_public_repair",
"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,
@@ -308,9 +470,18 @@
"name": "solana_mainnet_public_ws", "name": "solana_mainnet_public_ws",
"enabled": true, "enabled": true,
"provider": "solana-public", "provider": "solana-public",
"cluster": "mainnet-beta", "cluster": "mainnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet.solana.com}",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
} }
], ],
"grpc_endpoints": [ "grpc_endpoints": [
@@ -318,7 +489,7 @@
"name": "publicnode_solana_mainnet_yellowstone", "name": "publicnode_solana_mainnet_yellowstone",
"enabled": true, "enabled": true,
"provider": "publicnode", "provider": "publicnode",
"cluster": "mainnet-beta", "cluster": "mainnet",
"protocol": "solana_yellowstone", "protocol": "solana_yellowstone",
"url": "https://solana-yellowstone-grpc.publicnode.com:443", "url": "https://solana-yellowstone-grpc.publicnode.com:443",
"secret_metadata": [ "secret_metadata": [
@@ -330,6 +501,56 @@
} }
] ]
}, },
{
"profile_id": "testnet_public",
"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",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
}
]
},
{ {
"profile_id": "publicnode_testnet", "profile_id": "publicnode_testnet",
"endpoints": [ "endpoints": [
@@ -367,7 +588,16 @@
"provider": "solana-public", "provider": "solana-public",
"cluster": "testnet", "cluster": "testnet",
"kind": "solana_standard", "kind": "solana_standard",
"url": "wss://api.testnet.solana.com" "url": "wss://api.testnet.solana.com",
"capabilities": [
"account",
"block",
"logs",
"program",
"root",
"signature",
"slot"
]
} }
], ],
"grpc_endpoints": [ "grpc_endpoints": [

View File

@@ -53,19 +53,21 @@
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-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/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json", "../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json", "../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json" "../../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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs // file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 16 // version: 18
//! Structural desktop contract checks for the Backfill Desk scaffold. //! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -145,10 +145,12 @@ fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => return, std::option::Option::None => return,
}; };
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 16);
for required in [ for required in [
"../../config/composite.ksp-app-backfill-desk.json", "../../config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-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-wallet-desk.json", "../../config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json", "../../config/std.logging.json",
"../../config/std.offchain_transport.json", "../../config/std.offchain_transport.json",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs // file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
// version: 3 // version: 4
fn ready_options() -> crate::BackfillDeskOptionsDto { fn ready_options() -> crate::BackfillDeskOptionsDto {
let limits = crate::backfill_request_limits(); let limits = crate::backfill_request_limits();
@@ -22,7 +22,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
return crate::BackfillDeskOptionsDto { return crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(), commitments: crate::backfill_commitment_codes(),
composition_ready: true, composition_ready: true,
configured_networks: vec!["mainnet-beta".to_owned()], configured_networks: vec!["mainnet".to_owned()],
http_routes: vec![ http_routes: vec![
crate::BackfillHttpRouteOptionDto { crate::BackfillHttpRouteOptionDto {
pooled: true, pooled: true,
@@ -36,7 +36,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
program_id_options: crate::program_id_autocomplete_options(), program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(), scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None, store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("mainnet-beta".to_owned()), store_network: std::option::Option::Some("mainnet".to_owned()),
store_ready: true, store_ready: true,
transport_diagnostic: std::option::Option::None, transport_diagnostic: std::option::Option::None,
transport_ready: true, transport_ready: true,
@@ -81,7 +81,7 @@ fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(mapped.network().as_str(), "mainnet-beta"); assert_eq!(mapped.network().as_str(), "mainnet");
assert_eq!(mapped.role().as_str(), "backfill_pool"); assert_eq!(mapped.role().as_str(), "backfill_pool");
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized); assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress); assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
@@ -193,7 +193,7 @@ fn request_preview_never_contains_address_or_signature_values() {
if let std::result::Result::Ok(serialized) = serialized { if let std::result::Result::Ok(serialized) = serialized {
assert!(!serialized.contains("11111111111111111111111111111111")); assert!(!serialized.contains("11111111111111111111111111111111"));
assert!(!serialized.contains(&"1".repeat(64))); assert!(!serialized.contains(&"1".repeat(64)));
assert!(serialized.contains("mainnet-beta")); assert!(serialized.contains("mainnet"));
assert!(serialized.contains("backfill_pool")); assert!(serialized.contains("backfill_pool"));
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs // file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
// version: 3 // version: 4
#[test] #[test]
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() { fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
@@ -30,7 +30,7 @@ fn cancellable_runtime(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None, std::result::Result::Err(_) => return std::option::Option::None,
}; };
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") { let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None, std::result::Result::Err(_) => return std::option::Option::None,
}; };

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs // file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
// version: 1 // version: 2
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> { 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)) { let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
@@ -14,7 +14,7 @@ fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ks
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None, std::result::Result::Err(_) => return std::option::Option::None,
}; };
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") { let network = match ksp_store_lib::RawNetworkId::new("mainnet") {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None, std::result::Result::Err(_) => return std::option::Option::None,
}; };

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs // file: crates/ksp-app-backfill-desk/unit_tests/store_runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn store_health_mapping_is_explicit_and_conservative() { fn store_health_mapping_is_explicit_and_conservative() {
@@ -10,7 +10,7 @@ fn store_health_mapping_is_explicit_and_conservative() {
#[test] #[test]
fn store_transport_network_coherence_requires_exact_logical_identity() { fn store_transport_network_coherence_requires_exact_logical_identity() {
assert!(super::validate_network_coherence("devnet", "devnet").is_ok()); assert!(super::validate_network_coherence("devnet", "devnet").is_ok());
let mismatch = super::validate_network_coherence("mainnet-beta", "devnet"); let mismatch = super::validate_network_coherence("mainnet", "devnet");
assert!(mismatch.is_err()); assert!(mismatch.is_err());
if let std::result::Result::Err(error) = mismatch { if let std::result::Result::Err(error) = mismatch {
assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH); assert_eq!(error.code(), crate::ERROR_CODE_STORE_NETWORK_MISMATCH);

View File

@@ -53,19 +53,21 @@
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-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/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json", "../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json", "../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json" "../../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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs // file: crates/ksp-app-config-desk/tests/desktop_contract.rs
// version: 12 // version: 14
//! Desktop build/shell contract audits for Config Desk. //! Desktop build/shell contract audits for Config Desk.
@@ -110,15 +110,23 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object); let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some(), "packaged Config resources map must exist"); assert!(resources.is_some(), "packaged Config resources map must exist");
if let std::option::Option::Some(resources) = resources { if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 16);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str), resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"), std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
); );
assert_eq!(
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str), resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"), std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
); );
assert_eq!(
resources.get("../../config/composite.ksp-app-store-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-store-desk.json"),
);
assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json")); assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
assert_eq!( assert_eq!(
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str), resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs // file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 8 // version: 9
#[test] #[test]
fn profile_inventory_exposes_registered_profile_documents() { fn profile_inventory_exposes_registered_profile_documents() {
@@ -21,6 +21,9 @@ fn profile_inventory_exposes_registered_profile_documents() {
assert!(inventory.iter().any(|document| -> bool { assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET; return document.file_id == ksp_config_lib::FILE_ID_STD_WALLET;
})); }));
assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK;
}));
assert!(inventory.iter().any(|document| -> bool { assert!(inventory.iter().any(|document| -> bool {
return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK; return document.file_id == ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
})); }));

View File

@@ -0,0 +1,44 @@
# file: crates/ksp-app-raw-transaction-ingest-desk/Cargo.toml
# version: 4
[package]
name = "ksp-app-raw-transaction-ingest-desk"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lib]
name = "ksp_app_raw_transaction_ingest_desk_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "ksp-app-raw-transaction-ingest-desk"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-store-lib = { path = "../ksp-store-lib" }
ksp-worker-api = { path = "../ksp-worker-api" }
ksp-worker-raw-transaction-ingest-lib = { path = "../ksp-worker-raw-transaction-ingest-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

View File

@@ -0,0 +1,76 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 15 -->
# `ksp-app-raw-transaction-ingest-desk`
Application desktop Tauri KSP destinée à composer et superviser les routes live alimentant le Store commun en `RawTransaction`.
Le composant conserve les frontières KSP : Config possède les profils/endpoints/secrets, Transport les clients et sessions, Store la persistance et le Worker la logique d'ingestion. Le shell desktop ne déplace jamais ces responsabilités vers le frontend.
## Package
```text
package : ksp-app-raw-transaction-ingest-desk
lib : ksp_app_raw_transaction_ingest_desk_lib
bin : ksp-app-raw-transaction-ingest-desk
```
## Frontières
```text
Config -> composite, profils, secrets et résolution effective
Transport -> capabilities HTTP/WS/gRPC et ressources physiques
Desk -> catalogue logique, revalidation Start et ownership du lifecycle mono/multi-route
Store -> persistance via la façade KSP
Worker -> validation des contrats source, acquisition continue et lifecycle d'exécution
```
L'inventaire de routes est calculé côté Rust à partir du composite Config validé et des settings Transport/Store résolus. Il ne lance aucun probe réseau et n'ouvre ni Store ni Worker.
Au Start, le backend revalide une sélection logique par `profile_id + route_id + inventory_generation + commitment`. Il recharge Config, reproouve le réseau Store/Transport, reconstruit le pool HTTP ou l'endpoint WS/gRPC exact requis, ouvre le Store via `ksp-store-lib`, exige un health `Ready` puis lance le Worker exact via `RawTransactionIngestWorker::start_with_runtime_resources`. Les ressources physiques et handles restent exclusivement côté Rust.
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
Les races de contrôle sont fermées côté backend : un Stop qui rencontre une route encore `Starting` marque cette réservation pour arrêt coopératif et attend son cleanup au lieu de la considérer inactive. Dès qu'une fermeture de l'application commence, tout nouveau Start est refusé, les routes actives reçoivent Stop et les Starts déjà réservés sont marqués avant activation. La fenêtre principale ne quitte le processus qu'après cleanup borné des Workers et du Store partagé, ou avec un code d'échec après expiration de la borne.
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
Une route explicitement liée à un provider nest projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais nest pas présentée sur Testnet tant quHelius ne fournit pas de source Testnet configurée.
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
Les requêtes IPC Start/Stop sont shape-strict : les champs inconnus sont rejetés et `profile_id` est borné/validé avant toute reconstruction. Le bridge de logging frontend applique également des bornes strictes et refuse les caractères de contrôle avant émission dans la façade Logging.
## Routes V1
```text
yellowstone-hydrated
standard-logs-hydrated
standard-block-direct
helius-transaction-hydrated
http-block-polling
```
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont coalescées côté backend à une cadence bornée de `500 ms` maximum par route, émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker. Les snapshots steady-state patchent les champs de carte en place ; la reconstruction des contrôles Start/Stop est réservée aux changements de lifecycle, afin de préserver la réactivité des interactions.
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Les slots gRPC et hydrations `getBlock` restent bornés ; lorsqu'un consumer est saturé, la queue Transport attend de la capacité et propage la backpressure au stream au lieu de produire un `grpc_backpressure_overflow` local. Après un Stop explicite, un status provider reçu pendant le half-close ne remplace plus la fermeture coopérative ; un status observé pendant une session active reste soumis au reconnect/failure normal. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.
La convergence Store de `0.3.15` conserve également une exception volontairement étroite : si un canonique complet existe déjà et qu'une route apporte le même RAW hors `logMessages` avec un marqueur exact `Log truncated` après un préfixe identique, l'observation moins complète est acceptée sans remplacer le canonique ni fault la route. Le sens inverse et toute divergence non prouvée restent des conflits terminaux dans cette version ; la gestion durable de variantes/résolutions appartient à `0.3.16`.
## Ports de développement
```text
Vite HTTP : 1440
Vite WS : 1441
```
Lancement :
```bash
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Le cycle frontend est déclenché par Tauri via ses hooks configurés ; les scripts npm applicatifs ne sont pas lancés directement par l'opérateur.
Voir [`USAGE.md`](USAGE.md) et [`../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md`](../../docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md).

View File

@@ -0,0 +1,84 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 14 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
## 1. Lancement
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Tauri déclenche lui-même les hooks frontend configurés. Les scripts npm applicatifs de développement, contrôle ou build ne sont pas lancés directement par l'opérateur.
Le runtime prépare les ressources Config nécessaires, charge le composite dédié, initialise Logging puis ouvre le shell desktop.
## 2. Vue Routes
La vue Routes charge un inventaire déterministe construit côté Rust depuis le composite Config validé. Le sélecteur présente les réseaux logiques `devnet`, `mainnet` et `testnet`, jamais les providers. Chaque réseau agrège les profils Transport same-network configurés pour ses capabilities. La vue projette, pour chaque route V1 :
```text
route_id
famille et label sûrs
réseau logique lorsque résolu
état Config
selectable oui/non
raison sûre lorsque la route est indisponible
génération d'inventaire
```
`Configured` signifie uniquement que toutes les requirements Config nécessaires à la route sont cohérentes. Les profils `solana-public` standard committés déclarent explicitement les capabilities `Logs` et `Block`; `standard-block-direct` est donc Configured sur les trois réseaux logiques tant que ce socle Config reste valide. L'inventaire seul ne fait aucun probe réseau et n'ouvre aucun Store ni Worker.
Un refresh reconstruit l'inventaire depuis Config et publie une nouvelle génération. La sélection visuelle d'un réseau ne choisit aucune URL, provider ou ressource physique et ne démarre aucune acquisition. Une capability optionnelle dont le secret n'est pas résolu rend uniquement la route qui en dépend indisponible ; les routes satisfaites par le Transport de base du même réseau restent composables.
Une route provider-specific dont aucune source nest déclarée pour le réseau nest pas une erreur de configuration : elle est hors inventaire pour ce réseau. Par exemple, Helius Transaction est projetée sur Devnet/Mainnet lorsque `transport.helius` existe, mais pas sur Testnet lorsquaucune source Helius Testnet nest déclarée.
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
Les commandes Start/Stop refusent les champs IPC inconnus et les identifiants logiques non bornés ou contenant des caractères de contrôle. Le logging frontend est lui aussi borné avant l'IPC ; une entrée hostile ne doit jamais être recopiée dans les diagnostics backend.
### Start/Stop multi-route
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
Un Stop peut être demandé même si le Start ciblé n'a pas encore fini d'ouvrir le Store ou d'installer le Worker : la réservation est alors marquée pour arrêt et le backend attend sa résolution. Lors de la fermeture de la fenêtre principale, le Desk ferme d'abord l'admission Start, demande l'arrêt de toutes les routes actives ou encore en démarrage et attend un cleanup borné avant de quitter. Il n'est donc pas nécessaire d'arrêter manuellement chaque route avant de fermer l'application.
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La livraison gRPC et les hydrations restent bornées : une saturation locale applique de la backpressure au stream au lieu de dropper les updates ou de fault sur `grpc_backpressure_overflow`. Lors d'un Stop ciblé, un status distant reçu après le half-close local est une fermeture coopérative ; une erreur gRPC observée avant Stop reste une panne source/reconnect normale. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
Lorsque plusieurs routes observent la même transaction, l'entité canonique reste unique et les provenances restent séparées. Le cas `canonique complet + entrant explicitement Log truncated compatible` est accepté sans remplacer le canonique ni passer la route en `Faulted`. Les autres divergences de contenu restent fail-closed en `0.3.15`; les variantes conflictuelles durables et leur résolution via Store Desk sont prévues pour `0.3.16`.
## 3. Raisons d'indisponibilité
Les reasons exposées sont des codes applicatifs bornés, par exemple :
```text
profile_unresolved
network_mismatch
missing_http_get_block
missing_http_get_transaction
missing_http_block_scan
missing_ws_logs_capability
missing_ws_block_capability
missing_helius_transaction_capability
missing_yellowstone_grpc
missing_required_secret
```
Le texte arbitraire d'une erreur provider ou d'un secret Config n'est jamais projeté dans l'inventaire.
## 4. Diagnostics
La vue Diagnostics expose uniquement des informations techniques sûres nécessaires à l'exploitation du Desk, notamment l'identité de version, l'état du shell, la composition Config bootstrap, l'état Logging et la génération d'inventaire.
Les diagnostics ne doivent pas exposer de credentials, d'URI Store, d'URL d'endpoint, de source key, de handle runtime ou de payload `RawTransaction`.
## 5. Runtime packagé
Le build Tauri embarque les documents Config et schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable sans embarquer `.env`.
Les fichiers Config et les secrets restent administrés par les mécanismes Config KSP ; l'interface ne contourne pas cette ownership.

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/fonts/README.md -->
<!-- version: 1 -->
# Fonts
`DOS_Amazigh.ttf` est repris byte-exact du gabarit Desk KSP existant pour préserver l'identité visuelle commune.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

View File

@@ -0,0 +1,231 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/frontend/main.html -->
<!-- version: 7 -->
<!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>Raw Transaction Ingest Desk — Routes</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">Raw Transaction Ingest 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">Routes</span>
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<span id="headerProfile" class="badge text-bg-light border text-dark"></span>
<span id="headerStore" class="badge text-bg-secondary">Store idle</span>
<span id="headerPhase" class="badge text-bg-secondary">inventory</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 Raw Transaction Ingest Desk">
<button class="nav-link text-start active" type="button" data-view="routes">
<i class="fa-solid fa-route me-2" aria-hidden="true"></i>Routes
</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="routes">
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Routes live</h1>
<p class="text-body-secondary mb-0">Inventaire Config et runtime multi-route avec un Worker indépendant par route et Store partagé same-network.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<button id="refreshRouteMonitoring" class="btn btn-outline-secondary btn-sm" type="button">
<i class="fa-solid fa-wave-square me-2" aria-hidden="true"></i>Resync monitoring
</button>
<button id="refreshRouteInventory" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh inventory
</button>
</div>
</div>
<div class="row g-3 align-items-end mb-4">
<div class="col-12 col-md-4">
<label class="form-label" for="routeProfile">Réseau logique</label>
<select id="routeProfile" class="form-select" aria-label="Réseau Raw Transaction Ingest Desk"></select>
</div>
<div class="col-12 col-md-3">
<label class="form-label" for="routeCommitment">Commitment</label>
<select id="routeCommitment" class="form-select" aria-label="Commitment Raw Transaction Ingest Desk">
<option value="confirmed" selected>confirmed</option>
<option value="finalized">finalized</option>
</select>
</div>
<div class="col-6 col-md-2">
<div class="small text-body-secondary">Réseau</div>
<div id="routeNetwork" class="fw-semibold app-route-code"></div>
</div>
<div class="col-6 col-md-3">
<div class="small text-body-secondary">Génération</div>
<div id="routeInventoryGeneration" class="fw-semibold app-route-code"></div>
</div>
</div>
<div class="alert alert-info" role="status">
<strong>Configured</strong> signifie que Config prouve la composabilité. Start revalide Config puis lance un Worker indépendant. Les routes du même réseau partagent le Store ; chaque fault reste isolé à sa route.
</div>
<div id="routeCommandFeedback" class="alert alert-info" role="status" aria-live="polite" hidden></div>
<div class="card border-primary-subtle mb-4">
<div class="card-body d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="small text-body-secondary">Runtime multi-route / Store partagé</div>
<div class="d-flex flex-wrap gap-2 mt-1">
<span id="routeRuntimeState" class="badge text-bg-secondary">idle</span>
<span id="routeRuntimeIdentity" class="app-route-code small text-body-secondary">aucun Worker actif</span>
</div>
</div>
</div>
</div>
<div class="small text-body-secondary mb-3">Réseau affiché : <span id="routeSelectedProfile" class="app-route-code"></span></div>
<div id="routeFoundation" class="row g-3 app-route-grid" aria-live="polite"></div>
<section id="routeMonitoringDetail" class="card shadow-sm mt-4 app-monitoring-detail" aria-live="polite" hidden>
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="fw-semibold">Supervision route</div>
<div id="monitorRouteId" class="small text-body-secondary app-route-code"></div>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<span id="monitorLifecycle" class="badge text-bg-secondary"></span>
<span id="monitorHealthBadge" class="badge text-bg-secondary"></span>
<span id="monitorActivityBadge" class="badge text-bg-light border text-dark"></span>
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Network</div><div id="monitorNetwork" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Commitment</div><div id="monitorCommitment" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Sequence</div><div id="monitorSequence" class="app-route-code fw-semibold"></div></div>
<div class="col-6 col-lg-3"><div class="small text-body-secondary">Fault</div><div id="monitorFault" class="app-route-code fw-semibold"></div></div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Pipeline / persistence</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Health</dt><dd id="monitorHealth" class="col-5"></dd>
<dt class="col-7">Activity</dt><dd id="monitorActivity" class="col-5"></dd>
<dt class="col-7">Admission queue</dt><dd id="monitorAdmissionQueue" class="col-5"></dd>
<dt class="col-7">Persistence in-flight</dt><dd id="monitorPersistence" class="col-5"></dd>
<dt class="col-7">Admitted</dt><dd id="monitorAdmittedTotal" class="col-5"></dd>
<dt class="col-7">Canonicalized</dt><dd id="monitorCanonicalizedTotal" class="col-5"></dd>
<dt class="col-7">Persisted</dt><dd id="monitorPersistedTotal" class="col-5"></dd>
<dt class="col-7">RAW inserted</dt><dd id="monitorEntityInsertedTotal" class="col-5"></dd>
<dt class="col-7">RAW already present</dt><dd id="monitorEntityAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">RAW purged skipped</dt><dd id="monitorEntitySkippedPurgedTotal" class="col-5"></dd>
<dt class="col-7">Observations inserted</dt><dd id="monitorObservationInsertedTotal" class="col-5"></dd>
<dt class="col-7">Observations already present</dt><dd id="monitorObservationAlreadyPresentTotal" class="col-5"></dd>
<dt class="col-7">Content conflicts</dt><dd id="monitorContentConflictTotal" class="col-5"></dd>
<dt class="col-7">Store failures</dt><dd id="monitorStoreFailureTotal" class="col-5"></dd>
<dt class="col-7">Source failures</dt><dd id="monitorSourceFailureTotal" class="col-5"></dd>
<dt class="col-7">Backpressure waits</dt><dd id="monitorBackpressureWaitTotal" class="col-5"></dd>
<dt class="col-7">Hydration pending</dt><dd id="monitorHydrationPending" class="col-5"></dd>
<dt class="col-7">Processing frontier</dt><dd id="monitorProcessingFrontier" class="col-5"></dd>
<dt class="col-7">Oldest pending slot</dt><dd id="monitorOldestPending" class="col-5"></dd>
</dl>
</div>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card h-100 border-light-subtle">
<div class="card-header fw-semibold">Sources / continuité / repair</div>
<div class="card-body">
<dl class="row mb-0 app-monitoring-list">
<dt class="col-7">Source state</dt><dd id="monitorSourceState" class="col-5"></dd>
<dt class="col-7">Source counts</dt><dd id="monitorSourceCounts" class="col-5"></dd>
<dt class="col-7">Reconnects</dt><dd id="monitorSourceReconnectTotal" class="col-5"></dd>
<dt class="col-7">Replay attempts</dt><dd id="monitorSourceReplayAttemptTotal" class="col-5"></dd>
<dt class="col-7">Continuity gaps source</dt><dd id="monitorSourceContinuityGapTotal" class="col-5"></dd>
<dt class="col-7">Policy observed</dt><dd id="monitorContinuityPolicyObserved" class="col-5"></dd>
<dt class="col-7">Continuity frontier</dt><dd id="monitorContinuityFrontier" class="col-5"></dd>
<dt class="col-7">Open gaps</dt><dd id="monitorContinuityHasOpenGaps" class="col-5"></dd>
<dt class="col-7">Failed losses reconciled</dt><dd id="monitorFailedSourceLossesReconciled" class="col-5"></dd>
<dt class="col-7">Future coverage</dt><dd id="monitorFutureTargetCoverage" class="col-5"></dd>
<dt class="col-7">Open / repairing</dt><dd class="col-5"><span id="monitorOpenGapCount"></span> / <span id="monitorRepairingGapCount"></span></dd>
<dt class="col-7">Repaired / unresolved</dt><dd class="col-5"><span id="monitorRepairedGapTotal"></span> / <span id="monitorUnresolvedGapTotal"></span></dd>
<dt class="col-7">Oldest open gap</dt><dd id="monitorOldestOpenGap" class="col-5"></dd>
<dt class="col-7">Replay repair</dt><dd id="monitorReplayRepairTotal" class="col-5"></dd>
<dt class="col-7">Redundant coverage repair</dt><dd id="monitorRedundantCoverageRepairTotal" class="col-5"></dd>
<dt class="col-7">HTTP scan repair</dt><dd id="monitorHttpScanRepairTotal" class="col-5"></dd>
<dt class="col-7">Block fetch repair</dt><dd id="monitorRepairBlockFetchTotal" class="col-5"></dd>
<dt class="col-7">Transaction hydration repair</dt><dd id="monitorRepairTransactionHydrationTotal" class="col-5"></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="mt-3">
<div class="fw-semibold mb-2">Gaps courants / récents</div>
<div id="monitorGapList" class="list-group app-monitoring-gaps"></div>
</div>
</div>
</section>
</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 shell Config, Logging et de la génération d'inventaire.</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 composite bootstrap</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Inventaire default</dt><dd id="runtimeInventoryDefaultProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Génération inventaire</dt><dd id="runtimeInventoryGeneration" 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>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,167 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/sass/_app.scss
// version: 3
$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 {
width: 100%;
max-width: 1320px;
min-width: 0;
}
.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-runtime-list dd,
.app-route-code {
font-family: var(--bs-font-monospace);
}
.app-route-grid {
margin-right: 0;
margin-left: 0;
}
.app-route-grid > * {
min-width: 0;
}
.app-route-card {
min-width: 0;
max-width: 100%;
overflow: hidden;
border-left: 4px solid rgba($primary, 0.55);
}
.app-route-code {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-route-monitoring-summary {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.app-monitoring-detail {
min-width: 0;
overflow: hidden;
}
.app-monitoring-list dt,
.app-monitoring-list dd {
margin-bottom: 0.45rem;
}
.app-monitoring-list dd,
.app-monitoring-gaps,
.app-monitoring-detail .app-route-code {
font-family: var(--bs-font-monospace);
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-monitoring-gaps .list-group-item {
min-width: 0;
}
.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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,137 @@
// file: crates/ksp-app-raw-transaction-ingest-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);
}
}

View File

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

View File

@@ -0,0 +1,116 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
// version: 2
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_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 FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024;
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(" ");
}
function boundMessage(message: string): string {
return message.length <= FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS ? message : `${message.slice(0, FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS)}`;
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
const payload: FrontendLogPayloadDto = {
level,
targetId,
message: boundMessage(message),
};
await invoke("emit_frontend_log", { payload });
}
function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
return level === "trace"
? originalConsole.trace(message)
: level === "debug"
? originalConsole.debug(message)
: level === "info"
? originalConsole.info(message)
: level === "warn"
? originalConsole.warn(message)
: originalConsole.error(message);
}
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
writeOriginalConsole(level, message);
await sendFrontendLog(level, targetId, message);
}
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.trace(message);
void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.debug(message);
void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.info(message);
void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.warn(message);
void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.error(message);
void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
return (...items: unknown[]) => {
original(...items);
void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
};
}
export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
console.log = buildConsoleBridge("info", targetId, originalConsole.log);
console.info = buildConsoleBridge("info", targetId, originalConsole.info);
console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
console.error = buildConsoleBridge("error", targetId, originalConsole.error);
}

View File

@@ -0,0 +1,17 @@
// file: crates/ksp-app-raw-transaction-ingest-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;
}
}

View File

@@ -0,0 +1,760 @@
// file: crates/ksp-app-raw-transaction-ingest-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 type { ShellStatusDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts";
import type { RawIngestCommitment } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts";
import type { RawIngestRouteId } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts";
import type { RawIngestRouteInventoryDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts";
import type { RawIngestRouteRuntimeDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts";
import type { RawIngestRouteStartRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts";
import type { RawIngestRouteStopRequestDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts";
import type { RawIngestRouteMonitoringDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.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 = "routes" | "diagnostics";
const viewTitles: Record<ViewId, string> = {
routes: "Routes",
diagnostics: "Diagnostics",
};
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
const routeCards = new Map<string, HTMLElement>();
let routeInventory: RawIngestRouteInventoryDto | null = null;
let selectedProfileId: string | null = null;
let selectedMonitoringKey: string | null = null;
function isViewId(value: string | undefined): value is ViewId {
return value === "routes" || 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 = `Raw Transaction Ingest Desk — ${viewTitles[viewId]}`;
frontendTrace("main", "Raw Transaction Ingest 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", "Raw Transaction Ingest Desk frontend control clicked", {
controlId: control.id || null,
role: control.getAttribute("role"),
tagName: control.tagName.toLowerCase(),
viewId: control.dataset.view ?? null,
});
},
true,
);
}
function bindNavigation(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const viewId = button.dataset.view;
frontendDebug("main", "Raw Transaction Ingest Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
if (isViewId(viewId)) {
activateView(viewId, "user");
}
});
});
}
function reasonLabel(reason: string | null): string {
const labels: Record<string, string> = {
profile_unresolved: "profil Config non résolu",
network_mismatch: "réseau Transport/Store incohérent",
missing_http_get_block: "HTTP getBlock indisponible",
missing_http_get_transaction: "HTTP getTransaction indisponible",
missing_http_block_scan: "HTTP block scan incomplet",
missing_ws_logs_capability: "capability WS Logs absente",
missing_ws_block_capability: "capability WS Block absente",
missing_helius_transaction_capability: "capability Helius Transaction absente",
missing_yellowstone_grpc: "Yellowstone gRPC absent",
missing_required_secret: "secret Config requis non résolu",
};
if (reason === null) {
return "composable depuis Config";
}
return labels[reason] ?? reason;
}
function runtimeKey(profileId: string, routeId: RawIngestRouteId): string {
return `${profileId}::${routeId}`;
}
function lifecycleBadgeClass(state: string): string {
if (state === "faulted") {
return "badge text-bg-danger";
}
if (state === "running") {
return "badge text-bg-success";
}
if (state === "starting" || state === "stopping") {
return "badge text-bg-primary";
}
if (state === "configured") {
return "badge text-bg-info";
}
return "badge text-bg-secondary";
}
function yesNo(value: boolean): string {
return value ? "oui" : "non";
}
function optionalText(value: string | null): string {
return value ?? "—";
}
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 renderRouteFeedback(message: string, level: "danger" | "info" | "success"): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = false;
feedback.classList.remove("alert-danger", "alert-info", "alert-success");
feedback.classList.add(`alert-${level}`);
feedback.textContent = message;
}
function clearRouteFeedback(): void {
const feedback = document.querySelector<HTMLElement>("#routeCommandFeedback");
if (!feedback) {
return;
}
feedback.hidden = true;
feedback.textContent = "";
}
function monitoringRuntime(status: RawIngestRouteMonitoringDto): RawIngestRouteRuntimeDto {
return {
commitment: status.commitment,
inventoryGeneration: status.inventoryGeneration,
network: status.network,
profileId: status.profileId,
routeId: status.routeId,
state: status.state,
};
}
function recordRouteMonitoring(status: RawIngestRouteMonitoringDto): void {
const key = runtimeKey(status.profileId, status.routeId);
const runtime = monitoringRuntime(status);
routeMonitoring.set(key, status);
lastRuntimes.set(key, runtime);
if (runtimeIsActive(runtime)) {
activeRuntimes.set(key, runtime);
} else {
activeRuntimes.delete(key);
}
}
function selectRouteMonitoring(profileId: string, routeId: RawIngestRouteId, family: string, source: "event" | "user"): void {
const key = runtimeKey(profileId, routeId);
selectedMonitoringKey = key;
renderMonitoringDetail();
frontendDebug("main", "Raw Transaction Ingest Desk route supervision selected", {
family,
routeId,
selected: routeMonitoring.has(key),
source,
});
}
function renderSelectedProfile(): void {
if (routeInventory === null) {
return;
}
const profile = routeInventory.profiles.find(candidate => candidate.profileId === selectedProfileId) ?? routeInventory.profiles[0];
if (!profile) {
return;
}
selectedProfileId = profile.profileId;
text("#routeNetwork", profile.network ?? "non résolu");
text("#routeSelectedProfile", profile.profileId);
text("#headerProfile", profile.profileId);
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
if (routeRoot) {
routeCards.clear();
routeRoot.replaceChildren();
for (const route of profile.routes) {
const key = runtimeKey(profile.profileId, route.routeId);
const runtime = activeRuntimes.get(key) ?? lastRuntimes.get(key) ?? null;
const monitoring = routeMonitoring.get(key) ?? null;
const projectedState = monitoring?.state ?? runtime?.state ?? route.state;
const active = projectedState === "starting" || projectedState === "running" || projectedState === "stopping";
const column = document.createElement("div");
column.className = "col-12 col-xl-6 app-route-column";
const card = document.createElement("div");
card.className = "card h-100 shadow-sm app-route-card";
const body = document.createElement("div");
body.className = "card-body";
const heading = document.createElement("div");
heading.className = "d-flex align-items-start justify-content-between gap-3";
const headingText = document.createElement("div");
const title = document.createElement("h2");
title.className = "h6 mb-1";
title.textContent = route.label;
const code = document.createElement("div");
code.className = "app-route-code text-body-secondary small";
code.textContent = route.routeId;
headingText.append(title, code);
const family = document.createElement("span");
family.className = "badge text-bg-light border text-dark";
family.textContent = route.family;
heading.append(headingText, family);
const meta = document.createElement("div");
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
const state = document.createElement("span");
state.className = lifecycleBadgeClass(projectedState);
state.dataset.monitoringField = "state";
state.textContent = projectedState;
const network = document.createElement("span");
network.className = "badge text-bg-light border text-dark";
network.textContent = route.network ?? "network unresolved";
const reason = document.createElement("span");
reason.className = route.selectable ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
reason.textContent = reasonLabel(route.reason);
meta.append(state, network, reason);
body.append(heading, meta);
if (monitoring) {
const summary = document.createElement("div");
summary.className = "app-route-monitoring-summary mt-3";
const health = document.createElement("span");
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.dataset.monitoringField = "health";
health.textContent = `health: ${monitoring.health}`;
const activity = document.createElement("span");
activity.className = "badge text-bg-light border text-dark";
activity.dataset.monitoringField = "activity";
activity.textContent = `activity: ${monitoring.activity}`;
const persisted = document.createElement("span");
persisted.className = "badge text-bg-light border text-dark";
persisted.dataset.monitoringField = "persisted";
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
const sources = document.createElement("span");
sources.className = "badge text-bg-light border text-dark";
sources.dataset.monitoringField = "sources";
sources.textContent = `sources: ${monitoring.sourceActive}/${monitoring.sourceTotal}`;
const gaps = document.createElement("span");
gaps.className = monitoring.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.dataset.monitoringField = "gaps";
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
summary.append(health, activity, persisted, sources, gaps);
body.append(summary);
}
const actions = document.createElement("div");
actions.className = "d-flex flex-wrap gap-2 mt-3";
if (route.selectable) {
const start = document.createElement("button");
start.className = "btn btn-primary btn-sm";
start.type = "button";
start.textContent = "Start";
start.disabled = active;
start.dataset.routeId = route.routeId;
start.addEventListener("click", () => {
selectedMonitoringKey = key;
clearRouteFeedback();
void startRoute(route.routeId).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "Le démarrage de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Start failed", { routeId: route.routeId });
});
});
actions.append(start);
if (active && runtime !== null) {
const stop = document.createElement("button");
stop.className = "btn btn-outline-danger btn-sm";
stop.type = "button";
stop.textContent = "Stop";
stop.disabled = projectedState === "stopping";
stop.dataset.routeId = route.routeId;
stop.addEventListener("click", () => {
clearRouteFeedback();
void stopRoute(runtime).catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "L'arrêt de la route a été refusé."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route Stop failed", { routeId: route.routeId });
});
});
actions.append(stop);
}
}
if (monitoring) {
const supervise = document.createElement("button");
supervise.className = selectedMonitoringKey === key ? "btn btn-secondary btn-sm" : "btn btn-outline-secondary btn-sm";
supervise.type = "button";
supervise.textContent = "Supervision";
supervise.dataset.routeId = route.routeId;
supervise.addEventListener("click", () => selectRouteMonitoring(profile.profileId, route.routeId, route.family, "user"));
actions.append(supervise);
}
if (actions.childElementCount > 0) {
body.append(actions);
}
card.append(body);
routeCards.set(key, card);
column.append(card);
routeRoot.append(column);
}
}
renderMonitoringDetail();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory profile rendered", {
activeRouteCount: activeRuntimes.size,
monitoringCount: routeMonitoring.size,
profileId: profile.profileId,
routeCount: profile.routes.length,
selectableCount: profile.routes.filter(route => route.selectable).length,
});
}
function renderMonitoringDetail(): void {
const panel = document.querySelector<HTMLElement>("#routeMonitoringDetail");
if (!panel) {
return;
}
const monitoring = selectedMonitoringKey === null ? null : (routeMonitoring.get(selectedMonitoringKey) ?? null);
if (!monitoring) {
panel.hidden = true;
return;
}
panel.hidden = false;
text("#monitorRouteId", monitoring.routeId);
text("#monitorNetwork", monitoring.network);
text("#monitorCommitment", monitoring.commitment);
text("#monitorSequence", monitoring.sequence);
text("#monitorHealth", monitoring.health);
text("#monitorActivity", monitoring.activity);
text("#monitorSourceState", optionalText(monitoring.sourceState));
text("#monitorFault", monitoring.faultDomain === null || monitoring.faultCode === null ? "—" : `${monitoring.faultDomain}/${monitoring.faultCode}`);
const lifecycle = document.querySelector<HTMLElement>("#monitorLifecycle");
if (lifecycle) {
lifecycle.className = lifecycleBadgeClass(monitoring.state);
lifecycle.textContent = monitoring.state;
}
const health = document.querySelector<HTMLElement>("#monitorHealthBadge");
if (health) {
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = monitoring.health;
}
const activity = document.querySelector<HTMLElement>("#monitorActivityBadge");
if (activity) {
activity.className = "badge text-bg-light border text-dark";
activity.textContent = monitoring.activity;
}
const values: Record<string, string> = {
monitorAdmissionQueue: `${monitoring.admissionQueueDepth} / ${monitoring.admissionQueueCapacity}`,
monitorPersistence: `${monitoring.inFlightPersistence} / ${monitoring.persistenceConcurrency}`,
monitorAdmittedTotal: monitoring.admittedTotal,
monitorCanonicalizedTotal: monitoring.canonicalizedTotal,
monitorPersistedTotal: monitoring.persistedTotal,
monitorEntityInsertedTotal: monitoring.entityInsertedTotal,
monitorEntityAlreadyPresentTotal: monitoring.entityAlreadyPresentTotal,
monitorEntitySkippedPurgedTotal: monitoring.entitySkippedPurgedTotal,
monitorObservationInsertedTotal: monitoring.observationInsertedTotal,
monitorObservationAlreadyPresentTotal: monitoring.observationAlreadyPresentTotal,
monitorContentConflictTotal: monitoring.contentConflictTotal,
monitorStoreFailureTotal: monitoring.storeFailureTotal,
monitorSourceFailureTotal: monitoring.sourceFailureTotal,
monitorBackpressureWaitTotal: monitoring.backpressureWaitTotal,
monitorHydrationPending: monitoring.hydrationPending.toString(),
monitorProcessingFrontier: optionalText(monitoring.processingFrontierSlot),
monitorOldestPending: optionalText(monitoring.oldestPendingSlot),
monitorSourceCounts: `${monitoring.sourceActive} active / ${monitoring.sourceReconnecting} reconnecting / ${monitoring.sourceFailed} failed / ${monitoring.sourceTotal} total`,
monitorSourceReconnectTotal: monitoring.sourceReconnectTotal,
monitorSourceReplayAttemptTotal: monitoring.sourceReplayAttemptTotal,
monitorSourceContinuityGapTotal: monitoring.sourceContinuityGapTotal,
monitorContinuityPolicyObserved: yesNo(monitoring.continuityPolicyObserved),
monitorContinuityFrontier: optionalText(monitoring.continuityFrontierSlot),
monitorContinuityHasOpenGaps: yesNo(monitoring.continuityHasOpenGaps),
monitorFailedSourceLossesReconciled: yesNo(monitoring.failedSourceLossesReconciled),
monitorFutureTargetCoverage: yesNo(monitoring.futureTargetCoverage),
monitorOpenGapCount: monitoring.openGapCount.toString(),
monitorRepairingGapCount: monitoring.repairingGapCount.toString(),
monitorRepairedGapTotal: monitoring.repairedGapTotal,
monitorUnresolvedGapTotal: monitoring.unresolvedGapTotal,
monitorOldestOpenGap: optionalText(monitoring.oldestOpenGapStartSlot),
monitorReplayRepairTotal: monitoring.replayRepairTotal,
monitorRedundantCoverageRepairTotal: monitoring.redundantCoverageRepairTotal,
monitorHttpScanRepairTotal: monitoring.httpScanRepairTotal,
monitorRepairBlockFetchTotal: monitoring.repairBlockFetchTotal,
monitorRepairTransactionHydrationTotal: monitoring.repairTransactionHydrationTotal,
};
for (const [id, value] of Object.entries(values)) {
text(`#${id}`, value);
}
const gaps = document.querySelector<HTMLElement>("#monitorGapList");
if (gaps) {
gaps.replaceChildren();
if (monitoring.gaps.length === 0) {
const empty = document.createElement("div");
empty.className = "text-body-secondary small";
empty.textContent = "Aucun gap courant ou récent projeté.";
gaps.append(empty);
} else {
for (const gap of monitoring.gaps) {
const item = document.createElement("div");
item.className = "list-group-item";
const top = document.createElement("div");
top.className = "d-flex justify-content-between gap-3 flex-wrap";
const range = document.createElement("span");
range.className = "app-route-code";
range.textContent = `${gap.startSlot} ${gap.endSlot}`;
const badge = document.createElement("span");
badge.className = gap.state === "open" ? "badge text-bg-warning" : "badge text-bg-secondary";
badge.textContent = gap.state;
top.append(range, badge);
const meta = document.createElement("div");
meta.className = "small text-body-secondary mt-1";
meta.textContent = `gap ${gap.gapId} · ${gap.reason} · repair ${gap.lastMethod ?? "—"}`;
item.append(top, meta);
gaps.append(item);
}
}
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring detail rendered", {
gapCount: monitoring.gaps.length,
routeId: monitoring.routeId,
sequence: monitoring.sequence,
state: monitoring.state,
});
}
function renderRouteMonitoringCard(status: RawIngestRouteMonitoringDto): boolean {
const key = runtimeKey(status.profileId, status.routeId);
const card = routeCards.get(key);
if (!card) {
return false;
}
const field = (name: string): HTMLElement | null => card.querySelector<HTMLElement>(`[data-monitoring-field="${name}"]`);
const state = field("state");
const health = field("health");
const activity = field("activity");
const persisted = field("persisted");
const sources = field("sources");
const gaps = field("gaps");
if (!state || !health || !activity || !persisted || !sources || !gaps) {
return false;
}
state.className = lifecycleBadgeClass(status.state);
state.textContent = status.state;
health.className = status.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
health.textContent = `health: ${status.health}`;
activity.textContent = `activity: ${status.activity}`;
persisted.textContent = `persisted: ${status.persistedTotal}`;
sources.textContent = `sources: ${status.sourceActive}/${status.sourceTotal}`;
gaps.className = status.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
gaps.textContent = `gaps: ${status.openGapCount}`;
return true;
}
function selectedCommitment(): RawIngestCommitment {
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
return selector?.value === "finalized" ? "finalized" : "confirmed";
}
function runtimeIsActive(runtime: RawIngestRouteRuntimeDto): boolean {
return runtime.state === "starting" || runtime.state === "running" || runtime.state === "stopping";
}
function renderMultiRouteRuntime(): void {
const active = Array.from(activeRuntimes.values());
const state = document.querySelector<HTMLElement>("#routeRuntimeState");
if (state) {
state.textContent = active.length === 0 ? "idle" : `${active.length} active`;
state.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-primary";
}
const identities = active.map(runtime => `${runtime.routeId}/${runtime.commitment}`).join(", ");
text("#routeRuntimeIdentity", identities.length === 0 ? "aucun Worker actif" : identities);
const store = document.querySelector<HTMLElement>("#headerStore");
if (store) {
store.textContent = active.length === 0 ? "Store idle" : "Store shared";
store.className = active.length === 0 ? "badge text-bg-secondary" : "badge text-bg-success";
}
const profile = document.querySelector<HTMLSelectElement>("#routeProfile");
if (profile) {
profile.disabled = active.length > 0;
}
}
async function startRoute(routeId: RawIngestRouteId): Promise<void> {
if (routeInventory === null || selectedProfileId === null) {
return;
}
const key = runtimeKey(selectedProfileId, routeId);
const existing = activeRuntimes.get(key);
if (existing !== undefined && runtimeIsActive(existing)) {
return;
}
const request: RawIngestRouteStartRequestDto = {
commitment: selectedCommitment(),
inventoryGeneration: routeInventory.generation,
profileId: selectedProfileId,
routeId,
};
frontendDebug("main", "Raw Transaction Ingest Desk route Start requested", {
commitment: request.commitment,
profileId: request.profileId,
routeId: request.routeId,
});
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "start_route", { request });
lastRuntimes.set(key, response);
if (runtimeIsActive(response)) {
activeRuntimes.set(key, response);
} else {
activeRuntimes.delete(key);
}
renderMultiRouteRuntime();
renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} démarrée (${response.state}).`, "success");
await syncRouteMonitoring("start");
frontendInfo("main", "Raw Transaction Ingest Desk route Start acknowledged", {
activeRouteCount: activeRuntimes.size,
profileId: response.profileId,
routeId: response.routeId,
state: response.state,
});
}
async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
const key = runtimeKey(runtime.profileId, runtime.routeId);
const request: RawIngestRouteStopRequestDto = {
profileId: runtime.profileId,
routeId: runtime.routeId,
};
frontendDebug("main", "Raw Transaction Ingest Desk targeted route Stop requested", {
profileId: request.profileId,
routeId: request.routeId,
});
const response = await invokeKsp<RawIngestRouteRuntimeDto>("main", "stop_route", { request });
lastRuntimes.set(key, response);
if (runtimeIsActive(response)) {
activeRuntimes.set(key, response);
} else {
activeRuntimes.delete(key);
}
renderMultiRouteRuntime();
renderSelectedProfile();
renderRouteFeedback(`Route ${response.routeId} arrêtée (${response.state}).`, "info");
await syncRouteMonitoring("stop");
frontendInfo("main", "Raw Transaction Ingest Desk targeted route Stop completed", {
activeRouteCount: activeRuntimes.size,
profileId: response.profileId,
routeId: response.routeId,
state: response.state,
});
}
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
const key = runtimeKey(status.profileId, status.routeId);
const previous = routeMonitoring.get(key) ?? null;
recordRouteMonitoring(status);
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
selectedMonitoringKey = key;
}
renderMultiRouteRuntime();
const lifecycleChanged = previous === null || previous.state !== status.state;
if (lifecycleChanged || !renderRouteMonitoringCard(status)) {
renderSelectedProfile();
} else if (selectedMonitoringKey === key) {
renderMonitoringDetail();
}
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
lifecycleChanged,
routeId: status.routeId,
sequence: status.sequence,
source,
state: status.state,
terminal: status.terminal,
});
}
async function syncRouteMonitoring(source: "startup" | "start" | "stop" | "user"): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization requested", { source });
const statuses = await invokeKsp<RawIngestRouteMonitoringDto[]>("main", "get_route_monitoring");
routeMonitoring.clear();
activeRuntimes.clear();
lastRuntimes.clear();
for (const status of statuses) {
recordRouteMonitoring(status);
}
if (selectedMonitoringKey !== null && !routeMonitoring.has(selectedMonitoringKey)) {
selectedMonitoringKey = null;
}
if (selectedMonitoringKey === null) {
const preferred = statuses.find(status => status.profileId === selectedProfileId) ?? statuses[0];
if (preferred) {
selectedMonitoringKey = runtimeKey(preferred.profileId, preferred.routeId);
}
}
renderMultiRouteRuntime();
renderSelectedProfile();
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring resynchronization completed", {
activeRouteCount: activeRuntimes.size,
source,
statusCount: statuses.length,
});
}
async function bindRouteMonitoring(): Promise<void> {
await listen<RawIngestRouteMonitoringDto>("ksp-raw-ingest-route-status", event => {
applyRouteMonitoring(event.payload, "event");
});
document.querySelector<HTMLButtonElement>("#refreshRouteMonitoring")?.addEventListener("click", () => {
frontendDebug("main", "Raw Transaction Ingest Desk route monitoring refresh button clicked");
void syncRouteMonitoring("user").catch(caughtError => {
renderRouteFeedback(safeCommandErrorText(caughtError, "La resynchronisation du monitoring a échoué."), "danger");
frontendWarn("main", "Raw Transaction Ingest Desk route monitoring resynchronization failed");
});
});
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring listener and resynchronization control installed");
}
function renderRouteInventory(inventory: RawIngestRouteInventoryDto): void {
routeInventory = inventory;
const requestedProfile = selectedProfileId;
const selectedStillExists = requestedProfile !== null && inventory.profiles.some(profile => profile.profileId === requestedProfile);
selectedProfileId = selectedStillExists ? requestedProfile : inventory.defaultProfile;
const selector = document.querySelector<HTMLSelectElement>("#routeProfile");
if (selector) {
selector.replaceChildren();
for (const profile of inventory.profiles) {
const option = document.createElement("option");
option.value = profile.profileId;
option.textContent = profile.isDefault ? `${profile.profileId} (default)` : profile.profileId;
option.selected = profile.profileId === selectedProfileId;
selector.append(option);
}
}
text("#routeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryGeneration", inventory.generation.toString());
text("#runtimeInventoryDefaultProfile", inventory.defaultProfile);
renderSelectedProfile();
renderMultiRouteRuntime();
frontendTrace("main", "Raw Transaction Ingest Desk Config route inventory rendered", {
generation: inventory.generation,
profileCount: inventory.profiles.length,
});
}
function text(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function renderRuntimeStatus(status: ShellStatusDto): void {
text("#runtimeVersion", status.applicationVersion);
text("#runtimeShellPhase", status.shellPhase);
text("#runtimeConfigDocuments", status.configDocumentCount.toString());
text("#runtimeCompositeProfile", status.activeCompositeProfile ?? "fallback/unresolved");
text("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback");
text("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
text("#headerPhase", status.shellPhase);
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
if (diagnostic) {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic ? `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}` : "";
}
frontendTrace("main", "Raw Transaction Ingest Desk runtime status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
hasStartupDiagnostic: status.startupDiagnostic !== null,
});
}
async function refreshRouteInventory(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk Config route inventory refresh requested");
const inventory = await invokeKsp<RawIngestRouteInventoryDto>("main", "get_route_inventory");
renderRouteInventory(inventory);
}
async function refreshRuntimeStatus(): Promise<void> {
frontendDebug("main", "Raw Transaction Ingest Desk runtime status refresh requested");
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
}
function bindRefreshControls(): void {
document.querySelector<HTMLButtonElement>("#refreshRouteInventory")?.addEventListener("click", () => {
void refreshRouteInventory().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk Config route inventory refresh failed"));
});
document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus")?.addEventListener("click", () => {
void refreshRuntimeStatus().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk runtime status refresh failed"));
});
document.querySelector<HTMLSelectElement>("#routeProfile")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
selectedProfileId = selector.value;
selectedMonitoringKey = null;
const selected = routeInventory?.profiles.find(profile => profile.profileId === selectedProfileId) ?? null;
frontendDebug("main", "Raw Transaction Ingest Desk logical network selected", {
profileId: selectedProfileId,
routeCount: selected?.routes.length ?? 0,
selectionResolved: selected !== null,
});
renderSelectedProfile();
});
document.querySelector<HTMLSelectElement>("#routeCommitment")?.addEventListener("change", event => {
const selector = event.currentTarget;
if (!(selector instanceof HTMLSelectElement)) {
return;
}
frontendDebug("main", "Raw Transaction Ingest Desk commitment selection changed", { commitment: selectedCommitment() });
});
}
async function initializeMain(): Promise<void> {
frontendInfo("main", "Raw Transaction Ingest Desk main frontend loaded");
bindFrontendInteractions();
bindNavigation();
bindRefreshControls();
await bindRouteMonitoring();
activateView("routes", "startup");
await refreshRouteInventory();
await Promise.all([refreshRuntimeStatus(), syncRouteMonitoring("startup")]);
frontendInfo("main", "Raw Transaction Ingest Desk multi-route shared-Store runtime frontend ready");
}
void initializeMain().catch(() => frontendWarn("main", "Raw Transaction Ingest Desk frontend initialization failed"));

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-raw-transaction-ingest-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_raw_transaction_ingest_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", "Raw Transaction Ingest Desk splash frontend loaded", { windowLabel });
const container = document.querySelector<HTMLElement>("#splash-container");
if (container) {
container.style.opacity = "0";
container.style.willChange = "opacity";
frontendTrace("splash", "Splash container prepared for managed fade-in");
}
await listen<SplashOrderDto>("ksp-splash-order", event => {
void handleSplashOrder(event.payload);
});
frontendDebug("splash", "Splash lifecycle listener installed");
try {
await invokeKsp<void>("splash", "splash_frontend_ready");
} catch {
addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
if (container) {
container.style.opacity = "1";
container.style.willChange = "auto";
}
frontendError("splash", "Splash readiness command failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeSplash();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,26 @@
{
"name": "ksp-app-raw-transaction-ingest-desk",
"private": true,
"version": "0.3.15",
"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"
}
}

View File

@@ -0,0 +1,222 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
// version: 9
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
/// Shared Raw Transaction Ingest Desk application state managed by Tauri.
pub(crate) struct AppState {
active_composite_profile: std::option::Option<String>,
config_management: ksp_config_lib::ConfigManagement,
inventory_generation: std::sync::Mutex<u32>,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
route_runtime: std::sync::Arc<crate::RouteRuntimeState>,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
/// Initializes Config ownership and composite-managed Logging without constructing Transport, Store or Worker resources.
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 composite = crate::load_raw_transaction_ingest_desk_composite(&config_management);
let active_composite_profile = match composite {
std::result::Result::Ok(value) => std::option::Option::Some(value.profile_id().to_owned()),
std::result::Result::Err(_) => std::option::Option::None,
};
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 Raw Transaction Ingest Desk splash timings");
return std::result::Result::Ok(Self {
active_composite_profile,
config_management,
inventory_generation: std::sync::Mutex::new(0),
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,
}),
route_runtime: std::sync::Arc::new(crate::RouteRuntimeState::new()),
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
/// Returns the static V1 route/state vocabulary without making any Config composability claim.
#[must_use]
pub(crate) fn route_foundation(&self) -> crate::RawIngestRouteFoundationDto {
let _keep_config_owner_alive = &self.config_management;
return crate::RawIngestRouteFoundationDto::scaffold();
}
/// Rebuilds the complete Config-only route inventory and publishes a fresh monotonic generation.
pub(crate) fn route_inventory(&self) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let generation = self.inventory_generation.lock();
let mut generation = match generation {
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,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let next_generation = generation.checked_add(1);
let next_generation = match next_generation {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_INVENTORY_INVALID,
"Raw Transaction Ingest Desk route inventory generation is exhausted",
));
},
};
let inventory = crate::build_route_inventory(&self.config_management, next_generation);
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
*generation = next_generation;
return std::result::Result::Ok(inventory);
}
/// Revalidates one future Start request and reconstructs its exact Transport/Worker source contract without launching a Worker.
pub(crate) fn validate_route_start(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
let generation = self.inventory_generation.lock();
let generation = match generation {
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,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
return crate::validate_route_start(&self.config_management, generation, request);
}
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
let admission = self.route_runtime.ensure_start_admission_open();
if let std::result::Result::Err(error) = admission {
return std::result::Result::Err(error);
}
let prepared = {
let generation = self.inventory_generation.lock();
let generation = match generation {
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,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let prepared = crate::prepare_route_start(&self.config_management, *generation, request);
match prepared {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
}
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current same-network runtime session.
pub(crate) fn route_monitoring(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
return self.route_runtime.monitoring_statuses();
}
/// Requests cooperative Stop for one exact logical route and waits until its Worker cleanup is complete.
pub(crate) async fn stop_route(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
return self.route_runtime.stop_and_wait(request).await;
}
/// Closes route Start admission once and requests cooperative Stop for all routes during application shutdown.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
return self.route_runtime.begin_shutdown();
}
/// Waits for bounded cleanup of all route Workers and the shared Store during application shutdown.
pub(crate) async fn shutdown_routes(&self) -> ksp_core_lib::Result<()> {
return self.route_runtime.shutdown_and_wait().await;
}
/// Builds the safe scaffold status exposed by the 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 contains too many descriptors for the Raw Transaction Ingest Desk runtime DTO",
)
.with_source(error),
);
},
};
let runtime = self.logging_runtime.lock();
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk Logging runtime state lock is poisoned",
));
},
};
let _keep_guard_alive = &runtime.guard;
return std::result::Result::Ok(crate::ShellStatusDto {
active_composite_profile: self.active_composite_profile.clone(),
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.010-runtime-monitoring".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
/// 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>,
}

View File

@@ -0,0 +1,228 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/bootstrap.rs
// version: 2
//! Config composite and Logging bootstrap for Raw Transaction Ingest 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 Raw Transaction Ingest Desk Config composite using its registered logical `file_id` and autonomous default profile.
pub(crate) fn load_raw_transaction_ingest_desk_composite(
management: &ksp_config_lib::ConfigManagement,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
return crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::None);
}
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile by registered logical file identifier.
pub(crate) fn load_raw_transaction_ingest_desk_composite_profile(
management: &ksp_config_lib::ConfigManagement,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_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, requested_profile);
}
/// Returns one required standard profile from the Raw Transaction Ingest 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, "Raw Transaction Ingest 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,
"Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest 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_raw_transaction_ingest_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 Raw Transaction Ingest 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);
}
}

View File

@@ -0,0 +1,39 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/constants.rs
// version: 6
//! Application-owned tracing targets and Config component identifiers.
/// 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";
/// Composite-local optional Transport source for Helius WebSocket capabilities.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS: &str = "transport.helius";
/// Prefix used by optional same-network Transport capability sources.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX: &str = "transport.";
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE: &str = "transport.yellowstone";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "raw_transaction_ingest.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used while rebuilding Config-only route inventory.
pub(crate) const TRACING_DOMAIN_INVENTORY: &str = "raw_transaction_ingest.inventory";
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
pub(crate) const TRACING_DOMAIN_ROUTE_RUNTIME: &str = "raw_transaction_ingest.route_runtime";
/// Structured domain used while revalidating future Start requests and reconstructing route resources.
pub(crate) const TRACING_DOMAIN_ROUTE_START: &str = "raw_transaction_ingest.route_start";
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "raw_transaction_ingest.shell";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Raw Transaction Ingest Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-raw-transaction-ingest-desk";
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-raw-transaction-ingest-desk.frontend";
/// Owning target for main-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-raw-transaction-ingest-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-raw-transaction-ingest-desk.frontend.splash";

View File

@@ -0,0 +1,56 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_common.rs
// version: 1
//! Common Tauri DTOs shared by the Raw Transaction Ingest Desk shell.
use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_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 Raw Transaction Ingest Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_common/ShellStatusDto.ts")]
pub(crate) struct ShellStatusDto {
/// Active composite profile selected by Config, when the composite resolves successfully.
pub(crate) active_composite_profile: std::option::Option<String>,
/// 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 Raw Transaction Ingest 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;

View File

@@ -0,0 +1,365 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
// version: 7
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
use ts_rs::TS; // rust-rules: trait-import
/// Stable logical V1 route identifiers owned by Raw Transaction Ingest Desk.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "kebab-case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteId.ts")]
pub(crate) enum RawIngestRouteId {
/// Yellowstone block subscription plus same-network HTTP block reconciliation.
YellowstoneHydrated,
/// Standard Solana logs subscription plus same-network HTTP hydration.
StandardLogsHydrated,
/// Standard Solana block subscription with direct RAW projection.
StandardBlockDirect,
/// Helius transaction subscription plus same-network HTTP hydration.
HeliusTransactionHydrated,
/// HTTP block polling and direct block material projection.
HttpBlockPolling,
}
impl crate::RawIngestRouteId {
/// Returns all V1 route identifiers in stable UI order.
#[must_use]
pub(crate) fn all() -> std::vec::Vec<Self> {
return vec![Self::YellowstoneHydrated, Self::StandardLogsHydrated, Self::StandardBlockDirect, Self::HeliusTransactionHydrated, Self::HttpBlockPolling];
}
/// Returns the application-owned route family used by safe UI projections.
#[must_use]
pub(crate) const fn family(self) -> crate::RawIngestRouteFamily {
return match self {
Self::YellowstoneHydrated => crate::RawIngestRouteFamily::Yellowstone,
Self::StandardLogsHydrated => crate::RawIngestRouteFamily::StandardLogs,
Self::StandardBlockDirect => crate::RawIngestRouteFamily::StandardBlock,
Self::HeliusTransactionHydrated => crate::RawIngestRouteFamily::HeliusTransaction,
Self::HttpBlockPolling => crate::RawIngestRouteFamily::HttpBlockPolling,
};
}
/// Returns the stable operator-facing route label without exposing physical endpoint metadata.
#[must_use]
pub(crate) const fn label(self) -> &'static str {
return match self {
Self::YellowstoneHydrated => "Yellowstone Block + HTTP block reconciliation",
Self::StandardLogsHydrated => "Standard Logs + HTTP hydration",
Self::StandardBlockDirect => "Standard Block direct",
Self::HeliusTransactionHydrated => "Helius Transaction + HTTP hydration",
Self::HttpBlockPolling => "HTTP Block Polling",
};
}
/// Returns the stable kebab-case route identifier used by safe logs and IPC.
#[must_use]
pub(crate) const fn as_str(self) -> &'static str {
return match self {
Self::YellowstoneHydrated => "yellowstone-hydrated",
Self::StandardLogsHydrated => "standard-logs-hydrated",
Self::StandardBlockDirect => "standard-block-direct",
Self::HeliusTransactionHydrated => "helius-transaction-hydrated",
Self::HttpBlockPolling => "http-block-polling",
};
}
}
/// Start-time commitments accepted by Raw Transaction Ingest Desk.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestCommitment.ts")]
pub(crate) enum RawIngestCommitment {
/// Confirmed commitment.
Confirmed,
/// Finalized commitment.
Finalized,
}
impl crate::RawIngestCommitment {
/// Returns the stable wire label.
#[must_use]
pub(crate) const fn as_str(self) -> &'static str {
return match self {
Self::Confirmed => "confirmed",
Self::Finalized => "finalized",
};
}
/// Maps the application-owned bounded commitment into the Transport contract.
#[must_use]
pub(crate) const fn into_transport(self) -> ksp_onchain_transport_lib::SolanaCommitment {
return match self {
Self::Confirmed => ksp_onchain_transport_lib::SolanaCommitment::Confirmed,
Self::Finalized => ksp_onchain_transport_lib::SolanaCommitment::Finalized,
};
}
}
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
pub(crate) struct RawIngestRouteStartRequestDto {
/// Confirmed/finalized commitment selected for the future Worker.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation observed by the caller.
pub(crate) inventory_generation: u32,
/// Safe logical network-profile identifier selected from the current inventory.
pub(crate) profile_id: String,
/// Stable logical route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
}
/// Safe targeted Stop request containing only the logical Worker route identity.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
pub(crate) struct RawIngestRouteStopRequestDto {
/// Safe logical network-profile identifier that owns the Worker route.
pub(crate) profile_id: String,
/// Stable logical route identifier to stop.
pub(crate) route_id: crate::RawIngestRouteId,
}
const RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256;
impl crate::RawIngestRouteStartRequestDto {
/// Validates the only caller-controlled free-form logical identifier before Config or runtime reconstruction.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
impl crate::RawIngestRouteStopRequestDto {
/// Validates the only caller-controlled free-form logical identifier before runtime lookup.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
fn validate_profile_id(profile_id: &str) -> ksp_core_lib::Result<()> {
if profile_id.is_empty()
|| profile_id.len() > RAW_INGEST_PROFILE_ID_MAX_BYTES
|| profile_id.trim() != profile_id
|| profile_id.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_REQUEST_INVALID,
"Raw Transaction Ingest Desk route request contains an invalid logical profile identifier",
));
}
return std::result::Result::Ok(());
}
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartValidationDto.ts")]
pub(crate) struct RawIngestRouteStartValidationDto {
/// Commitment validated by the exact Worker source constructor.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation validated against backend state.
pub(crate) inventory_generation: u32,
/// Logical network re-proven from Store and Transport Config.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Exact logical route whose Transport resources were reconstructed.
pub(crate) route_id: crate::RawIngestRouteId,
}
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle state.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteRuntimeDto.ts")]
pub(crate) struct RawIngestRouteRuntimeDto {
/// Confirmed/finalized commitment bound to the active Worker.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation revalidated before Store opening.
pub(crate) inventory_generation: u32,
/// Logical network bound identically to Store and Worker.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Exact logical route owned by the independent Worker.
pub(crate) route_id: crate::RawIngestRouteId,
/// Safe application-owned projection of current Worker lifecycle.
pub(crate) state: crate::RawIngestRouteState,
}
/// Stable application-owned route families used only for safe presentation and orchestration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFamily.ts")]
pub(crate) enum RawIngestRouteFamily {
/// Yellowstone gRPC subscription route.
Yellowstone,
/// Standard Solana logs WebSocket route.
StandardLogs,
/// Standard Solana block WebSocket route.
StandardBlock,
/// Helius transaction WebSocket route.
HeliusTransaction,
/// HTTP block polling route.
HttpBlockPolling,
}
/// Safe reasons explaining why one logical route cannot currently be composed from Config.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteUnavailableReason.ts")]
pub(crate) enum RawIngestRouteUnavailableReason {
/// The selected composite or one of its typed Config components could not be resolved safely.
ProfileUnresolved,
/// Transport and Store do not prove one identical logical network.
NetworkMismatch,
/// No HTTP role can satisfy `getBlock` for same-network block reconciliation.
MissingHttpGetBlock,
/// No HTTP role can satisfy `getTransaction` for same-network hydration.
MissingHttpGetTransaction,
/// No HTTP role can satisfy the complete block-polling method set.
MissingHttpBlockScan,
/// No enabled standard WebSocket endpoint explicitly declares `Logs`.
MissingWsLogsCapability,
/// No enabled standard WebSocket endpoint explicitly declares `Block`.
MissingWsBlockCapability,
/// No enabled Helius WebSocket endpoint explicitly declares `HeliusTransaction`.
MissingHeliusTransactionCapability,
/// No enabled same-network Yellowstone gRPC endpoint is configured.
MissingYellowstoneGrpc,
/// A required Config secret is not resolved for the selected composite profile.
MissingRequiredSecret,
}
/// Stable Raw Transaction Ingest Desk route lifecycle states.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteState.ts")]
pub(crate) enum RawIngestRouteState {
/// Required Config composition is incomplete or incoherent.
Unavailable,
/// Config proves composability but no runtime Worker is active.
Configured,
/// Runtime resources are being revalidated and assembled.
Starting,
/// One Worker instance is active for the logical route.
Running,
/// Stop has been requested and bounded termination is pending.
Stopping,
/// The route Worker terminated normally and its handle was removed.
Stopped,
/// Start or runtime failed with a safe projected code.
Faulted,
}
impl crate::RawIngestRouteState {
/// Returns all lifecycle states in stable presentation order.
#[must_use]
pub(crate) fn all() -> std::vec::Vec<Self> {
return vec![Self::Unavailable, Self::Configured, Self::Starting, Self::Running, Self::Stopping, Self::Stopped, Self::Faulted];
}
}
/// Static route/state foundation retained as the bounded vocabulary behind the live Config inventory.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteFoundationDto.ts")]
pub(crate) struct RawIngestRouteFoundationDto {
/// Five stable V1 logical route identifiers; this is not a composability claim.
pub(crate) route_ids: std::vec::Vec<crate::RawIngestRouteId>,
/// Stable lifecycle state vocabulary reserved for runtime projections.
pub(crate) states: std::vec::Vec<crate::RawIngestRouteState>,
}
impl crate::RawIngestRouteFoundationDto {
/// Builds the bounded route/state vocabulary without resolving endpoints, secrets or Worker resources.
#[must_use]
pub(crate) fn scaffold() -> Self {
return Self { route_ids: crate::RawIngestRouteId::all(), states: crate::RawIngestRouteState::all() };
}
}
/// Safe Config-only composability projection for one logical route.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteDto.ts")]
pub(crate) struct RawIngestRouteDto {
/// Stable route family used for presentation and bounded orchestration.
pub(crate) family: crate::RawIngestRouteFamily,
/// Stable operator-facing label with no physical resource identity.
pub(crate) label: String,
/// Logical network proven by Config when available.
pub(crate) network: std::option::Option<String>,
/// Safe reason explaining an unavailable route.
pub(crate) reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
/// Stable logical V1 route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Whether all Config requirements for the route are currently composable.
pub(crate) selectable: bool,
/// Config-level lifecycle projection; `Configured` is not a runtime health claim.
pub(crate) state: crate::RawIngestRouteState,
}
impl crate::RawIngestRouteDto {
/// Builds one route that is composable from Config without claiming runtime operability.
#[must_use]
pub(crate) fn configured(route_id: crate::RawIngestRouteId, network: &str) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: std::option::Option::Some(network.to_owned()),
reason: std::option::Option::None,
route_id,
selectable: true,
state: crate::RawIngestRouteState::Configured,
};
}
/// Builds one route that is not composable from the current Config profile.
#[must_use]
pub(crate) fn unavailable(route_id: crate::RawIngestRouteId, network: std::option::Option<&str>, reason: crate::RawIngestRouteUnavailableReason) -> Self {
return Self {
family: route_id.family(),
label: route_id.label().to_owned(),
network: network.map(str::to_owned),
reason: std::option::Option::Some(reason),
route_id,
selectable: false,
state: crate::RawIngestRouteState::Unavailable,
};
}
}
/// Safe route inventory for one logical Raw Transaction Ingest Desk composite profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestProfileInventoryDto.ts")]
pub(crate) struct RawIngestProfileInventoryDto {
/// Whether this profile is the composite's autonomous default.
pub(crate) is_default: bool,
/// Logical network proven from the resolved Config profile when available.
pub(crate) network: std::option::Option<String>,
/// Stable composite profile identifier.
pub(crate) profile_id: String,
/// Logical routes applicable to this network profile in stable V1 presentation order; provider-specific routes may be absent when their Config source is not declared.
pub(crate) routes: std::vec::Vec<crate::RawIngestRouteDto>,
}
/// Deterministic Config-only route inventory returned to the frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteInventoryDto.ts")]
pub(crate) struct RawIngestRouteInventoryDto {
/// Composite default profile identifier.
pub(crate) default_profile: String,
/// Monotonic application-owned generation incremented after each successful inventory rebuild.
pub(crate) generation: u32,
/// Safe inventories for every validated composite profile in source order.
pub(crate) profiles: std::vec::Vec<crate::RawIngestProfileInventoryDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_route.rs"]
mod tests;

View File

@@ -0,0 +1,79 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
// version: 6
//! Stable Raw Transaction Ingest Desk application error codes.
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "app_state_invalid");
/// Shared Raw Transaction Ingest 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("raw_transaction_ingest_desk", "app_state_lock_failed");
/// The Raw Transaction Ingest Desk Config composite is missing or misroutes a required component.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_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("raw_transaction_ingest_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("raw_transaction_ingest_desk", "frontend_log_target_invalid");
/// Frontend logging payload exceeds the bounded technical IPC contract.
pub(crate) const ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_payload_invalid");
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
/// Config-only route inventory cannot be projected safely from the validated composite.
pub(crate) const ERROR_CODE_ROUTE_INVENTORY_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_inventory_invalid");
/// Exact Start-time Transport resources cannot be reconstructed safely.
pub(crate) const ERROR_CODE_ROUTE_START_PREPARATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_preparation_failed");
/// The selected logical route is already starting/active or shared Store lifecycle is temporarily exclusive.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_active");
/// No selected route Worker runtime is currently active.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_not_active");
/// A selected route cannot share the currently open Store because its network differs.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_network_mismatch");
/// Store or Worker startup failed after Start-time route revalidation.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_START_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_start_failed");
/// The opened Store did not prove Ready before Worker launch.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_not_ready");
/// The Store could not be reclaimed or explicitly closed after Worker termination.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_store_shutdown_failed");
/// The multi-route runtime reservation/handle state changed unexpectedly.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutting_down");
/// Application shutdown could not join all route Workers and shared Store cleanup within the bounded deadline.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutdown_failed");
/// A Start/Stop IPC request contains an invalid logical identifier or unsupported shape.
pub(crate) const ERROR_CODE_ROUTE_REQUEST_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_request_invalid");
/// Start request references an inventory generation that is no longer current.
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");
/// Start request references a route that is no longer Configured/applicable.
pub(crate) const ERROR_CODE_ROUTE_START_UNAVAILABLE: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_start_unavailable");
/// 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("raw_transaction_ingest_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("raw_transaction_ingest_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction_ingest_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("raw_transaction_ingest_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("raw_transaction_ingest_desk", "tauri_window_operation_failed");

View File

@@ -0,0 +1,241 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
// version: 2
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_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,
}
const FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192;
const FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
let shape = validate_payload_shape(&payload);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
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 validate_payload_shape(payload: &FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
if payload.level.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.target_id.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.message.len() > FRONTEND_LOG_MESSAGE_MAX_BYTES
|| payload.message.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID,
"Frontend log payload exceeds the bounded technical IPC contract",
));
}
return std::result::Result::Ok(());
}
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
let level = level.trim();
if level.eq_ignore_ascii_case("trace") {
return std::result::Result::Ok(FrontendLogLevel::Trace);
}
if level.eq_ignore_ascii_case("debug") {
return std::result::Result::Ok(FrontendLogLevel::Debug);
}
if level.eq_ignore_ascii_case("info") {
return std::result::Result::Ok(FrontendLogLevel::Info);
}
if level.eq_ignore_ascii_case("warn") {
return std::result::Result::Ok(FrontendLogLevel::Warn);
}
if level.eq_ignore_ascii_case("error") {
return std::result::Result::Ok(FrontendLogLevel::Error);
}
return 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> {
let target_id = target_id.trim();
if target_id.eq_ignore_ascii_case("frontend") {
return std::result::Result::Ok(FrontendLogTarget::Frontend);
}
if target_id.eq_ignore_ascii_case("main") {
return std::result::Result::Ok(FrontendLogTarget::Main);
}
if target_id.eq_ignore_ascii_case("splash") {
return std::result::Result::Ok(FrontendLogTarget::Splash);
}
return 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;

View File

@@ -0,0 +1,201 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
// version: 8
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod app_state;
mod bootstrap;
mod constants;
mod dto_common;
mod dto_route;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod route_inventory;
mod route_monitoring;
mod route_runtime;
mod route_start;
mod splash;
mod tauri;
mod tw_main;
mod tw_splash;
/// Runs the KSP Raw Transaction Ingest desktop application.
pub use self::tauri::run;
/// Shared Raw Transaction Ingest Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) use self::bootstrap::config_management;
/// Initializes Logging from the Raw Transaction Ingest Desk composite with a bounded fallback.
pub(crate) use self::bootstrap::initialize_logging;
/// Loads the Raw Transaction Ingest Desk composite selected by its registered logical file identifier.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite;
/// Loads one explicit or default Raw Transaction Ingest Desk composite profile.
pub(crate) use self::bootstrap::load_raw_transaction_ingest_desk_composite_profile;
/// Resolves one required standard Config profile from the Raw Transaction Ingest Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the standard Logging component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite-local identifier for the standard Store component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
/// Composite-local identifier for the standard Transport component.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Composite-local optional Transport source for Helius WebSocket capabilities.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS;
/// Prefix used by optional same-network Transport capability sources.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX;
/// Composite-local optional Transport source for Yellowstone gRPC capabilities.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE;
/// 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 while rebuilding Config-only route inventory.
pub(crate) use self::constants::TRACING_DOMAIN_INVENTORY;
/// Structured domain used by multi-route shared-Store and Worker lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_RUNTIME;
/// Structured domain used while revalidating future Start requests.
pub(crate) use self::constants::TRACING_DOMAIN_ROUTE_START;
/// Structured domain used by the Raw Transaction Ingest Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Raw Transaction Ingest Desk.
pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto;
/// Bounded confirmed/finalized commitment accepted by future route Start requests.
pub(crate) use self::dto_route::RawIngestCommitment;
/// Safe route inventory for one logical composite profile.
pub(crate) use self::dto_route::RawIngestProfileInventoryDto;
/// Safe Config-only composability projection for one logical route.
pub(crate) use self::dto_route::RawIngestRouteDto;
/// Stable application-owned route family.
pub(crate) use self::dto_route::RawIngestRouteFamily;
/// Static route/state foundation retained as the bounded vocabulary behind the Config inventory.
pub(crate) use self::dto_route::RawIngestRouteFoundationDto;
/// Stable logical V1 route identifiers owned by the Desk.
pub(crate) use self::dto_route::RawIngestRouteId;
/// Complete deterministic Config-only route inventory.
pub(crate) use self::dto_route::RawIngestRouteInventoryDto;
/// Safe per-route runtime acknowledgement containing only logical identity and lifecycle.
pub(crate) use self::dto_route::RawIngestRouteRuntimeDto;
/// Safe future Start request containing only logical route selection and inventory generation.
pub(crate) use self::dto_route::RawIngestRouteStartRequestDto;
/// Safe acknowledgement that exact Start-time resources were reconstructed without Worker launch.
pub(crate) use self::dto_route::RawIngestRouteStartValidationDto;
/// Stable route lifecycle states reserved for runtime projections.
pub(crate) use self::dto_route::RawIngestRouteState;
/// Safe targeted Stop request containing only logical route identity.
pub(crate) use self::dto_route::RawIngestRouteStopRequestDto;
/// Safe reason explaining why one logical route is not composable from Config.
pub(crate) use self::dto_route::RawIngestRouteUnavailableReason;
/// Shared Raw Transaction Ingest Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Raw Transaction Ingest Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// Raw Transaction Ingest Desk Config composite is missing or invalid.
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 payload exceeds the bounded technical IPC contract.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID;
/// Frontend logging requested an unsupported target.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Managed and fallback Logging initialization failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config-only route inventory cannot be projected safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
/// Start/Stop IPC request contains an invalid logical shape.
pub(crate) use self::errors::ERROR_CODE_ROUTE_REQUEST_INVALID;
/// A Worker runtime already owns the same logical route in the shared application runtime.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
/// Selected route cannot share the currently open Store because its network differs.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
/// No selected route Worker runtime is currently active.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
/// Application shutdown could not complete all bounded route/Store cleanup.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED;
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN;
/// Store or Worker startup failed after Start-time route revalidation.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
/// The multi-route runtime reservation or handle state changed unexpectedly.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID;
/// The opened Store did not prove Ready before Worker launch.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY;
/// The Store could not be reclaimed or explicitly closed after Worker termination.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED;
/// Exact Start-time Transport resources cannot be reconstructed safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_PREPARATION_FAILED;
/// Start request references a stale inventory generation.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_STALE_INVENTORY;
/// Start request references a route that is no longer Configured/applicable.
pub(crate) use self::errors::ERROR_CODE_ROUTE_START_UNAVAILABLE;
/// Splash readiness originated from an invalid window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// Splash timing configuration is invalid.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Raw Transaction Ingest 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 Raw Transaction Ingest Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Rebuilds the complete safe route inventory from validated Config without runtime I/O.
pub(crate) use self::route_inventory::build_route_inventory;
/// Rebuilds the route inventory from one already captured Config environment snapshot.
pub(crate) use self::route_inventory::build_route_inventory_with_environment;
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) use self::route_monitoring::RAW_INGEST_ROUTE_STATUS_EVENT_NAME;
/// Frontend-safe bounded projection of one run-local continuity gap.
pub(crate) use self::route_monitoring::RawIngestRouteGapDto;
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
pub(crate) use self::route_monitoring::RawIngestRouteMonitoringDto;
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) use self::route_monitoring::project_route_monitoring;
/// Launch ownership for one independent route Worker sharing the application Store when eligible.
pub(crate) use self::route_runtime::RouteRuntimeLaunch;
/// Shared multi-route runtime state with one independent Worker per route and one same-network Store.
pub(crate) use self::route_runtime::RouteRuntimeState;
/// Opens Store and starts one exact route Worker after Start-time revalidation.
pub(crate) use self::route_runtime::start_route_runtime;
/// Fully revalidated Start preparation retaining Store settings and exact Worker resources.
pub(crate) use self::route_start::PreparedRouteStart;
/// Revalidates one Start request and retains its backend-only Store/Worker resources.
pub(crate) use self::route_start::prepare_route_start;
/// Revalidates one future Start request and reconstructs its exact Transport-owned Worker source contract without launch.
pub(crate) use self::route_start::validate_route_start;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings;
/// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_main::require_main_window;
/// Shows and focuses the main Raw Transaction Ingest 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;

View File

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

View File

@@ -0,0 +1,58 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/main.rs
// version: 1
//! Binary entry point for the KSP Raw Transaction Ingest 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 Raw Transaction Ingest 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_raw_transaction_ingest_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-raw-transaction-ingest-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_raw_transaction_ingest_desk_lib::run(arguments.as_slice());
return match run_result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
eprintln!("application error: {error}");
std::process::ExitCode::FAILURE
},
};
}
fn configure_runtime_working_directory() -> std::io::Result<()> {
#[cfg(debug_assertions)]
{
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
return std::env::set_current_dir(workspace_root);
}
#[cfg(not(debug_assertions))]
{
return std::result::Result::Ok(());
}
}

View File

@@ -0,0 +1,459 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
// version: 6
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
struct CompositeProfileCatalog {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
#[derive(Default)]
struct RouteCapabilities {
has_get_transaction: bool,
has_http_block_scan: bool,
has_yellowstone: bool,
has_standard_logs: bool,
has_standard_block: bool,
has_helius_transaction: bool,
helius_source_declared: bool,
helius_source_missing_secret: bool,
yellowstone_has_get_block: bool,
yellowstone_source_missing_secret: bool,
}
impl RouteCapabilities {
fn absorb_transport(&mut self, transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) {
self.has_get_transaction |= http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
self.has_http_block_scan |= http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
self.has_yellowstone |= has_yellowstone_grpc(transport, network);
self.has_standard_logs |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
);
self.has_standard_block |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
);
self.has_helius_transaction |= has_ws_capability(
transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
);
}
}
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
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),
};
return build_route_inventory_with_environment(management, &environment, generation);
}
/// Rebuilds the complete safe route inventory from one already captured Config environment snapshot.
pub(crate) fn build_route_inventory_with_environment(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
generation: u32,
) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
let catalog = composite_profile_catalog(management);
let catalog = match catalog {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
for profile_id in &catalog.profile_ids {
profiles.push(build_profile_inventory(management, environment, profile_id.as_str(), profile_id == &catalog.default_profile));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_INVENTORY,
generation,
profile_count = profiles.len(),
"rebuilt Raw Transaction Ingest Desk network-centric Config-only route inventory"
);
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
}
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = management.engine().load_validated_document(&file_id);
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root = document.value().as_object();
let root = match root {
std::option::Option::Some(value) => value,
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
};
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
let default_profile = match default_profile {
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
};
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
let profile_values = match profile_values {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
};
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
let mut seen = std::collections::BTreeSet::<String>::new();
for profile in profile_values {
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
let profile_id = match profile_id {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
};
if !seen.insert(profile_id.to_owned()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
}
profile_ids.push(profile_id.to_owned());
}
if !seen.contains(default_profile.as_str()) {
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
}
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
}
fn build_profile_inventory(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
profile_id: &str,
is_default: bool,
) -> crate::RawIngestProfileInventoryDto {
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let store_profile = match store_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
},
};
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let reason = resolution_error_reason(&error);
return unavailable_profile(profile_id, is_default, std::option::Option::None, reason);
},
};
let network = store.settings().network().as_str().to_owned();
let base_transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let base_transport_profile = match base_transport_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
},
};
let base_transport = management.engine().resolve_transport_config_profile(&base_transport_profile, environment);
let base_transport = match base_transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return unavailable_profile(profile_id, is_default, std::option::Option::Some(network.as_str()), resolution_error_reason(&error));
},
};
if !transport_matches_network(&base_transport, network.as_str()) {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
);
}
let mut capabilities = RouteCapabilities::default();
capabilities.absorb_transport(&base_transport, network.as_str());
for (component_id, component) in composite.components() {
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT {
continue;
}
if !component_id.starts_with(crate::COMPOSITE_COMPONENT_ID_TRANSPORT_PREFIX) {
continue;
}
if component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS && component_id != crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
capabilities.helius_source_declared = true;
}
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
}
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING => {
mark_optional_source_missing_secret(&mut capabilities, component_id.as_str());
continue;
},
std::result::Result::Err(_) => {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
);
},
};
if !transport_matches_network(&resolved, network.as_str()) {
return unavailable_profile(
profile_id,
is_default,
std::option::Option::Some(network.as_str()),
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
);
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
capabilities.yellowstone_has_get_block = http_role_supports_methods(resolved.http_settings(), &["getBlock"]);
}
capabilities.absorb_transport(&resolved, network.as_str());
}
let routes = build_configured_profile_routes(&capabilities, network.as_str());
return crate::RawIngestProfileInventoryDto { is_default, network: std::option::Option::Some(network), profile_id: profile_id.to_owned(), routes };
}
fn mark_optional_source_missing_secret(capabilities: &mut RouteCapabilities, component_id: &str) {
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS {
capabilities.helius_source_missing_secret = true;
}
if component_id == crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE {
capabilities.yellowstone_source_missing_secret = true;
}
}
fn build_configured_profile_routes(capabilities: &RouteCapabilities, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
let yellowstone_reason = if capabilities.yellowstone_source_missing_secret {
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
} else {
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc
};
let helius_reason = if capabilities.helius_source_missing_secret {
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret
} else {
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability
};
let mut routes = vec![
route_with_requirements(
crate::RawIngestRouteId::YellowstoneHydrated,
network,
&[
(capabilities.has_yellowstone, yellowstone_reason),
(capabilities.yellowstone_has_get_block, crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
network,
&[
(capabilities.has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
),
route_with_requirements(
crate::RawIngestRouteId::StandardBlockDirect,
network,
&[(capabilities.has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
),
];
if capabilities.helius_source_declared {
routes.push(route_with_requirements(
crate::RawIngestRouteId::HeliusTransactionHydrated,
network,
&[
(capabilities.has_helius_transaction, helius_reason),
(capabilities.has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
));
}
routes.push(route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
network,
&[(capabilities.has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
));
return routes;
}
fn route_with_requirements(
route_id: crate::RawIngestRouteId,
network: &str,
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
) -> crate::RawIngestRouteDto {
for (available, reason) in requirements.iter().copied() {
if !available {
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
}
}
return crate::RawIngestRouteDto::configured(route_id, network);
}
fn unavailable_profile(
profile_id: &str,
is_default: bool,
network: std::option::Option<&str>,
reason: crate::RawIngestRouteUnavailableReason,
) -> crate::RawIngestProfileInventoryDto {
let routes = crate::RawIngestRouteId::all()
.into_iter()
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
.collect::<std::vec::Vec<_>>();
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
}
fn resolution_error_reason(error: &ksp_core_lib::Error) -> crate::RawIngestRouteUnavailableReason {
if error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING {
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
}
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
}
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let networks = enabled_transport_networks(transport);
return networks.len() == 1 && networks.contains(network);
}
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
let mut networks = std::collections::BTreeSet::<String>::new();
for endpoint in transport.http_settings().endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
if let std::option::Option::Some(settings) = transport.ws_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
if let std::option::Option::Some(settings) = transport.grpc_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
return networks;
}
fn has_ws_capability(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
protocol: ksp_onchain_transport_lib::WsProtocolKind,
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
) -> bool {
let settings = match transport.ws_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
for endpoint in settings.endpoints() {
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
return true;
}
}
return false;
}
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let settings = match transport.grpc_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
}
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
for method_name in methods {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
request_kinds.push(descriptor.request_kind());
}
let mut role_names = std::collections::BTreeSet::<String>::new();
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
role_names.insert(role.role().as_str().to_owned());
}
}
}
for role_name in role_names {
let mut complete = true;
for request_kind in &request_kinds {
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
complete = false;
break;
}
}
if complete {
return true;
}
}
return false;
}
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
for endpoint in settings.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if !role.enabled() || role.role().as_str() != role_name {
continue;
}
for capability in role.request_kinds() {
if capability.is_wildcard() || capability.as_str() == request_kind {
return true;
}
}
}
}
return false;
}
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
}
#[cfg(test)]
#[path = "../unit_tests/route_inventory.rs"]
mod tests;

View File

@@ -0,0 +1,352 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_monitoring.rs
// version: 1
//! Frontend-safe latest-value monitoring projection for Raw Transaction Ingest Desk route Workers.
use ts_rs::TS; // rust-rules: trait-import
/// Stable Tauri event carrying one complete latest-value route monitoring projection.
pub(crate) const RAW_INGEST_ROUTE_STATUS_EVENT_NAME: &str = "ksp-raw-ingest-route-status";
/// Frontend-safe bounded projection of one run-local continuity gap.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteGapDto.ts")]
pub(crate) struct RawIngestRouteGapDto {
/// Run-local gap identifier encoded as decimal text to avoid JavaScript integer truncation.
pub(crate) gap_id: String,
/// Inclusive first slot encoded as decimal text.
pub(crate) start_slot: String,
/// Inclusive last slot encoded as decimal text.
pub(crate) end_slot: String,
/// Stable source-neutral gap lifecycle code.
pub(crate) state: String,
/// Stable source-neutral reason code.
pub(crate) reason: String,
/// Stable source-neutral latest repair mechanism code, when any.
pub(crate) last_method: std::option::Option<String>,
}
/// Frontend-safe latest-value monitoring projection of one independent route Worker.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/route_monitoring/RawIngestRouteMonitoringDto.ts")]
pub(crate) struct RawIngestRouteMonitoringDto {
/// Confirmed/finalized commitment bound to this Worker run.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation revalidated before this Worker started.
pub(crate) inventory_generation: u32,
/// Logical network shared by Worker and Store.
pub(crate) network: String,
/// Safe logical network-profile identifier.
pub(crate) profile_id: String,
/// Stable logical route identifier.
pub(crate) route_id: crate::RawIngestRouteId,
/// Monotone Worker snapshot sequence encoded as decimal text.
pub(crate) sequence: String,
/// Safe application-owned lifecycle projection.
pub(crate) state: crate::RawIngestRouteState,
/// Stable generic Worker health code.
pub(crate) health: String,
/// Stable generic Worker activity code.
pub(crate) activity: String,
/// Whether this latest Worker snapshot is terminal.
pub(crate) terminal: bool,
/// Stable terminal fault domain when lifecycle is faulted.
pub(crate) fault_domain: std::option::Option<String>,
/// Stable terminal fault code when lifecycle is faulted.
pub(crate) fault_code: std::option::Option<String>,
/// Configured bounded admission queue capacity.
pub(crate) admission_queue_capacity: u32,
/// Latest admission queue depth.
pub(crate) admission_queue_depth: u32,
/// Configured Store persistence concurrency.
pub(crate) persistence_concurrency: u32,
/// Latest number of in-flight Store persistence operations.
pub(crate) in_flight_persistence: u32,
/// Total admitted ingress entries encoded as decimal text.
pub(crate) admitted_total: String,
/// Total canonicalized ingress entries encoded as decimal text.
pub(crate) canonicalized_total: String,
/// Total successful Store persistence outcomes encoded as decimal text.
pub(crate) persisted_total: String,
/// Total newly inserted canonical RAW entities encoded as decimal text.
pub(crate) entity_inserted_total: String,
/// Total canonical RAW entities already present encoded as decimal text.
pub(crate) entity_already_present_total: String,
/// Total purge tombstones respected by persistence encoded as decimal text.
pub(crate) entity_skipped_purged_total: String,
/// Total newly inserted acquisition observations encoded as decimal text.
pub(crate) observation_inserted_total: String,
/// Total acquisition observations already present encoded as decimal text.
pub(crate) observation_already_present_total: String,
/// Total durable content conflicts encoded as decimal text.
pub(crate) content_conflict_total: String,
/// Total non-conflict Store failures encoded as decimal text.
pub(crate) store_failure_total: String,
/// Total source-task failures encoded as decimal text.
pub(crate) source_failure_total: String,
/// Total bounded-admission backpressure waits encoded as decimal text.
pub(crate) backpressure_wait_total: String,
/// Latest number of source signals awaiting hydration/admission processing.
pub(crate) hydration_pending: u32,
/// Run-local processing frontier encoded as decimal text when established.
pub(crate) processing_frontier_slot: std::option::Option<String>,
/// Oldest slot still owning pending source work encoded as decimal text when present.
pub(crate) oldest_pending_slot: std::option::Option<String>,
/// Whether continuity health policy has emitted a run-local projection.
pub(crate) continuity_policy_observed: bool,
/// Latest run-local continuity frontier encoded as decimal text when established.
pub(crate) continuity_frontier_slot: std::option::Option<String>,
/// Whether one or more continuity gaps are currently open.
pub(crate) continuity_has_open_gaps: bool,
/// Whether all failed-source continuity obligations have been reconciled.
pub(crate) failed_source_losses_reconciled: bool,
/// Whether current coverage proves the target can continue into future slots.
pub(crate) future_target_coverage: bool,
/// Stable source-neutral latest productive-source lifecycle code.
pub(crate) source_state: std::option::Option<String>,
/// Configured logical live-source count.
pub(crate) source_total: u32,
/// Latest Active source count.
pub(crate) source_active: u32,
/// Latest Reconnecting source count.
pub(crate) source_reconnecting: u32,
/// Latest Failed source count.
pub(crate) source_failed: u32,
/// Successful source reconnect count encoded as decimal text.
pub(crate) source_reconnect_total: String,
/// Replay-bearing reconnect attempt count encoded as decimal text.
pub(crate) source_replay_attempt_total: String,
/// Proven replay-retention continuity gap count encoded as decimal text.
pub(crate) source_continuity_gap_total: String,
/// Number of currently open continuity gaps.
pub(crate) open_gap_count: u32,
/// Number of continuity gaps currently under repair.
pub(crate) repairing_gap_count: u32,
/// Cumulative repaired-gap count encoded as decimal text.
pub(crate) repaired_gap_total: String,
/// Cumulative unresolved-gap count encoded as decimal text.
pub(crate) unresolved_gap_total: String,
/// Cumulative replay repair count encoded as decimal text.
pub(crate) replay_repair_total: String,
/// Cumulative redundant-coverage repair count encoded as decimal text.
pub(crate) redundant_coverage_repair_total: String,
/// Cumulative bounded HTTP scan repair count encoded as decimal text.
pub(crate) http_scan_repair_total: String,
/// Cumulative direct block-fetch repair count encoded as decimal text.
pub(crate) repair_block_fetch_total: String,
/// Cumulative transaction-hydration repair count encoded as decimal text.
pub(crate) repair_transaction_hydration_total: String,
/// Oldest currently open gap start slot encoded as decimal text when present.
pub(crate) oldest_open_gap_start_slot: std::option::Option<String>,
/// Bounded source-neutral details for current and recent continuity gaps.
pub(crate) gaps: std::vec::Vec<crate::RawIngestRouteGapDto>,
}
/// Projects one complete concrete Worker snapshot to the safe route monitoring contract.
pub(crate) fn project_route_monitoring(
runtime: &crate::RawIngestRouteRuntimeDto,
snapshot: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshot,
) -> ksp_core_lib::Result<crate::RawIngestRouteMonitoringDto> {
let worker = snapshot.worker_snapshot();
let fault = worker.state().fault_code();
let admission_queue_capacity = usize_to_u32(snapshot.admission_queue_capacity(), "admission_queue_capacity");
let admission_queue_capacity = match admission_queue_capacity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admission_queue_depth = usize_to_u32(snapshot.admission_queue_depth(), "admission_queue_depth");
let admission_queue_depth = match admission_queue_depth {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persistence_concurrency = usize_to_u32(snapshot.persistence_concurrency(), "persistence_concurrency");
let persistence_concurrency = match persistence_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let in_flight_persistence = usize_to_u32(snapshot.in_flight_persistence(), "in_flight_persistence");
let in_flight_persistence = match in_flight_persistence {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hydration_pending = usize_to_u32(snapshot.hydration_pending(), "hydration_pending");
let hydration_pending = match hydration_pending {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_total = usize_to_u32(snapshot.source_total(), "source_total");
let source_total = match source_total {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_active = usize_to_u32(snapshot.source_active(), "source_active");
let source_active = match source_active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_reconnecting = usize_to_u32(snapshot.source_reconnecting(), "source_reconnecting");
let source_reconnecting = match source_reconnecting {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source_failed = usize_to_u32(snapshot.source_failed(), "source_failed");
let source_failed = match source_failed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let open_gap_count = usize_to_u32(snapshot.open_gap_count(), "open_gap_count");
let open_gap_count = match open_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let repairing_gap_count = usize_to_u32(snapshot.repairing_gap_count(), "repairing_gap_count");
let repairing_gap_count = match repairing_gap_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let gaps = snapshot.gaps().iter().map(project_gap).collect::<std::vec::Vec<_>>();
return std::result::Result::Ok(crate::RawIngestRouteMonitoringDto {
commitment: runtime.commitment,
inventory_generation: runtime.inventory_generation,
network: runtime.network.clone(),
profile_id: runtime.profile_id.clone(),
route_id: runtime.route_id,
sequence: worker.sequence().value().to_string(),
state: project_worker_state(worker.state()),
health: worker.health().code().to_owned(),
activity: worker.activity().code().to_owned(),
terminal: worker.state().is_terminal(),
fault_domain: fault.map(|code| return code.domain().to_owned()),
fault_code: fault.map(|code| return code.code().to_owned()),
admission_queue_capacity,
admission_queue_depth,
persistence_concurrency,
in_flight_persistence,
admitted_total: snapshot.admitted_total().to_string(),
canonicalized_total: snapshot.canonicalized_total().to_string(),
persisted_total: snapshot.persisted_total().to_string(),
entity_inserted_total: snapshot.entity_inserted_total().to_string(),
entity_already_present_total: snapshot.entity_already_present_total().to_string(),
entity_skipped_purged_total: snapshot.entity_skipped_purged_total().to_string(),
observation_inserted_total: snapshot.observation_inserted_total().to_string(),
observation_already_present_total: snapshot.observation_already_present_total().to_string(),
content_conflict_total: snapshot.content_conflict_total().to_string(),
store_failure_total: snapshot.store_failure_total().to_string(),
source_failure_total: snapshot.source_failure_total().to_string(),
backpressure_wait_total: snapshot.backpressure_wait_total().to_string(),
hydration_pending,
processing_frontier_slot: slot_text(snapshot.processing_frontier_slot()),
oldest_pending_slot: slot_text(snapshot.oldest_pending_slot()),
continuity_policy_observed: snapshot.continuity_policy_observed(),
continuity_frontier_slot: slot_text(snapshot.continuity_frontier_slot()),
continuity_has_open_gaps: snapshot.continuity_has_open_gaps(),
failed_source_losses_reconciled: snapshot.failed_source_losses_reconciled(),
future_target_coverage: snapshot.future_target_coverage(),
source_state: snapshot.source_state().map(|state| return source_state_code(state).to_owned()),
source_total,
source_active,
source_reconnecting,
source_failed,
source_reconnect_total: snapshot.source_reconnect_total().to_string(),
source_replay_attempt_total: snapshot.source_replay_attempt_total().to_string(),
source_continuity_gap_total: snapshot.source_continuity_gap_total().to_string(),
open_gap_count,
repairing_gap_count,
repaired_gap_total: snapshot.repaired_gap_total().to_string(),
unresolved_gap_total: snapshot.unresolved_gap_total().to_string(),
replay_repair_total: snapshot.replay_repair_total().to_string(),
redundant_coverage_repair_total: snapshot.redundant_coverage_repair_total().to_string(),
http_scan_repair_total: snapshot.http_scan_repair_total().to_string(),
repair_block_fetch_total: snapshot.repair_block_fetch_total().to_string(),
repair_transaction_hydration_total: snapshot.repair_transaction_hydration_total().to_string(),
oldest_open_gap_start_slot: slot_text(snapshot.oldest_open_gap_start_slot()),
gaps,
});
}
fn project_gap(gap: &ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapSnapshot) -> crate::RawIngestRouteGapDto {
return crate::RawIngestRouteGapDto {
gap_id: gap.gap_id().value().to_string(),
start_slot: gap.start_slot().to_string(),
end_slot: gap.end_slot().to_string(),
state: gap_state_code(gap.state()).to_owned(),
reason: gap_reason_code(gap.reason()).to_owned(),
last_method: gap.last_method().map(|method| return repair_method_code(method).to_owned()),
};
}
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
return match state {
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
_ => crate::RawIngestRouteState::Faulted,
};
}
fn source_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active => "active",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting => "reconnecting",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closing => "closing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Closed => "closed",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Failed => "failed",
};
}
fn gap_state_code(state: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState) -> &'static str {
return match state {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Pending => "pending",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing => "repairing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repaired => "repaired",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Unresolved => "unresolved",
};
}
fn gap_reason_code(reason: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason) -> &'static str {
return match reason {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::HttpProducedBlockUnavailable => "http_produced_block_unavailable",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::KnownReferenceMissing => "known_reference_missing",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure => "source_failure",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::TransportOverflow => "transport_overflow",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::WebSocketReconnect => "websocket_reconnect",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::YellowstoneRetention => "yellowstone_retention",
};
}
fn repair_method_code(method: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod) -> &'static str {
return match method {
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::Replay => "replay",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::RedundantCoverage => "redundant_coverage",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::HttpScan => "http_scan",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::BlockFetch => "block_fetch",
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration => "transaction_hydration",
};
}
fn slot_text(value: std::option::Option<u64>) -> std::option::Option<String> {
return value.map(|slot| return slot.to_string());
}
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,
"Raw Transaction Ingest Desk cannot project a bounded Worker counter to the frontend",
)
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/route_monitoring.rs"]
mod tests;

View File

@@ -0,0 +1,910 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
// version: 8
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
const ROUTE_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
struct RouteRuntimeInner {
last_monitoring: std::vec::Vec<crate::RawIngestRouteMonitoringDto>,
last_terminals: std::vec::Vec<crate::RawIngestRouteRuntimeDto>,
network: std::option::Option<String>,
next_sequence: u64,
routes: std::vec::Vec<RouteRuntimeSlot>,
shutting_down: bool,
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
store_closing_token: std::option::Option<u64>,
store_opening_token: std::option::Option<u64>,
}
/// Shared multi-route runtime state. Each route owns an independent Worker while same-network routes share one Store.
pub(crate) struct RouteRuntimeState {
inner: std::sync::Mutex<RouteRuntimeInner>,
}
impl crate::RouteRuntimeState {
/// Creates an idle multi-route runtime state without Store or Worker ownership.
#[must_use]
pub(crate) fn new() -> Self {
return Self {
inner: std::sync::Mutex::new(RouteRuntimeInner {
last_monitoring: std::vec::Vec::new(),
last_terminals: std::vec::Vec::new(),
network: std::option::Option::None,
next_sequence: 0,
routes: std::vec::Vec::new(),
shutting_down: false,
store: std::option::Option::None,
store_closing_token: std::option::Option::None,
store_opening_token: std::option::Option::None,
}),
};
}
/// Rejects new route Start work after application shutdown has closed admission.
pub(crate) fn ensure_start_admission_open(&self) -> ksp_core_lib::Result<()> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
return std::result::Result::Ok(());
}
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
if inner.store_closing_token.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk Store is still closing after the previous route set",
));
}
if inner.routes.iter().any(|slot| return slot.matches_identity(prepared.profile_id.as_str(), prepared.route_id)) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk already owns one Worker for the selected logical route",
));
}
if let std::option::Option::Some(network) = inner.network.as_deref()
&& network != prepared.network.as_str()
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
"Raw Transaction Ingest Desk cannot share one Store across different logical networks",
));
}
let needs_store_open = inner.store.is_none();
if needs_store_open && inner.store_opening_token.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
"Raw Transaction Ingest Desk same-network Store is still opening",
));
}
let sequence = match inner.next_sequence.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED,
"Raw Transaction Ingest Desk Worker sequence is exhausted",
));
},
};
let worker_id = ksp_worker_api::WorkerId::new(format!("raw-ingest-desk-{sequence}"));
let worker_id = match worker_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot create Raw Transaction Ingest Desk Worker identity")
.with_source(error),
);
},
};
let identity = RouteRuntimeIdentity {
commitment: prepared.commitment,
inventory_generation: prepared.inventory_generation,
network: prepared.network.clone(),
profile_id: prepared.profile_id.clone(),
route_id: prepared.route_id,
};
if inner.network.is_none() {
inner.last_monitoring.clear();
inner.last_terminals.clear();
inner.network = std::option::Option::Some(prepared.network.clone());
}
inner.last_monitoring.retain(|status| return !identity.matches_monitoring(status));
inner.last_terminals.retain(|terminal| return !identity.matches_dto(terminal));
inner.next_sequence = sequence;
if needs_store_open {
inner.store_opening_token = std::option::Option::Some(sequence);
}
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: sequence });
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
}
fn install_store(&self, token: u64, store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.store_opening_token != std::option::Option::Some(token) || inner.store.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk shared Store reservation changed before installation",
));
}
inner.store = std::option::Option::Some(store);
inner.store_opening_token = std::option::Option::None;
return std::result::Result::Ok(());
}
fn shared_store(&self, expected_network: &str) -> ksp_core_lib::Result<std::sync::Arc<ksp_store_lib::Store>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.network.as_deref() != std::option::Option::Some(expected_network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH,
"Raw Transaction Ingest Desk shared Store network no longer matches the selected route",
));
}
return match &inner.store {
std::option::Option::Some(store) => std::result::Result::Ok(std::sync::Arc::clone(store)),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk shared Store is not installed",
)),
};
}
fn activate(
&self,
reservation: &RouteRuntimeReservation,
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let position = inner.routes.iter().position(|slot| return slot.starting_token() == std::option::Option::Some(reservation.token));
let position = match position {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID,
"Raw Transaction Ingest Desk route reservation changed before Worker activation",
));
},
};
let stop_requested = match &inner.routes[position] {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested,
RouteRuntimeSlot::Active { .. } => false,
};
let should_stop = stop_requested || inner.shutting_down;
if should_stop {
let _accepted = handle.request_stop();
}
let state = if should_stop {
crate::RawIngestRouteState::Stopping
} else {
project_worker_state(handle.snapshot_source().current().worker_snapshot().state())
};
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
return std::result::Result::Ok(reservation.identity.dto(state));
}
fn rollback(&self, token: u64) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let cancelled_terminal = inner.routes.iter().find(|slot| return slot.token() == token).and_then(RouteRuntimeSlot::cancelled_start_terminal);
inner.routes.retain(|slot| return slot.token() != token);
if let std::option::Option::Some(terminal) = cancelled_terminal {
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal);
}
if inner.store_opening_token == std::option::Option::Some(token) {
inner.store_opening_token = std::option::Option::None;
}
if inner.routes.is_empty() {
let store = inner.store.take();
if store.is_some() {
inner.store_closing_token = std::option::Option::Some(token);
} else {
inner.network = std::option::Option::None;
}
return std::result::Result::Ok(store);
}
return std::result::Result::Ok(std::option::Option::None);
}
fn finish(
&self,
token: u64,
terminal: crate::RawIngestRouteRuntimeDto,
monitoring: std::option::Option<crate::RawIngestRouteMonitoringDto>,
) -> ksp_core_lib::Result<std::option::Option<std::sync::Arc<ksp_store_lib::Store>>> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let before = inner.routes.len();
inner.routes.retain(|slot| return slot.token() != token);
if inner.routes.len() == before {
return std::result::Result::Ok(std::option::Option::None);
}
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal);
if let std::option::Option::Some(status) = monitoring {
inner.last_monitoring.retain(|previous| return previous.profile_id != status.profile_id || previous.route_id != status.route_id);
inner.last_monitoring.push(status);
}
if inner.routes.is_empty() {
let store = inner.store.take();
if store.is_some() {
inner.store_closing_token = std::option::Option::Some(token);
} else {
inner.network = std::option::Option::None;
}
return std::result::Result::Ok(store);
}
return std::result::Result::Ok(std::option::Option::None);
}
fn complete_store_close(&self, token: u64) {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
if inner.store_closing_token == std::option::Option::Some(token) {
inner.store_closing_token = std::option::Option::None;
if inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() {
inner.network = std::option::Option::None;
}
}
}
/// Returns complete latest-value monitoring projections for active and recent terminal routes in the current runtime session.
pub(crate) fn monitoring_statuses(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let mut values = inner.last_monitoring.clone();
for slot in &inner.routes {
if let RouteRuntimeSlot::Active { handle, identity, .. } = slot {
let snapshot = handle.snapshot_source().current();
let runtime = identity.dto(project_worker_state(snapshot.worker_snapshot().state()));
let projected = crate::project_route_monitoring(&runtime, &snapshot);
let projected = match projected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
values.retain(|previous| return previous.profile_id != projected.profile_id || previous.route_id != projected.route_id);
values.push(projected);
}
}
return std::result::Result::Ok(values);
}
/// Closes route Start admission exactly once and requests cooperative Stop for all active or still-starting routes.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Ok(false);
}
inner.shutting_down = true;
for slot in &mut inner.routes {
match slot {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested = true,
RouteRuntimeSlot::Active { handle, .. } => {
let _accepted = handle.request_stop();
},
}
}
return std::result::Result::Ok(true);
}
/// Waits until all owned route Workers and the shared Store have completed bounded application-shutdown cleanup.
pub(crate) async fn shutdown_and_wait(&self) -> ksp_core_lib::Result<()> {
let begin = self.begin_shutdown();
if let std::result::Result::Err(error) = begin {
return std::result::Result::Err(error);
}
let started = std::time::Instant::now();
loop {
let complete = self.shutdown_complete();
let complete = match complete {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if complete {
return std::result::Result::Ok(());
}
if started.elapsed() >= ROUTE_SHUTDOWN_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk application shutdown did not release all route Workers and shared Store state before the bounded deadline",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn shutdown_complete(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
return std::result::Result::Ok(
inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() && inner.store_closing_token.is_none(),
);
}
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
let target = self.stop_target(request);
let target = match target {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (handle, identity, token) = match target {
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
RouteStopTarget::Starting { identity, token } => return self.wait_route_cleanup(token, &identity).await,
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
};
let _first_stop_request = handle.request_stop();
if let std::result::Result::Err(error) = handle.wait_terminal().await {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID, "Cannot observe terminal Raw Transaction Ingest Desk Worker state")
.with_source(error),
);
}
return self.wait_route_cleanup(token, &identity).await;
}
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let position = inner.routes.iter().position(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
if let std::option::Option::Some(position) = position {
return match &mut inner.routes[position] {
RouteRuntimeSlot::Active { handle, identity, token } => {
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
},
RouteRuntimeSlot::Starting { identity, stop_requested, token } => {
*stop_requested = true;
std::result::Result::Ok(RouteStopTarget::Starting { identity: identity.clone(), token: *token })
},
};
}
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
return match terminal {
std::option::Option::Some(value) if inner.store_closing_token.is_none() => std::result::Result::Ok(RouteStopTarget::Terminal(value.clone())),
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
)),
};
}
async fn wait_route_cleanup(&self, token: u64, identity: &RouteRuntimeIdentity) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let started = std::time::Instant::now();
loop {
let cleanup = self.route_cleanup_complete(token, identity);
let cleanup = match cleanup {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = cleanup {
return std::result::Result::Ok(value);
}
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk terminal cleanup did not release the selected route",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
async fn wait_terminal_store_cleanup(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let started = std::time::Instant::now();
loop {
let terminal = self.late_terminal_after_store_cleanup(request);
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = terminal {
return std::result::Result::Ok(value);
}
if started.elapsed() >= ROUTE_STOP_CLEANUP_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk final shared Store cleanup did not complete for the selected terminal route",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn late_terminal_after_store_cleanup(
&self,
request: &crate::RawIngestRouteStopRequestDto,
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
let terminal = match terminal {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk terminal route is no longer retained",
));
},
};
if inner.store_closing_token.is_some() {
return std::result::Result::Ok(std::option::Option::None);
}
return std::result::Result::Ok(std::option::Option::Some(terminal.clone()));
}
fn route_cleanup_complete(
&self,
token: u64,
identity: &RouteRuntimeIdentity,
) -> ksp_core_lib::Result<std::option::Option<crate::RawIngestRouteRuntimeDto>> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.routes.iter().any(|slot| return slot.token() == token) || inner.store_closing_token == std::option::Option::Some(token) {
return std::result::Result::Ok(std::option::Option::None);
}
let terminal = inner.last_terminals.iter().find(|value| return identity.matches_dto(value));
return std::result::Result::Ok(terminal.cloned());
}
}
/// Launch ownership returned only after shared Store readiness, Worker creation and route-handle installation have succeeded.
pub(crate) struct RouteRuntimeLaunch {
acknowledgement: crate::RawIngestRouteRuntimeDto,
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
token: u64,
}
impl crate::RouteRuntimeLaunch {
/// Returns the safe Start acknowledgement captured after the Worker handle was installed.
#[must_use]
pub(crate) fn acknowledgement(&self) -> crate::RawIngestRouteRuntimeDto {
return self.acknowledgement.clone();
}
/// Returns the complete safe logical route identity used to project Worker monitoring snapshots.
#[must_use]
pub(crate) fn monitoring_identity(&self) -> crate::RawIngestRouteRuntimeDto {
return self.acknowledgement.clone();
}
/// Returns an independent latest-value source for frontend-safe monitoring relay.
#[must_use]
pub(crate) fn monitoring_source(&self) -> ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource {
return self.handle.snapshot_source();
}
/// Waits for this Worker terminal state, removes only its route and closes Store only after the final same-network route terminates.
pub(crate) async fn monitor(self) {
let terminal = self.handle.wait_terminal().await;
let snapshot = self.handle.snapshot_source().current();
let worker = snapshot.worker_snapshot();
let snapshot_fault = worker.state().fault_code();
let snapshot_fault_domain = snapshot_fault.map_or("none", |code| return code.domain());
let snapshot_fault_code = snapshot_fault.map_or("none", |code| return code.code());
let terminal_state = match terminal {
std::result::Result::Ok(state) => {
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = self.acknowledgement.profile_id.as_str(),
network = self.acknowledgement.network.as_str(),
route_id = self.acknowledgement.route_id.as_str(),
commitment = self.acknowledgement.commitment.as_str(),
worker_state = state.code(),
worker_health = worker.health().code(),
worker_activity = worker.activity().code(),
fault_domain = snapshot_fault_domain,
fault_code = snapshot_fault_code,
source_failure_total = snapshot.source_failure_total(),
store_failure_total = snapshot.store_failure_total(),
content_conflict_total = snapshot.content_conflict_total(),
source_active = snapshot.source_active(),
source_reconnecting = snapshot.source_reconnecting(),
source_failed = snapshot.source_failed(),
open_gap_count = snapshot.open_gap_count(),
continuity_has_open_gaps = snapshot.continuity_has_open_gaps(),
future_target_coverage = snapshot.future_target_coverage(),
failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled(),
"Raw Transaction Ingest Desk route Worker reached terminal state"
);
project_worker_state(state)
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = self.acknowledgement.profile_id.as_str(),
network = self.acknowledgement.network.as_str(),
route_id = self.acknowledgement.route_id.as_str(),
commitment = self.acknowledgement.commitment.as_str(),
error_domain = error.code().domain(),
error_code = error.code().code(),
worker_health = worker.health().code(),
snapshot_fault_domain = snapshot_fault_domain,
snapshot_fault_code = snapshot_fault_code,
source_failure_total = snapshot.source_failure_total(),
store_failure_total = snapshot.store_failure_total(),
content_conflict_total = snapshot.content_conflict_total(),
"Raw Transaction Ingest Desk terminal Worker wait failed"
);
crate::RawIngestRouteState::Faulted
},
};
let mut terminal = self.acknowledgement;
terminal.state = terminal_state;
let monitoring = crate::project_route_monitoring(&terminal, &snapshot);
let monitoring = match monitoring {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not retain terminal route monitoring projection"
);
std::option::Option::None
},
};
let store = self.runtime_state.finish(self.token, terminal, monitoring);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk route cleanup state update failed"
);
return;
},
};
if let std::option::Option::Some(store) = store {
let close = close_store_arc(store).await;
if let std::result::Result::Err(error) = close {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk shared Store shutdown failed after final Worker"
);
}
self.runtime_state.complete_store_close(self.token);
}
}
}
/// Starts one exact route Worker and reuses the same ready Store for any already-active same-network route.
pub(crate) async fn start_route_runtime(
runtime_state: std::sync::Arc<crate::RouteRuntimeState>,
prepared: crate::PreparedRouteStart,
) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
let reservation = match runtime_state.reserve(&prepared) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = if reservation.needs_store_open {
let store = ksp_store_lib::Store::open(prepared.store_settings).await;
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _rollback = runtime_state.rollback(reservation.token);
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot open Raw Transaction Ingest Desk shared Store runtime")
.with_source(error),
);
},
};
let health = store.health().await;
if health.state() != ksp_store_lib::StoreHealthState::Ready {
let close = store.close().await;
let _rollback = runtime_state.rollback(reservation.token);
if let std::result::Result::Err(error) = close {
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Cannot close non-ready Raw Transaction Ingest Desk shared Store runtime",
)
.with_source(error),
);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_NOT_READY,
"Raw Transaction Ingest Desk shared Store readiness probe did not prove Ready",
));
}
let store = std::sync::Arc::new(store);
if let std::result::Result::Err(error) = runtime_state.install_store(reservation.token, std::sync::Arc::clone(&store)) {
let close = close_store_arc(store).await;
let _rollback = runtime_state.rollback(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
return std::result::Result::Err(error);
}
store
} else {
std::mem::drop(prepared.store_settings);
match runtime_state.shared_store(prepared.network.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _rollback = runtime_state.rollback(reservation.token);
return std::result::Result::Err(error);
},
}
};
let network = store.runtime_snapshot().network().clone();
let settings = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSettings::with_defaults(network, reservation.worker_id.clone());
let handle = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestWorker::start_with_runtime_resources(
settings,
std::sync::Arc::clone(&store),
prepared.runtime_resources,
);
let handle = match handle {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let cleanup_store = runtime_state.rollback(reservation.token);
let cleanup_store = match cleanup_store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
};
std::mem::drop(store);
if let std::option::Option::Some(value) = cleanup_store {
let close = close_store_arc(value).await;
runtime_state.complete_store_close(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
}
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_RUNTIME_START_FAILED, "Cannot start Raw Transaction Ingest Desk route Worker runtime")
.with_source(error),
);
},
};
let acknowledgement = runtime_state.activate(&reservation, handle.clone());
let acknowledgement = match acknowledgement {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let _stop_requested = handle.request_stop();
let _terminal = handle.wait_terminal().await;
let cleanup_store = runtime_state.rollback(reservation.token);
let cleanup_store = match cleanup_store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(rollback_error) => return std::result::Result::Err(rollback_error),
};
std::mem::drop(store);
if let std::option::Option::Some(value) = cleanup_store {
let close = close_store_arc(value).await;
runtime_state.complete_store_close(reservation.token);
if let std::result::Result::Err(close_error) = close {
return std::result::Result::Err(close_error);
}
}
return std::result::Result::Err(error);
},
};
std::mem::drop(store);
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
profile_id = reservation.identity.profile_id.as_str(),
network = reservation.identity.network.as_str(),
route_id = reservation.identity.route_id.as_str(),
commitment = reservation.identity.commitment.as_str(),
"Raw Transaction Ingest Desk independent route Worker handle installed on shared Store"
);
return std::result::Result::Ok(crate::RouteRuntimeLaunch { acknowledgement, handle, runtime_state, token: reservation.token });
}
#[derive(Clone)]
struct RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment,
inventory_generation: u32,
network: String,
profile_id: String,
route_id: crate::RawIngestRouteId,
}
impl RouteRuntimeIdentity {
fn dto(&self, state: crate::RawIngestRouteState) -> crate::RawIngestRouteRuntimeDto {
return crate::RawIngestRouteRuntimeDto {
commitment: self.commitment,
inventory_generation: self.inventory_generation,
network: self.network.clone(),
profile_id: self.profile_id.clone(),
route_id: self.route_id,
state,
};
}
fn matches_dto(&self, value: &crate::RawIngestRouteRuntimeDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id;
}
fn matches_monitoring(&self, value: &crate::RawIngestRouteMonitoringDto) -> bool {
return self.profile_id == value.profile_id && self.route_id == value.route_id;
}
}
enum RouteStopTarget {
Active {
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
identity: RouteRuntimeIdentity,
token: u64,
},
Starting {
identity: RouteRuntimeIdentity,
token: u64,
},
Terminal(crate::RawIngestRouteRuntimeDto),
TerminalClosing,
}
struct RouteRuntimeReservation {
identity: RouteRuntimeIdentity,
needs_store_open: bool,
token: u64,
worker_id: ksp_worker_api::WorkerId,
}
enum RouteRuntimeSlot {
Starting {
identity: RouteRuntimeIdentity,
stop_requested: bool,
token: u64,
},
Active {
handle: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
identity: RouteRuntimeIdentity,
token: u64,
},
}
impl RouteRuntimeSlot {
fn matches_identity(&self, profile_id: &str, route_id: crate::RawIngestRouteId) -> bool {
let identity = match self {
Self::Starting { identity, .. } | Self::Active { identity, .. } => identity,
};
return identity.profile_id == profile_id && identity.route_id == route_id;
}
fn starting_token(&self) -> std::option::Option<u64> {
return match self {
Self::Starting { token, .. } => std::option::Option::Some(*token),
Self::Active { .. } => std::option::Option::None,
};
}
fn cancelled_start_terminal(&self) -> std::option::Option<crate::RawIngestRouteRuntimeDto> {
return match self {
Self::Starting { identity, stop_requested: true, .. } => std::option::Option::Some(identity.dto(crate::RawIngestRouteState::Stopped)),
Self::Starting { stop_requested: false, .. } | Self::Active { .. } => std::option::Option::None,
};
}
fn token(&self) -> u64 {
return match self {
Self::Starting { token, .. } | Self::Active { token, .. } => *token,
};
}
}
async fn close_store_arc(store: std::sync::Arc<ksp_store_lib::Store>) -> ksp_core_lib::Result<()> {
let started = std::time::Instant::now();
let mut store = store;
loop {
match std::sync::Arc::try_unwrap(store) {
std::result::Result::Ok(value) => {
let close = value.close().await;
return match close {
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_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Cannot close Raw Transaction Ingest Desk shared Store runtime",
)
.with_source(error),
),
};
},
std::result::Result::Err(shared) => store = shared,
}
if started.elapsed() >= STORE_RECLAIM_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk shared Store remained referenced after final Worker",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn project_worker_state(state: ksp_worker_api::WorkerState) -> crate::RawIngestRouteState {
return match state {
ksp_worker_api::WorkerState::Created | ksp_worker_api::WorkerState::Starting => crate::RawIngestRouteState::Starting,
ksp_worker_api::WorkerState::Running => crate::RawIngestRouteState::Running,
ksp_worker_api::WorkerState::Stopping => crate::RawIngestRouteState::Stopping,
ksp_worker_api::WorkerState::Stopped => crate::RawIngestRouteState::Stopped,
ksp_worker_api::WorkerState::Faulted(_) => crate::RawIngestRouteState::Faulted,
_ => crate::RawIngestRouteState::Faulted,
};
}
fn runtime_lock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Raw Transaction Ingest Desk multi-route runtime state lock is poisoned");
}
#[cfg(test)]
#[path = "../unit_tests/route_runtime.rs"]
mod tests;

View File

@@ -0,0 +1,518 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
// version: 5
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
/// Fully revalidated Start preparation retaining backend-only Store settings and exact Worker runtime resources.
pub(crate) struct PreparedRouteStart {
/// Confirmed/finalized commitment validated by the exact route constructor.
pub(crate) commitment: crate::RawIngestCommitment,
/// Inventory generation proven current while this preparation was built.
pub(crate) inventory_generation: u32,
/// Logical network proven identical across Store and Transport.
pub(crate) network: String,
/// Logical network-profile identifier selected by the operator.
pub(crate) profile_id: String,
/// Logical route selected by the operator.
pub(crate) route_id: crate::RawIngestRouteId,
/// Backend-neutral Store settings resolved from Config and retained only in Rust.
pub(crate) store_settings: ksp_store_lib::StoreSettings,
/// Exact single-route Worker runtime resources reconstructed from typed Transport contracts.
pub(crate) runtime_resources: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources,
}
/// Revalidates one Start request and retains the exact backend resources required for a later Worker launch.
pub(crate) fn prepare_route_start(
management: &ksp_config_lib::ConfigManagement,
current_generation: u32,
request: &crate::RawIngestRouteStartRequestDto,
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
if current_generation == 0 || request.inventory_generation != current_generation {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,
"Raw Transaction Ingest Desk Start request uses a stale route inventory generation",
));
}
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Config environment", error),
};
let inventory = crate::build_route_inventory_with_environment(management, &environment, current_generation);
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot rebuild Start-time route inventory", error),
};
let profile = inventory.profiles.iter().find(|profile| return profile.profile_id == request.profile_id);
let profile = match profile {
std::option::Option::Some(value) => value,
std::option::Option::None => return route_unavailable_error(),
};
let route = profile.routes.iter().find(|route| return route.route_id == request.route_id);
let route = match route {
std::option::Option::Some(value) if value.selectable && value.state == crate::RawIngestRouteState::Configured => value,
std::option::Option::Some(_) | std::option::Option::None => return route_unavailable_error(),
};
let network = match route.network.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return route_unavailable_error(),
};
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(request.profile_id.as_str()));
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time composite profile", error),
};
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let store_profile = match store_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store profile", error),
};
let store = management.engine().resolve_store_config_profile(&store_profile, &environment);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time Store settings", error),
};
if store.settings().network().as_str() != network {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk Start-time Store network no longer matches the selected route",
));
}
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let transport_profile = match transport_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport profile", error),
};
let transport = management.engine().resolve_transport_config_profile(&transport_profile, &environment);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot resolve Start-time base Transport settings", error),
};
if !transport_matches_network(&transport, network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk Start-time Transport network no longer matches the selected route",
));
}
let commitment = request.commitment.into_transport();
let runtime_resources = match request.route_id {
crate::RawIngestRouteId::YellowstoneHydrated => prepare_yellowstone(management, &environment, &composite, network, commitment),
crate::RawIngestRouteId::StandardLogsHydrated => prepare_standard_logs(&transport, network, commitment),
crate::RawIngestRouteId::StandardBlockDirect => prepare_standard_block(&transport, network, commitment),
crate::RawIngestRouteId::HeliusTransactionHydrated => prepare_helius(management, &environment, &composite, &transport, network, commitment),
crate::RawIngestRouteId::HttpBlockPolling => prepare_http_polling(&transport, network, commitment),
};
let runtime_resources = match runtime_resources {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return route_start_error("Cannot reconstruct Start-time Transport resources", error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_START,
profile_id = request.profile_id.as_str(),
network,
route_id = request.route_id.as_str(),
inventory_generation = request.inventory_generation,
commitment = request.commitment.as_str(),
"revalidated Start request and reconstructed exact Store/Transport-owned Worker launch resources"
);
return std::result::Result::Ok(crate::PreparedRouteStart {
commitment: request.commitment,
inventory_generation: request.inventory_generation,
network: network.to_owned(),
profile_id: request.profile_id.clone(),
route_id: request.route_id,
store_settings: store.into_settings(),
runtime_resources,
});
}
/// Revalidates one future Start request and destroys the reconstructed resources without opening Store or launching a Worker.
pub(crate) fn validate_route_start(
management: &ksp_config_lib::ConfigManagement,
current_generation: u32,
request: &crate::RawIngestRouteStartRequestDto,
) -> ksp_core_lib::Result<crate::RawIngestRouteStartValidationDto> {
let prepared = crate::prepare_route_start(management, current_generation, request);
let prepared = match prepared {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::RawIngestRouteStartValidationDto {
commitment: prepared.commitment,
inventory_generation: prepared.inventory_generation,
network: prepared.network,
profile_id: prepared.profile_id,
route_id: prepared.route_id,
});
}
fn prepare_standard_logs(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let endpoint =
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = single_http_role(transport.http_settings(), network, &["getTransaction"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardLogsSource::new(
endpoint,
ksp_onchain_transport_lib::SolanaLogsSubscribeFilter::AllWithVotes,
commitment,
pool,
role,
);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_logs_source(source));
}
fn prepare_standard_block(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let endpoint =
single_ws_endpoint(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestStandardBlockSource::new(
endpoint,
ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All,
commitment,
);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_standard_block_source(source));
}
fn prepare_helius(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
base_transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_HELIUS, network);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = single_ws_endpoint(
&transport,
network,
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = single_http_role(base_transport.http_settings(), network, &["getTransaction"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(base_transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHeliusTransactionSource::new(endpoint, filter, commitment, pool, role);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_helius_transaction_source(source));
}
fn prepare_yellowstone(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let transport = resolve_optional_transport(management, environment, composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT_YELLOWSTONE, network);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = single_grpc_endpoint(&transport, network);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
let channel = match channel {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("raw-ingest-blocks");
let filter_name = match filter_name {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut block_filter = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new();
block_filter.set_include_transactions(std::option::Option::Some(false));
block_filter.set_include_accounts(std::option::Option::Some(false));
block_filter.set_include_entries(std::option::Option::Some(false));
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let inserted = request.insert_block_filter(filter_name, block_filter);
if let std::result::Result::Err(error) = inserted {
return std::result::Result::Err(error);
}
request.set_commitment(std::option::Option::Some(commitment));
let role = single_http_role(transport.http_settings(), network, &["getBlock"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestYellowstoneSource::new(channel, request, pool, role);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::new(source));
}
fn prepare_http_polling(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
commitment: ksp_onchain_transport_lib::SolanaCommitment,
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources> {
let role = single_http_role(transport.http_settings(), network, &["getSlot", "getBlocksWithLimit", "getBlock"]);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.http_settings().clone());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHttpBlockPollingSource::new(pool, role, commitment);
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRuntimeResources::from_http_block_polling_source(source));
}
fn resolve_optional_transport(
management: &ksp_config_lib::ConfigManagement,
environment: &ksp_config_lib::ConfigEnvironment,
composite: &ksp_config_lib::ResolvedConfigComposite,
component_id: &str,
network: &str,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig> {
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_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk selected route has no declared Transport source",
));
},
};
if component.resolved().file_id().as_str() != ksp_config_lib::FILE_ID_STD_TRANSPORT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk selected route references an unexpected Transport document",
));
}
let resolved = management.engine().resolve_transport_config_profile(component.resolved(), environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !transport_matches_network(&resolved, network) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"Raw Transaction Ingest Desk provider Transport network no longer matches the selected route",
));
}
return std::result::Result::Ok(resolved);
}
fn single_ws_endpoint(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
protocol: ksp_onchain_transport_lib::WsProtocolKind,
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsEndpointSettings> {
let settings = match transport.ws_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one WebSocket endpoint but none is configured"),
};
let matching = settings
.endpoints()
.iter()
.filter(|endpoint| {
return endpoint.enabled()
&& endpoint.cluster().as_str() == network
&& endpoint.protocol() == protocol
&& endpoint.supports_subscription(capability);
})
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one matching WebSocket endpoint in its selected Transport profile");
}
return std::result::Result::Ok(matching[0].clone());
}
fn single_grpc_endpoint(
transport: &ksp_config_lib::ResolvedTransportConfig,
network: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
let settings = match transport.grpc_settings() {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route requires one Yellowstone gRPC endpoint but none is configured"),
};
let matching = settings
.endpoints()
.iter()
.filter(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network)
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one matching Yellowstone gRPC endpoint in its selected Transport profile");
}
return std::result::Result::Ok(matching[0].clone());
}
fn single_http_role(
settings: &ksp_onchain_transport_lib::HttpTransportSettings,
network: &str,
methods: &[&'static str],
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
for method_name in methods {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
let descriptor = match descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => return ambiguous_resource_error("Start-time route references an unknown HTTP method requirement"),
};
request_kinds.push(descriptor.request_kind());
}
let mut roles = std::collections::BTreeSet::<String>::new();
for endpoint in settings.endpoints() {
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
roles.insert(role.role().as_str().to_owned());
}
}
}
let matching = roles
.into_iter()
.filter(|role_name| {
return request_kinds.iter().all(|request_kind| return http_role_supports_request_kind(settings, network, role_name.as_str(), request_kind));
})
.collect::<std::vec::Vec<_>>();
if matching.len() != 1 {
return ambiguous_resource_error("Start-time route requires exactly one logical HTTP role satisfying its complete method set");
}
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(matching[0].clone()));
}
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, network: &str, role_name: &str, request_kind: &str) -> bool {
for endpoint in settings.endpoints() {
if !endpoint.enabled() || endpoint.cluster().as_str() != network {
continue;
}
for role in endpoint.roles() {
if !role.enabled() || role.role().as_str() != role_name {
continue;
}
if role.request_kinds().iter().any(|kind| return kind.is_wildcard() || kind.as_str() == request_kind) {
return true;
}
}
}
return false;
}
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
let mut networks = std::collections::BTreeSet::<String>::new();
for endpoint in transport.http_settings().endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
if let std::option::Option::Some(settings) = transport.ws_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
if let std::option::Option::Some(settings) = transport.grpc_settings() {
for endpoint in settings.endpoints() {
if endpoint.enabled() {
networks.insert(endpoint.cluster().as_str().to_owned());
}
}
}
return networks.len() == 1 && networks.contains(network);
}
fn route_unavailable_error<T>() -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_UNAVAILABLE,
"Raw Transaction Ingest Desk selected route is no longer Configured in the current Start-time inventory",
));
}
fn ambiguous_resource_error<T>(message: &'static str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message));
}
fn route_start_error<T>(message: &'static str, source: ksp_core_lib::Error) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, message).with_source(source));
}
#[cfg(test)]
#[path = "../unit_tests/route_start.rs"]
mod tests;

View File

@@ -0,0 +1,214 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/splash.rs
// version: 1
//! Common splash settings and frontend event contracts for Raw Transaction Ingest 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_raw_transaction_ingest_desk/splash/SplashOrderDto.ts")]
pub(crate) struct SplashOrderDto {
/// Splash action name.
pub(crate) action: String,
/// Optional status text.
pub(crate) message: std::option::Option<String>,
/// Optional message severity used by the bottom message feed.
pub(crate) status: std::option::Option<String>,
/// Optional animation duration in milliseconds.
pub(crate) duration_ms: std::option::Option<u32>,
}
impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self {
action: action.to_owned(),
message: message.map(std::string::ToString::to_string),
status: status.map(std::string::ToString::to_string),
duration_ms,
};
}
}
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
let parsed = value.parse::<u32>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
#[cfg(test)]
#[path = "../unit_tests/splash.rs"]
mod tests;

View File

@@ -0,0 +1,318 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 9
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Runs the Raw Transaction Ingest 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 = builder.manage(app_state);
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
builder = configure_commands(builder);
builder = configure_window_events(builder);
builder = 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, "Raw Transaction Ingest Desk Tauri shell setup completed");
return std::result::Result::Ok(());
});
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 Raw Transaction Ingest 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),
),
};
}
#[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![
emit_frontend_log,
get_route_foundation,
get_route_inventory,
get_route_monitoring,
get_runtime_status,
splash_frontend_ready,
start_route,
stop_route,
validate_route_start
]);
}
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>();
let begin = state.begin_shutdown();
let begin = match begin {
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(),
"Raw Transaction Ingest Desk could not enter graceful shutdown; exiting after safe error projection"
);
app_handle.exit(1);
return;
},
};
if !begin {
return;
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk main-window close requested; route Start admission closed and cooperative shutdown requested"
);
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<crate::AppState>();
let shutdown = state.shutdown_routes().await;
match shutdown {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk route and shared Store shutdown completed; exiting application"
);
app_handle.exit(0);
},
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(),
"Raw Transaction Ingest Desk bounded route shutdown failed; exiting application after bounded cleanup attempt"
);
app_handle.exit(1);
},
}
});
}
});
}
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(),
"Raw Transaction Ingest Desk Tauri command failed"
);
return crate::CommandErrorDto::from_error(error);
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
};
}
#[tauri::command]
fn get_route_foundation(state: tauri::State<'_, crate::AppState>) -> crate::RawIngestRouteFoundationDto {
return state.route_foundation();
}
#[tauri::command]
fn get_route_inventory(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RawIngestRouteInventoryDto, crate::CommandErrorDto> {
let result = state.route_inventory();
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_route_inventory", crate::TRACING_DOMAIN_INVENTORY, &error)),
};
}
#[tauri::command]
fn get_route_monitoring(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::RawIngestRouteMonitoringDto>, crate::CommandErrorDto> {
let result = state.route_monitoring();
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_route_monitoring", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &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)),
};
}
#[tauri::command]
async fn start_route(
app: tauri::AppHandle,
request: crate::RawIngestRouteStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
let launch = state.start_route(&request).await;
let launch = match launch {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(project_command_error("start_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error));
},
};
let acknowledgement = launch.acknowledgement();
let monitoring_identity = launch.monitoring_identity();
let monitoring_source = launch.monitoring_source();
let monitoring_app = app.clone();
let monitoring_task = tauri::async_runtime::spawn(async move {
monitor_route_status(monitoring_app, monitoring_identity, monitoring_source).await;
});
std::mem::drop(monitoring_task);
let terminal_task = tauri::async_runtime::spawn(async move {
launch.monitor().await;
});
std::mem::drop(terminal_task);
return std::result::Result::Ok(acknowledgement);
}
const ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500;
async fn monitor_route_status(
app: tauri::AppHandle,
runtime: crate::RawIngestRouteRuntimeDto,
source: ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSnapshotSource,
) {
let mut snapshot = source.current();
loop {
let projected = crate::project_route_monitoring(&runtime, &snapshot);
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::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status);
if emitted.is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
"Raw Transaction Ingest Desk could not emit latest-value route monitoring status"
);
}
}
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_ROUTE_RUNTIME,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not project latest-value route monitoring status"
);
},
}
if snapshot.worker_snapshot().state().is_terminal() {
return;
}
let observed = snapshot.worker_snapshot().sequence();
snapshot = source.wait_for_change(observed).await;
if !snapshot.worker_snapshot().state().is_terminal() {
tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await;
snapshot = source.current();
}
}
}
#[tauri::command]
async fn stop_route(
request: crate::RawIngestRouteStopRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteRuntimeDto, crate::CommandErrorDto> {
let result = state.stop_route(&request).await;
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("stop_route", crate::TRACING_DOMAIN_ROUTE_RUNTIME, &error)),
};
}
#[tauri::command]
async fn validate_route_start(
request: crate::RawIngestRouteStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::RawIngestRouteStartValidationDto, crate::CommandErrorDto> {
let result = state.validate_route_start(&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("validate_route_start", crate::TRACING_DOMAIN_ROUTE_START, &error)),
};
}

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_main.rs
// version: 1
//! Tauri-window helpers for the Raw Transaction Ingest 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, "Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest Desk main window")
.with_context("window_label", WINDOW_LABEL_MAIN)
.with_source(error),
);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_MAIN,
"main window shown and focused"
);
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,173 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tw_splash.rs
// version: 1
//! Tauri-window lifecycle for the Raw Transaction Ingest 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, "Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest 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 Raw Transaction Ingest Desk splash order")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_context("action", action)
.with_source(error),
),
};
}

View File

@@ -0,0 +1,73 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Raw Transaction Ingest Desk",
"identifier": "com.sasedev.ksp-app-raw-transaction-ingest-desk",
"build": {
"beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1440",
"beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-raw-transaction-ingest-desk/dist"
},
"app": {
"windows": [
{
"label": "splash",
"url": "splash.html",
"title": "Chargement — KSP Raw Transaction Ingest Desk",
"width": 960,
"height": 637,
"resizable": false,
"decorations": false,
"transparent": true,
"center": true,
"alwaysOnTop": true
},
{
"label": "main",
"url": "main.html",
"title": "KSP Raw Transaction Ingest 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-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-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"
}
}
}

View File

@@ -0,0 +1,98 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/dependency_boundary.rs
// version: 5
//! Dependency-boundary canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
#![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_008_manifest_adds_store_facade_and_worker_api_without_physical_backend_edge() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
for required in [
"ksp-config-lib",
"ksp-core-lib",
"ksp-logging-lib",
"ksp-onchain-transport-lib",
"ksp-store-lib",
"ksp-worker-api",
"ksp-worker-raw-transaction-ingest-lib",
"tauri-plugin-tracing",
] {
assert!(manifest.contains(required), "missing pre.008 dependency {required}");
}
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "ksp-job-backfill-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
assert!(!manifest.contains(forbidden), "pre.008 opens forbidden direct dependency {forbidden}");
}
}
#[test]
fn pre_007_route_start_reconstructs_exact_worker_sources_without_network_store_or_worker_launch() {
let source = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"HttpTransportPool::new",
"YellowstoneGrpcChannel::prepare",
"RawTransactionIngestYellowstoneSource::new",
"RawTransactionIngestStandardLogsSource::new",
"RawTransactionIngestStandardBlockSource::new",
"RawTransactionIngestHeliusTransactionSource::new",
"RawTransactionIngestHttpBlockPollingSource::new",
] {
assert!(source.contains(required), "missing exact resource constructor {required}");
}
for forbidden in [
"YellowstoneGrpcChannel::connect",
"SolanaStandardWsSession::connect",
"HeliusWsSession::connect",
"Store::open",
"RawTransactionIngestWorker::",
"start_with_runtime_resources",
"request_stop",
] {
assert!(!source.contains(forbidden), "pre.007 performs premature runtime operation {forbidden}");
}
}
#[test]
fn pre_008_route_runtime_owns_real_store_worker_start_stop_without_physical_backend_types() {
let source = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"Store::open",
"StoreHealthState::Ready",
"RawTransactionIngestWorker::start_with_runtime_resources",
"request_stop",
"wait_terminal",
".close().await",
"Arc::try_unwrap",
] {
assert!(source.contains(required), "missing mono-route runtime operation {required}");
}
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "PostgresStore", "connection_uri", "endpoint_url"] {
assert!(!source.contains(forbidden), "pre.008 crosses physical or secret boundary {forbidden}");
}
}
#[test]
fn pre_010_monitoring_projects_existing_worker_snapshot_facade_without_new_backend_edges() {
let source = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in ["RawTransactionIngestSnapshot", "worker_snapshot()", "source_reconnect_total()", "source_replay_attempt_total()", "gaps()"] {
assert!(source.contains(required), "missing Worker snapshot facade marker {required}");
}
for forbidden in ["tokio_postgres", "ksp_store_postgres_lib", "reqwest", "tonic", "yellowstone_grpc_client"] {
assert!(!source.contains(forbidden), "monitoring opens forbidden backend edge {forbidden}");
}
}

View File

@@ -0,0 +1,343 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 13
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
#![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_005_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() {
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
assert!(config.is_ok());
let config = match config {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(config.is_ok());
if let std::result::Result::Ok(config) = config {
assert_eq!(config["productName"], "KSP Raw Transaction Ingest Desk");
assert_eq!(config["identifier"], "com.sasedev.ksp-app-raw-transaction-ingest-desk");
assert_eq!(config["build"]["devUrl"], "http://localhost:1440");
}
let vite = read_text(app_root().join("vite.config.ts").as_path());
assert!(vite.contains("port: 1440"));
assert!(vite.contains("strictPort: true"));
assert!(vite.contains("port: 1441"));
}
#[test]
fn pre_005_rust_package_is_mixed_lib_bin_and_workspace_member() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
assert!(manifest.contains("name = \"ksp-app-raw-transaction-ingest-desk\""));
assert!(manifest.contains("name = \"ksp_app_raw_transaction_ingest_desk_lib\""));
assert!(manifest.contains("crate-type = [\"staticlib\", \"cdylib\", \"rlib\"]"));
let workspace = read_text(app_root().join("../../Cargo.toml").as_path());
assert!(workspace.contains("crates/ksp-app-raw-transaction-ingest-desk"));
}
#[test]
fn pre_005_package_dependencies_match_current_desk_baseline() {
let package = std::fs::read_to_string(app_root().join("package.json"));
assert!(package.is_ok());
let package = match package {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(package.is_ok());
if let std::result::Result::Ok(package) = package {
let dependencies = &package["dependencies"];
for dependency in
["@fltsci/tauri-plugin-tracing", "@fortawesome/fontawesome-free", "@tauri-apps/api", "bootstrap", "resize-observer-polyfill", "simplebar"]
{
assert!(dependencies.get(dependency).is_some(), "missing frontend dependency {dependency}");
}
assert!(dependencies.get("datatables.net-bs5").is_none());
let development = &package["devDependencies"];
for dependency in ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"] {
assert!(development.get(dependency).is_some(), "missing frontend development dependency {dependency}");
}
}
}
#[test]
fn pre_005_packaging_contains_current_sixteen_config_resources() {
let config = std::fs::read_to_string(app_root().join("tauri.conf.json"));
assert!(config.is_ok());
let config = match config {
std::result::Result::Ok(value) => serde_json::from_str::<serde_json::Value>(value.as_str()),
std::result::Result::Err(_) => return,
};
assert!(config.is_ok());
if let std::result::Result::Ok(config) = config {
let resources = config["bundle"]["resources"].as_object();
assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 16);
assert!(resources.contains_key("../../config/composite.ksp-app-raw-transaction-ingest-desk.json"));
}
}
}
#[test]
fn pre_005_splash_assets_match_existing_ksp_desk_template_byte_exact() {
let reference = app_root().join("../ksp-app-backfill-desk");
for relative in ["icons/favicon.ico", "icons/favicon.png", "frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png"] {
let current = std::fs::read(app_root().join(relative));
let expected = std::fs::read(reference.join(relative));
assert!(current.is_ok());
assert!(expected.is_ok());
if let (std::result::Result::Ok(current), std::result::Result::Ok(expected)) = (current, expected) {
assert_eq!(current, expected, "binary template asset differs: {relative}");
}
}
}
#[test]
fn pre_006_route_inventory_is_backend_owned_and_keeps_exact_five_ids_and_seven_states() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for route in ["YellowstoneHydrated", "StandardLogsHydrated", "StandardBlockDirect", "HeliusTransactionHydrated", "HttpBlockPolling"] {
assert!(dto.contains(route));
}
for state in ["Unavailable", "Configured", "Starting", "Running", "Stopping", "Stopped", "Faulted"] {
assert!(dto.contains(state));
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("get_route_foundation"));
assert!(tauri.contains("get_route_inventory"));
for forbidden in ["start_worker", "stop_worker", "RawTransactionIngestWorker", "Store::open"] {
assert!(!tauri.contains(forbidden), "scaffold Tauri surface contains premature runtime marker {forbidden}");
}
}
#[test]
fn pre_005_fix_002_shell_reuses_common_light_template_and_bounds_route_foundation() {
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in [
"<header class=\"app-header\">",
"navbar h-100 py-0 bg-light text-dark",
"col-auto app-sidebar border-end bg-body-tertiary",
"col app-content app-scrollable h-100",
"card app-shell-card mx-auto shadow-sm border-0",
"id=\"routeFoundation\" class=\"row g-3 app-route-grid\"",
] {
assert!(html.contains(required), "missing common shell marker: {required}");
}
for forbidden in ["app-header bg-dark", "app-sidebar bg-dark text-light", "app-content flex-grow-1"] {
assert!(!html.contains(forbidden), "obsolete shell marker remains: {forbidden}");
}
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
for required in [
".app-scrollable {",
".app-shell-card {",
".app-route-grid {",
".app-route-grid > * {",
"min-width: 0;",
"max-width: 100%;",
"overflow: hidden;",
"overflow-wrap: anywhere;",
] {
assert!(styles.contains(required), "missing bounded layout marker: {required}");
}
}
#[test]
fn pre_006_frontend_projects_profile_network_generation_and_safe_route_reasons() {
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["id=\"routeProfile\"", "id=\"routeNetwork\"", "id=\"routeInventoryGeneration\"", "id=\"routeFoundation\""] {
assert!(html.contains(required), "missing route inventory UI marker: {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["get_route_inventory", "missing_ws_block_capability", "missing_required_secret", "Config route inventory"] {
assert!(main.contains(required), "missing route inventory frontend marker: {required}");
}
for forbidden in ["endpointUrl", "connectionUri", "apiKey", "sourceKey", "x-token"] {
assert!(!main.contains(forbidden), "frontend exposes forbidden physical marker: {forbidden}");
}
}
#[test]
fn pre_007_start_preflight_is_backend_owned_generation_bound_and_does_not_launch_worker() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in
["RawIngestCommitment", "RawIngestRouteStartRequestDto", "inventory_generation", "profile_id", "route_id", "RawIngestRouteStartValidationDto"]
{
assert!(dto.contains(required), "missing pre.007 Start DTO marker: {required}");
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("validate_route_start"));
for forbidden in ["start_worker", "stop_worker", "start_with_runtime_resources", "Store::open"] {
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface contains premature runtime marker {forbidden}");
}
}
#[test]
fn pre_009_start_stop_controls_are_multi_route_targeted_and_backend_owned() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["start_route", "stop_route", "RawIngestRouteStopRequestDto", "launch.monitor().await"] {
assert!(tauri.contains(required), "missing pre.009 Tauri runtime marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["id=\"routeCommitment\"", "id=\"routeRuntimeState\"", "id=\"routeRuntimeIdentity\"", "Runtime multi-route / Store partagé"] {
assert!(html.contains(required), "missing pre.009 frontend runtime marker {required}");
}
assert!(!html.contains("id=\"stopRoute\""));
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["activeRuntimes", "targeted route Stop requested", "start_route", "stop_route", "RawIngestRouteStopRequestDto"] {
assert!(main.contains(required), "missing pre.009 frontend control marker {required}");
}
}
#[test]
fn pre_010_runtime_monitoring_is_backend_owned_event_driven_and_resynchronizable() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"get_route_monitoring",
"monitor_route_status",
"window.emit(crate::RAW_INGEST_ROUTE_STATUS_EVENT_NAME, status)",
"source.wait_for_change(observed).await",
] {
assert!(tauri.contains(required), "missing pre.010 monitoring bridge marker {required}");
}
let state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(state.contains("route_monitoring(&self)"));
assert!(state.contains("pre.010-runtime-monitoring"));
}
#[test]
fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supervision() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"listen<RawIngestRouteMonitoringDto>",
"ksp-raw-ingest-route-status",
"get_route_monitoring",
"routeMonitoring",
"syncRouteMonitoring",
"persistedTotal",
"sourceActive",
"openGapCount",
"repairTransactionHydrationTotal",
"route supervision selected",
"route monitoring resynchronization requested",
] {
assert!(main.contains(required), "missing pre.011 frontend monitoring marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in [
"id=\"refreshRouteMonitoring\"",
"id=\"routeMonitoringDetail\"",
"id=\"monitorPersistedTotal\"",
"id=\"monitorSourceCounts\"",
"id=\"monitorContinuityFrontier\"",
"id=\"monitorGapList\"",
"id=\"headerStore\"",
] {
assert!(html.contains(required), "missing pre.011 supervision UI marker {required}");
}
let styles = read_text(app_root().join("frontend/sass/_app.scss").as_path());
for required in [".app-route-monitoring-summary {", ".app-monitoring-detail {", ".app-monitoring-list dd", ".app-monitoring-gaps"] {
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
}
}
#[test]
fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_route_controls() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500",
"tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await",
"snapshot = source.current()",
] {
assert!(tauri.contains(required), "missing pre.011-fix.001 bounded relay marker {required}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"const routeCards = new Map<string, HTMLElement>()",
"renderRouteMonitoringCard",
"const lifecycleChanged = previous === null || previous.state !== status.state",
"if (lifecycleChanged || !renderRouteMonitoringCard(status))",
] {
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
}
}
#[test]
fn pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"configure_window_events(builder)",
"tauri::WindowEvent::CloseRequested",
"api.prevent_close()",
"state.begin_shutdown()",
"state.shutdown_routes().await",
"app_handle.exit(0)",
"app_handle.exit(1)",
] {
assert!(tauri.contains(required), "missing pre.012 bounded shutdown marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"shutting_down: bool",
"stop_requested: bool",
"ROUTE_SHUTDOWN_TIMEOUT",
"ensure_start_admission_open",
"shutdown_and_wait",
"RouteStopTarget::Starting",
"*stop_requested = true",
"handle.request_stop()",
] {
assert!(runtime.contains(required), "missing pre.012 route-race marker {required}");
}
let app_state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(app_state.contains("let prepared = {"));
assert!(app_state.contains("let generation = self.inventory_generation.lock()"));
assert!(!app_state.contains("std::mem::drop(generation)"));
assert!(app_state.contains("self.route_runtime.ensure_start_admission_open()"));
}
#[test]
fn pre_012_fix_002_terminal_fault_logging_keeps_safe_route_identity_and_worker_cause_counters() {
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
let start = runtime.find("pub(crate) async fn monitor(self)");
assert!(start.is_some(), "missing RouteRuntimeLaunch::monitor");
let std::option::Option::Some(start) = start else {
return;
};
let remainder = &runtime[start..];
let end = remainder.find("/// Starts one exact route Worker");
assert!(end.is_some(), "missing route runtime monitor end marker");
let std::option::Option::Some(end) = end else {
return;
};
let monitor = &remainder[..end];
for required in [
"worker_health = worker.health().code()",
"fault_domain = snapshot_fault_domain",
"fault_code = snapshot_fault_code",
"source_failure_total = snapshot.source_failure_total()",
"store_failure_total = snapshot.store_failure_total()",
"content_conflict_total = snapshot.content_conflict_total()",
"open_gap_count = snapshot.open_gap_count()",
"future_target_coverage = snapshot.future_target_coverage()",
"failed_source_losses_reconciled = snapshot.failed_source_losses_reconciled()",
] {
assert!(monitor.contains(required), "missing pre.012-fix.002 terminal diagnostic marker {required}");
}
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "signature", "canonical_bytes", "wire_bytes"] {
assert!(!monitor.contains(forbidden), "terminal diagnostic surface exposes forbidden marker {forbidden}");
}
}

View File

@@ -0,0 +1,225 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 9
//! Desktop security canaries for the Raw Transaction Ingest 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 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 collect_typescript(root: &std::path::Path, files: &mut std::vec::Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(root);
if let std::result::Result::Ok(entries) = entries {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_typescript(path.as_path(), files);
} else if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("ts") {
files.push(path);
}
}
}
}
#[test]
fn pre_005_capability_surface_is_core_plus_tracing_only() {
let capability = read_text(app_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));
}
}
#[test]
fn pre_005_frontend_has_no_network_persistence_or_native_dialog_surface() {
let mut files = std::vec::Vec::new();
collect_typescript(app_root().join("frontend/ts").as_path(), &mut files);
assert!(!files.is_empty());
for path in files {
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 frontend surface {forbidden}", path.display());
}
}
}
#[test]
fn pre_006_frontend_tracing_is_control_inventory_and_command_oriented_only() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("frontend control clicked"));
assert!(main.contains("navigation tab clicked"));
assert!(main.contains("runtime status refresh requested"));
assert!(main.contains("Config route inventory refresh requested"));
assert!(main.contains("logical network selected"));
let invoke = read_text(app_root().join("frontend/ts/invoke.ts").as_path());
assert!(invoke.contains("Frontend IPC command requested"));
for forbidden in ["apiKey", "authorization", "connectionUri", "endpointUrl", "sourceKey", "rawTransaction", "transactionPayload", "x-token"] {
assert!(!main.contains(forbidden), "frontend contains forbidden sensitive marker {forbidden}");
}
}
#[test]
fn pre_006_route_inventory_dto_surface_excludes_endpoint_secret_and_physical_store_fields() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in ["RawIngestRouteInventoryDto", "RawIngestProfileInventoryDto", "RawIngestRouteUnavailableReason"] {
assert!(dto.contains(required));
}
for forbidden in ["endpoint_name", "endpoint_url", "api_key", "connection_uri", "source_key", "secret_metadata", "worker_handle"] {
assert!(!dto.contains(forbidden), "route DTO contains forbidden field marker {forbidden}");
}
}
#[test]
fn pre_007_start_request_and_acknowledgement_are_logical_only_and_resource_free() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
for required in ["profile_id", "route_id", "inventory_generation", "commitment", "network"] {
assert!(dto.contains(required), "missing safe Start field marker {required}");
}
for forbidden in [
"endpoint_name",
"endpoint_url",
"api_key",
"connection_uri",
"source_key",
"secret_metadata",
"worker_handle",
"http_pool",
"ws_endpoint",
"yellowstone_channel",
] {
assert!(!dto.contains(forbidden), "Start DTO exposes forbidden physical marker {forbidden}");
}
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
assert!(route_start.contains("ERROR_CODE_ROUTE_START_STALE_INVENTORY"));
assert!(route_start.contains("build_route_inventory_with_environment"));
assert!(route_start.contains("resolve_store_config_profile"));
assert!(!route_start.contains("with_context(\"endpoint\""));
assert!(!route_start.contains("with_context(\"provider\""));
}
#[test]
fn pre_008_runtime_acknowledgement_and_frontend_tracing_remain_secret_and_handle_free() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.contains("RawIngestRouteRuntimeDto"));
for forbidden in ["worker_id", "worker_handle", "endpoint_url", "connection_uri", "api_key", "source_key"] {
assert!(!dto.contains(forbidden), "runtime DTO exposes forbidden marker {forbidden}");
}
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in ["profileId", "routeId", "commitment", "state"] {
assert!(main.contains(required), "missing safe runtime trace marker {required}");
}
for forbidden in ["endpointUrl", "connectionUri", "workerId", "sourceKey", "x-token", "rawTransaction"] {
assert!(!main.contains(forbidden), "frontend runtime trace exposes forbidden marker {forbidden}");
}
}
#[test]
fn pre_010_monitoring_projection_is_source_neutral_payload_free_and_javascript_exact() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"sequence: String",
"processing_frontier_slot: std::option::Option<String>",
"continuity_frontier_slot: std::option::Option<String>",
"fault_domain",
"fault_code",
"gaps: std::vec::Vec<crate::RawIngestRouteGapDto>",
] {
assert!(monitoring.contains(required), "missing pre.010 safe monitoring marker {required}");
}
for forbidden in ["endpoint_url", "connection_uri", "api_key", "source_key", "worker_id", "x-token"] {
assert!(!monitoring.contains(forbidden), "monitoring projection exposes forbidden physical marker {forbidden}");
}
let dto_start = monitoring.find("pub(crate) struct RawIngestRouteMonitoringDto");
assert!(dto_start.is_some(), "monitoring DTO declaration is missing");
let dto_start = match dto_start {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dto_end = monitoring[dto_start..].find("/// Projects one complete concrete Worker snapshot");
assert!(dto_end.is_some(), "monitoring DTO declaration boundary is missing");
let dto_end = match dto_end {
std::option::Option::Some(value) => dto_start + value,
std::option::Option::None => return,
};
let dto_surface = &monitoring[dto_start..dto_end];
for forbidden in ["raw_transaction", "signature", "payload", "canonical_bytes", "wire_bytes", "transaction_bytes"] {
assert!(!dto_surface.contains(forbidden), "monitoring DTO exposes forbidden RAW marker {forbidden}");
}
}
#[test]
fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_material() {
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"route supervision selected",
"route monitoring latest value applied",
"route monitoring resynchronization requested",
"route monitoring refresh button clicked",
"commitment selection changed",
"routeId",
"sequence",
"statusCount",
] {
assert!(main.contains(required), "missing pre.011 safe tracing marker {required}");
}
for forbidden in [
"endpointUrl",
"connectionUri",
"apiKey",
"sourceKey",
"workerId",
"x-token",
"rawTransaction",
"transactionPayload",
"canonicalBytes",
"wireBytes",
"transactionBytes",
"signature",
] {
assert!(!main.contains(forbidden), "pre.011 frontend exposes forbidden marker {forbidden}");
}
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
}
#[test]
fn pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.matches("deny_unknown_fields").count() >= 2);
assert!(dto.contains("RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256"));
assert!(dto.contains("validate_logical_identity"));
assert!(dto.contains("ERROR_CODE_ROUTE_REQUEST_INVALID"));
let frontend_logging = read_text(app_root().join("src/frontend_logging.rs").as_path());
for required in [
"deny_unknown_fields",
"FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192",
"FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16",
"payload.message.chars().any(char::is_control)",
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"eq_ignore_ascii_case",
] {
assert!(frontend_logging.contains(required), "missing pre.012 frontend IPC guard {required}");
}
assert!(!frontend_logging.contains("to_ascii_lowercase()"));
let frontend = read_text(app_root().join("frontend/ts/frontend_log.ts").as_path());
assert!(frontend.contains("FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024"));
assert!(frontend.contains("boundMessage(message)"));
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("error_domain = error.code().domain()"));
assert!(tauri.contains("error_code = error.code().code()"));
assert!(!tauri.contains("error = ?error"));
}

View File

@@ -0,0 +1,14 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/public_api.rs
// version: 1
//! Public API checks for the KSP Raw Transaction Ingest 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_raw_transaction_ingest_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<()>>());
}

View File

@@ -0,0 +1,226 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 12
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
#![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_010_production_module_inventory_adds_only_route_monitoring_to_pre_009_surface() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let expected = [
"mod app_state;",
"mod bootstrap;",
"mod constants;",
"mod dto_common;",
"mod dto_route;",
"mod errors;",
"mod frontend_logging;",
"mod logging_runtime;",
"mod route_inventory;",
"mod route_monitoring;",
"mod route_runtime;",
"mod route_start;",
"mod splash;",
"mod tauri;",
"mod tw_main;",
"mod tw_splash;",
];
assert_eq!(lib.matches("mod ").count(), expected.len());
for module in expected {
assert!(lib.contains(module));
}
}
#[test]
fn pre_008_public_surface_still_exposes_only_application_run_entry_point() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let public_reexports = lib.lines().filter(|line| return line.starts_with("pub use ")).collect::<std::vec::Vec<_>>();
assert_eq!(public_reexports, vec!["pub use self::tauri::run;"]);
}
#[test]
fn pre_008_keeps_five_exact_source_reconstructions_separate_from_store_worker_lifecycle() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"RawTransactionIngestYellowstoneSource::new",
"RawTransactionIngestStandardLogsSource::new",
"RawTransactionIngestStandardBlockSource::new",
"RawTransactionIngestHeliusTransactionSource::new",
"RawTransactionIngestHttpBlockPollingSource::new",
"RawTransactionIngestRuntimeResources",
] {
assert!(route_start.contains(required), "missing pre.008 exact Worker resource marker {required}");
}
for forbidden in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal"] {
assert!(!route_start.contains(forbidden), "route preparation owns runtime lifecycle marker {forbidden}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in ["Store::open", "start_with_runtime_resources", "request_stop", "wait_terminal", "StoreHealthState::Ready", "close_store_arc"] {
assert!(runtime.contains(required), "missing pre.008 lifecycle marker {required}");
}
}
#[test]
fn pre_007_start_revalidation_reuses_inventory_and_requires_profile_network_identity() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
for required in [
"current_generation == 0",
"request.inventory_generation != current_generation",
"build_route_inventory_with_environment",
"request.profile_id",
"resolve_store_config_profile",
"store.settings().network().as_str() != network",
"transport_matches_network",
] {
assert!(route_start.contains(required), "missing pre.007 revalidation marker {required}");
}
}
#[test]
fn pre_009_multi_route_runtime_keeps_independent_workers_on_one_same_network_store() {
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"routes: std::vec::Vec<RouteRuntimeSlot>",
"store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>",
"ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH",
"already owns one Worker for the selected logical route",
"shared_store(prepared.network.as_str())",
"self.runtime_state.finish(self.token, terminal, monitoring)",
"Arc::try_unwrap",
] {
assert!(runtime.contains(required), "missing pre.009 shared-Store marker {required}");
}
assert!(!runtime.contains("RouteRuntimeSlot::Idle"));
}
#[test]
fn pre_009_targeted_stop_is_route_scoped_and_late_terminal_safe() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.contains("RawIngestRouteStopRequestDto"));
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto)"));
assert!(runtime.contains("terminal.profile_id == request.profile_id && terminal.route_id == request.route_id"));
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("request: crate::RawIngestRouteStopRequestDto"));
}
#[test]
fn pre_009_yellowstone_uses_block_subscription_and_one_get_block_path_instead_of_per_transaction_hydration() {
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
let start = route_start.find("fn prepare_yellowstone(");
let end = route_start.find("fn prepare_http_polling(");
assert!(start.is_some());
assert!(end.is_some());
let yellowstone = match (start, end) {
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &route_start[start..end],
_ => "",
};
assert!(yellowstone.contains("insert_block_filter"));
assert!(yellowstone.contains("set_include_transactions(std::option::Option::Some(false))"));
assert!(yellowstone.contains("single_http_role(transport.http_settings(), network, &[\"getBlock\"])"));
assert!(!yellowstone.contains("insert_transaction_filter"));
let workspace = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs").as_path());
assert!(workspace.contains("RawTransactionIngestYellowstoneMode::BlockHydration"));
assert!(workspace.contains("fetch_yellowstone_block_ingresses"));
assert!(workspace.contains("get_block_observed"));
assert!(workspace.contains("RAW_TRANSACTION_INGEST_YELLOWSTONE_BLOCK_RECONCILIATION_ATTEMPTS"));
assert!(workspace.contains("stop_receiver.changed()"));
}
#[test]
fn pre_010_latest_value_monitoring_reuses_worker_snapshot_source_and_tauri_event_bridge() {
let monitoring = read_text(app_root().join("src/route_monitoring.rs").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"RawIngestRouteGapDto",
"project_route_monitoring",
"continuity_frontier_slot",
"future_target_coverage",
"source_reconnect_total",
"source_replay_attempt_total",
"repaired_gap_total",
"backpressure_wait_total",
] {
assert!(monitoring.contains(required), "missing pre.010 monitoring marker {required}");
}
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["get_route_monitoring", "RAW_INGEST_ROUTE_STATUS_EVENT_NAME", "monitor_route_status", "source.wait_for_change(observed).await"] {
assert!(tauri.contains(required), "missing pre.010 event/resync marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("monitoring_statuses"));
assert!(runtime.contains("last_monitoring"));
}
#[test]
fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
for required in [
"RawIngestRouteMonitoringDto",
"bindRouteMonitoring",
"renderMonitoringDetail",
"syncRouteMonitoring(\"startup\")",
"syncRouteMonitoring(\"start\")",
"syncRouteMonitoring(\"stop\")",
] {
assert!(main.contains(required), "missing pre.011 frontend completeness marker {required}");
}
let html = read_text(app_root().join("frontend/main.html").as_path());
for required in ["Supervision route", "Pipeline / persistence", "Sources / continuité / repair", "Gaps courants / récents"] {
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
}
}
#[test]
fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("source.wait_for_change(observed).await"));
assert!(tauri.contains("ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500"));
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
assert!(main.contains("renderRouteMonitoringCard"));
assert!(main.contains("renderSelectedProfile"));
}
#[test]
fn pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
for required in [
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"ERROR_CODE_ROUTE_REQUEST_INVALID",
"ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN",
"ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED",
] {
assert!(lib.contains(required), "missing pre.012 crate-root error export {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("stop_requested: bool"));
assert!(runtime.contains("RouteStopTarget::Starting"));
assert!(runtime.contains("cancelled_start_terminal"));
assert!(runtime.contains("inner.routes.is_empty()"));
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
assert!(route_start.contains("build_route_inventory_with_environment"));
assert!(route_start.contains("request.validate_logical_identity()"));
let worker_manifest = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/Cargo.toml").as_path());
assert!(!worker_manifest.contains("ksp-app-raw-transaction-ingest-desk"));
}

View 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"
]
}

View File

@@ -0,0 +1,14 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_common.rs
// version: 1
#[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");
}

View File

@@ -0,0 +1,124 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
// version: 5
#[test]
fn pre_005_route_ids_are_exact_and_stably_ordered() {
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
assert_eq!(foundation.route_ids.len(), 5);
let value = serde_json::to_value(foundation.route_ids);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(
value,
serde_json::json!(["yellowstone-hydrated", "standard-logs-hydrated", "standard-block-direct", "helius-transaction-hydrated", "http-block-polling"])
);
}
}
#[test]
fn pre_005_route_states_are_exact_without_runtime_claims() {
let foundation = crate::RawIngestRouteFoundationDto::scaffold();
assert_eq!(foundation.states.len(), 7);
let value = serde_json::to_value(foundation.states);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(value, serde_json::json!(["unavailable", "configured", "starting", "running", "stopping", "stopped", "faulted"]));
}
}
#[test]
fn pre_006_unavailable_reason_codes_are_exact_and_safe() {
let values = [
crate::RawIngestRouteUnavailableReason::ProfileUnresolved,
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
crate::RawIngestRouteUnavailableReason::MissingHttpGetBlock,
crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction,
crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan,
crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability,
crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability,
crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability,
crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc,
crate::RawIngestRouteUnavailableReason::MissingRequiredSecret,
];
let serialized = serde_json::to_value(values);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert_eq!(
serialized,
serde_json::json!([
"profile_unresolved",
"network_mismatch",
"missing_http_get_block",
"missing_http_get_transaction",
"missing_http_block_scan",
"missing_ws_logs_capability",
"missing_ws_block_capability",
"missing_helius_transaction_capability",
"missing_yellowstone_grpc",
"missing_required_secret"
])
);
}
}
#[test]
fn pre_008_runtime_acknowledgement_serializes_only_safe_logical_identity_and_state() {
let dto = crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 7,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::StandardBlockDirect,
state: crate::RawIngestRouteState::Running,
};
let value = serde_json::to_value(dto);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
assert_eq!(
value,
serde_json::json!({
"commitment": "confirmed",
"inventoryGeneration": 7,
"network": "mainnet",
"profileId": "mainnet",
"routeId": "standard-block-direct",
"state": "running"
})
);
let encoded = value.to_string();
for forbidden in ["endpoint", "provider", "connectionUri", "sourceKey", "workerId", "payload"] {
assert!(!encoded.contains(forbidden));
}
}
}
#[test]
fn pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity() {
let start = serde_json::from_value::<crate::RawIngestRouteStartRequestDto>(serde_json::json!({
"commitment": "confirmed",
"inventoryGeneration": 1,
"profileId": "mainnet",
"routeId": "http-block-polling",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(start.is_err());
let stop = serde_json::from_value::<crate::RawIngestRouteStopRequestDto>(serde_json::json!({
"profileId": "mainnet",
"routeId": "http-block-polling",
"workerId": "forbidden"
}));
assert!(stop.is_err());
let oversized = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 1,
profile_id: "x".repeat(super::RAW_INGEST_PROFILE_ID_MAX_BYTES + 1),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let oversized = oversized.validate_logical_identity();
assert!(oversized.is_err());
if let std::result::Result::Err(error) = oversized {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_REQUEST_INVALID);
}
let control = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet\n".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
assert!(control.validate_logical_identity().is_err());
}

View File

@@ -0,0 +1,63 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
// version: 2
#[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-raw-transaction-ingest-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);
}
#[test]
fn pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages() {
let unknown = serde_json::from_value::<crate::FrontendLogPayloadDto>(serde_json::json!({
"level": "info",
"targetId": "main",
"message": "safe",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(unknown.is_err());
let oversized = crate::FrontendLogPayloadDto {
level: "info".to_owned(),
target_id: "main".to_owned(),
message: "x".repeat(super::FRONTEND_LOG_MESSAGE_MAX_BYTES + 1),
};
let result = super::validate_payload_shape(&oversized);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID);
}
let nul = crate::FrontendLogPayloadDto { level: "info".to_owned(), target_id: "main".to_owned(), message: "safe\0hostile".to_owned() };
assert!(super::validate_payload_shape(&nul).is_err());
}

View File

@@ -0,0 +1,215 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_inventory.rs
// version: 5
#[test]
fn pre_006_route_requirement_projection_is_fail_closed_and_ordered() {
let route = super::route_with_requirements(
crate::RawIngestRouteId::StandardLogsHydrated,
"devnet",
&[
(false, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
(false, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
],
);
assert!(!route.selectable);
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability));
assert_eq!(route.network.as_deref(), std::option::Option::Some("devnet"));
let configured = super::route_with_requirements(
crate::RawIngestRouteId::HttpBlockPolling,
"mainnet",
&[(true, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
);
assert!(configured.selectable);
assert_eq!(configured.state, crate::RawIngestRouteState::Configured);
assert_eq!(configured.reason, std::option::Option::None);
}
#[test]
fn pre_006_unavailable_profile_projects_all_five_routes_without_physical_material() {
let profile =
super::unavailable_profile("mainnet", false, std::option::Option::Some("mainnet"), crate::RawIngestRouteUnavailableReason::MissingRequiredSecret);
assert_eq!(profile.routes.len(), 5);
assert_eq!(profile.network.as_deref(), std::option::Option::Some("mainnet"));
for route in profile.routes {
assert!(!route.selectable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
}
}
#[test]
fn pre_006_fix_003_network_profiles_aggregate_same_network_capabilities_and_omit_undeclared_provider_routes() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok());
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 7);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(inventory.generation, 7);
assert_eq!(inventory.default_profile, "devnet");
assert_eq!(
inventory.profiles.iter().map(|profile| return profile.profile_id.as_str()).collect::<std::vec::Vec<_>>(),
vec!["devnet", "mainnet", "testnet"]
);
let devnet = profile(&inventory, "devnet");
assert!(devnet.is_some());
if let std::option::Option::Some(devnet) = devnet {
assert!(devnet.is_default);
assert_eq!(devnet.network.as_deref(), std::option::Option::Some("devnet"));
assert_optional_secret_route(devnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(devnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(devnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_optional_secret_route(devnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_route(devnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let mainnet = profile(&inventory, "mainnet");
assert!(mainnet.is_some());
if let std::option::Option::Some(mainnet) = mainnet {
assert_eq!(mainnet.network.as_deref(), std::option::Option::Some("mainnet"));
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(mainnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(mainnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_optional_secret_route(mainnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_route(mainnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let testnet = profile(&inventory, "testnet");
assert!(testnet.is_some());
if let std::option::Option::Some(testnet) = testnet {
assert_eq!(testnet.network.as_deref(), std::option::Option::Some("testnet"));
assert_optional_secret_route(testnet, crate::RawIngestRouteId::YellowstoneHydrated);
assert_route(testnet, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(testnet, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_route_absent(testnet, crate::RawIngestRouteId::HeliusTransactionHydrated);
assert_eq!(testnet.routes.len(), 4);
assert_route(testnet, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
let serialized = serde_json::to_string(&inventory);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
for forbidden in [
"http://",
"https://",
"ws://",
"wss://",
"api-key",
"x-token",
"connection_uri",
"source_key",
"endpoint_name",
"publicnode_mainnet",
"helius_mainnet",
] {
assert!(!serialized.contains(forbidden), "safe network inventory leaked forbidden source marker {forbidden}");
}
}
}
#[test]
fn pre_006_fix_003_base_network_routes_remain_configured_independently_of_optional_sources() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok());
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory_with_environment(&management, &environment, 11);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for profile_id in ["devnet", "mainnet", "testnet"] {
let selected = profile(&inventory, profile_id);
assert!(selected.is_some());
if let std::option::Option::Some(selected) = selected {
assert_route(selected, crate::RawIngestRouteId::StandardLogsHydrated, true, std::option::Option::None);
assert_route(selected, crate::RawIngestRouteId::StandardBlockDirect, true, std::option::Option::None);
assert_route(selected, crate::RawIngestRouteId::HttpBlockPolling, true, std::option::Option::None);
}
}
}
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let crates_root = manifest.parent();
let crates_root = match crates_root {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test manifest has no crates parent"));
},
};
let workspace = crates_root.parent();
let workspace = match workspace {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, "test crates root has no workspace parent"));
},
};
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();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
}
fn profile<'a>(inventory: &'a crate::RawIngestRouteInventoryDto, profile_id: &str) -> std::option::Option<&'a crate::RawIngestProfileInventoryDto> {
return inventory.profiles.iter().find(|profile| return profile.profile_id == profile_id);
}
fn assert_optional_secret_route(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some());
if let std::option::Option::Some(route) = route {
if route.selectable {
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
assert_eq!(route.reason, std::option::Option::None);
} else {
assert_eq!(route.state, crate::RawIngestRouteState::Unavailable);
assert_eq!(route.reason, std::option::Option::Some(crate::RawIngestRouteUnavailableReason::MissingRequiredSecret));
}
}
}
fn assert_route_absent(profile: &crate::RawIngestProfileInventoryDto, route_id: crate::RawIngestRouteId) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_none());
}
fn assert_route(
profile: &crate::RawIngestProfileInventoryDto,
route_id: crate::RawIngestRouteId,
selectable: bool,
reason: std::option::Option<crate::RawIngestRouteUnavailableReason>,
) {
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some());
if let std::option::Option::Some(route) = route {
assert_eq!(route.selectable, selectable);
assert_eq!(route.reason, reason);
assert_eq!(route.state, if selectable { crate::RawIngestRouteState::Configured } else { crate::RawIngestRouteState::Unavailable });
}
}

View File

@@ -0,0 +1,43 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_monitoring.rs
// version: 1
#[test]
fn pre_010_source_gap_and_repair_codes_are_exact_and_source_neutral() {
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Active), "active");
assert_eq!(super::source_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSourceState::Reconnecting), "reconnecting");
assert_eq!(super::gap_state_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapState::Repairing), "repairing");
assert_eq!(super::gap_reason_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestGapReason::SourceFailure), "source_failure");
assert_eq!(
super::repair_method_code(ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestRepairMethod::TransactionHydration),
"transaction_hydration"
);
}
#[test]
fn pre_010_slot_projection_uses_decimal_text_for_javascript_safe_exactness() {
assert_eq!(super::slot_text(std::option::Option::Some(u64::MAX)), std::option::Option::Some(u64::MAX.to_string()));
assert_eq!(super::slot_text(std::option::Option::None), std::option::Option::None);
}
#[test]
fn pre_010_bounded_cardinality_projection_rejects_only_unrepresentable_values() {
let zero = super::usize_to_u32(0, "zero");
assert!(matches!(zero, std::result::Result::Ok(0)));
let maximum = usize::try_from(u32::MAX);
assert!(maximum.is_ok());
if let std::result::Result::Ok(maximum) = maximum {
let converted = super::usize_to_u32(maximum, "maximum");
assert!(matches!(converted, std::result::Result::Ok(u32::MAX)));
}
if usize::BITS > u32::BITS {
let overflow_value = usize::try_from(u64::from(u32::MAX) + 1);
assert!(overflow_value.is_ok());
if let std::result::Result::Ok(overflow_value) = overflow_value {
let overflow = super::usize_to_u32(overflow_value, "overflow");
assert!(overflow.is_err());
if let std::result::Result::Err(error) = overflow {
assert_eq!(error.code(), crate::ERROR_CODE_APP_STATE_INVALID);
}
}
}
}

View File

@@ -0,0 +1,143 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
// version: 5
#[test]
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
let state = crate::RouteRuntimeState::new();
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty());
assert!(inner.last_monitoring.is_empty());
assert!(inner.store.is_none());
assert!(inner.network.is_none());
assert!(inner.store_opening_token.is_none());
assert!(inner.store_closing_token.is_none());
}
}
#[test]
fn pre_009_late_targeted_stop_returns_only_the_matching_terminal_route() {
let state = crate::RouteRuntimeState::new();
let terminal = crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 9,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
state: crate::RawIngestRouteState::Faulted,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.last_terminals.push(terminal.clone());
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
let target = state.stop_target(&request);
assert!(target.is_ok());
assert!(matches!(&target, std::result::Result::Ok(super::RouteStopTarget::Terminal(_))));
if let std::result::Result::Ok(super::RouteStopTarget::Terminal(value)) = target {
assert_eq!(value.profile_id, terminal.profile_id);
assert_eq!(value.route_id, terminal.route_id);
assert_eq!(value.state, crate::RawIngestRouteState::Faulted);
}
}
#[test]
fn pre_009_late_targeted_stop_waits_while_final_shared_store_is_closing() {
let state = crate::RouteRuntimeState::new();
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.last_terminals.push(crate::RawIngestRouteRuntimeDto {
commitment: crate::RawIngestCommitment::Finalized,
inventory_generation: 11,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
state: crate::RawIngestRouteState::Stopped,
});
inner.store_closing_token = std::option::Option::Some(77);
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
}
#[test]
fn pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 12,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: 12 });
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::Starting { token: 12, .. })));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
let rollback = state.rollback(12);
assert!(matches!(rollback, std::result::Result::Ok(std::option::Option::None)));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty());
assert!(matches!(inner.last_terminals.first(), std::option::Option::Some(value) if value.state == crate::RawIngestRouteState::Stopped));
}
}
#[test]
fn pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Finalized,
inventory_generation: 13,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity, stop_requested: false, token: 13 });
}
let first = state.begin_shutdown();
assert!(matches!(first, std::result::Result::Ok(true)));
let second = state.begin_shutdown();
assert!(matches!(second, std::result::Result::Ok(false)));
let admission = state.ensure_start_admission_open();
assert!(admission.is_err());
if let std::result::Result::Err(error) = admission {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN);
}
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.shutting_down);
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
}
#[test]
fn pre_012_fix_001_start_route_future_is_send_for_tauri_command() {
fn assert_send_future<'a>(
state: &'a crate::AppState,
request: &'a crate::RawIngestRouteStartRequestDto,
) -> impl std::future::Future<Output = ksp_core_lib::Result<crate::RouteRuntimeLaunch>> + std::marker::Send + 'a {
return state.start_route(request);
}
let _assertion = assert_send_future;
}

View File

@@ -0,0 +1,97 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_start.rs
// version: 2
#[test]
fn pre_007_stale_generation_is_rejected_before_config_or_transport_reconstruction() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 3,
profile_id: "devnet".to_owned(),
route_id: crate::RawIngestRouteId::StandardBlockDirect,
};
let result = crate::validate_route_start(&management, 2, &request);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY);
}
}
#[test]
fn pre_007_commitments_are_bounded_to_confirmed_and_finalized() {
assert_eq!(crate::RawIngestCommitment::Confirmed.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Confirmed);
assert_eq!(crate::RawIngestCommitment::Finalized.into_transport(), ksp_onchain_transport_lib::SolanaCommitment::Finalized);
assert_eq!(crate::RawIngestCommitment::Confirmed.as_str(), "confirmed");
assert_eq!(crate::RawIngestCommitment::Finalized.as_str(), "finalized");
}
#[test]
fn pre_007_public_standard_profiles_reconstruct_block_and_http_sources_without_network_io() {
let management = committed_management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let inventory = crate::build_route_inventory(&management, 1);
assert!(inventory.is_ok());
let inventory = match inventory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for profile in inventory.profiles {
for route_id in [crate::RawIngestRouteId::StandardLogsHydrated, crate::RawIngestRouteId::StandardBlockDirect, crate::RawIngestRouteId::HttpBlockPolling]
{
let route = profile.routes.iter().find(|route| return route.route_id == route_id);
assert!(route.is_some(), "missing base route for {}", profile.profile_id);
if let std::option::Option::Some(route) = route {
assert_eq!(route.state, crate::RawIngestRouteState::Configured);
let request = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 1,
profile_id: profile.profile_id.clone(),
route_id,
};
let prepared = crate::validate_route_start(&management, 1, &request);
assert!(prepared.is_ok(), "failed Start-time reconstruction for {} / {}", profile.profile_id, route_id.as_str());
}
}
}
}
fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let crates_root = manifest.parent();
let crates_root = match crates_root {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED, "test manifest has no crates parent"));
},
};
let workspace = crates_root.parent();
let workspace = match workspace {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_PREPARATION_FAILED,
"test crates root has no workspace parent",
));
},
};
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();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
}

View File

@@ -0,0 +1,36 @@
// file: crates/ksp-app-raw-transaction-ingest-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());
}

View File

@@ -0,0 +1,72 @@
// file: crates/ksp-app-raw-transaction-ingest-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-raw-transaction-ingest-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: 1440,
strictPort: true,
host: devHost || false,
ws: {
protocol: "ws",
host: devHost || "localhost",
port: 1441,
},
watch: {
ignored: ["**/src/**"],
},
},
});

View File

@@ -53,19 +53,21 @@
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json", "../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-raw-transaction-ingest-desk.json": "config/composite.ksp-app-raw-transaction-ingest-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-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/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json",
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
"../../config/std.store.json": "config/std.store.json",
"../../config/std.transport.json": "config/std.transport.json",
"../../config/std.wallet.json": "config/std.wallet.json",
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json", "../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.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.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.store.schema.json": "config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json", "../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json" "../../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"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs // file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
// version: 11 // version: 13
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`. //! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
@@ -85,21 +85,29 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
} }
#[test] #[test]
fn pre_004_packaging_contains_current_fourteen_config_resources() { fn v0_3_8_pre_006_packaging_contains_current_sixteen_config_resources() {
let root = app_root(); let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path()); let tauri = read_json(root.join("tauri.conf.json").as_path());
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object); let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
assert!(resources.is_some()); assert!(resources.is_some());
if let std::option::Option::Some(resources) = resources { if let std::option::Option::Some(resources) = resources {
assert_eq!(resources.len(), 14); assert_eq!(resources.len(), 16);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str), resources.get("../../config/composite.ksp-app-backfill-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"), std::option::Option::Some("config/composite.ksp-app-backfill-desk.json"),
); );
assert_eq!(
resources.get("../../config/composite.ksp-app-raw-transaction-ingest-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-raw-transaction-ingest-desk.json"),
);
assert_eq!( assert_eq!(
resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str), resources.get("../../config/composite.ksp-app-solprices-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"), std::option::Option::Some("config/composite.ksp-app-solprices-desk.json"),
); );
assert_eq!(
resources.get("../../config/composite.ksp-app-store-desk.json").and_then(serde_json::Value::as_str),
std::option::Option::Some("config/composite.ksp-app-store-desk.json"),
);
assert!(resources.contains_key("../../config/std.offchain_transport.json")); assert!(resources.contains_key("../../config/std.offchain_transport.json"));
assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json")); assert!(resources.contains_key("../../config/schemas/std.offchain_transport.schema.json"));
assert!(resources.contains_key("../../config/std.store.json")); assert!(resources.contains_key("../../config/std.store.json"));

View File

@@ -0,0 +1,41 @@
# file: crates/ksp-app-store-desk/Cargo.toml
# version: 2
[package]
name = "ksp-app-store-desk"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lib]
name = "ksp_app_store_desk_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "ksp-app-store-desk"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-store-lib = { path = "../ksp-store-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["sync", "time"] }
ts-rs.workspace = true
[dev-dependencies]
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,106 @@
<!-- file: crates/ksp-app-store-desk/README.md -->
<!-- version: 1 -->
# `ksp-app-store-desk`
`ksp-app-store-desk` est lapplication desktop spécialisée dinspection read-only du Store KSP.
Elle reste une couche de composition Tauri : Config sélectionne le profil Store et Logging, `ksp-store-lib` possède la façade backend-neutral et les contrats dinspection, et le backend physique reste entièrement masqué à lapplication comme au frontend.
## Package
```text
package : ksp-app-store-desk
lib : ksp_app_store_desk_lib
bin : ksp-app-store-desk
```
Le binaire est un launcher mince. La bibliothèque applicative possède le bootstrap, le lifecycle Store, les DTOs Tauri, les commands, les projections de détail bornées et le bridge de tracing frontend.
## Composition
```text
ksp-config-lib -> composite, profils, secrets et runtime packagé
ksp-core-lib -> erreurs et primitives communes
ksp-store-lib -> façade Store backend-neutral et contrats RAW/inspection
ksp-logging-lib -> logging/tracing applicatif
ksp-app-store-desk -> orchestration Tauri + projections sûres read-only
```
Lapplication ne dépend pas directement de `ksp-store-api`, `ksp-store-postgres-lib`, `tokio-postgres` ou dun autre backend physique. Elle ne possède aucun SQL.
## Vues
La surface utilisateur comprend :
- **Overview** : état du runtime Store, health portable, réseau, backend logique, migration et compteurs de pool sûrs ;
- **RAW Transactions** : inspection DataTables server-side, filtres de slot et direction, état de rétention et détail explicite ;
- **RAW Accounts** : inspection DataTables server-side, filtre pubkey exact, plage de slots, direction et détail explicite ;
- **Diagnostics** : projection sûre du shell, de Config et du runtime Store.
Les détails Transaction et Account contiennent également leurs observations dans des DataTables server-side dédiées lorsque ces observations existent.
## Pagination et inspection
Store Desk utilise la surface dinspection random-access de `ksp-store-lib` : `offset + limit` avec counts exacts. DataTables est lunique propriétaire de la pagination visible et les tailles admises sont `25`, `50` et `100`.
Cette voie dinspection humaine ne remplace pas la pagination cursor/keyset du Store. Les workers, jobs et autres consumers machine peuvent continuer à utiliser `RawPageCursor` / `RawPageRequest`; Store Desk ne consomme pas ces cursors.
Le frontend conserve `draw` localement et ne transmet ni search globale DataTables, ni ordre de colonnes arbitraire, ni cursor, ni réseau physique au Store.
## Rows, détails et observations
Les rows de table ne transportent jamais les bytes RAW complets :
- Transaction : identité, slot, format, hash, taille éventuelle et état de rétention ;
- Account : pubkey, slot, state hash, owner, lamports, executable, rent epoch et longueur de data ;
- Observation : provenance dacquisition sûre et metadata bornées, sans source payload bytes.
Un détail est chargé explicitement pour une seule entité. Les previews payload Transaction et data Account sont limitées à **512 bytes**, rendues en hexadécimal et accompagnées de la taille exacte et dun indicateur de troncature.
Pour une Transaction, les états `Full`, `Archived` et `Purged` restent distincts. Un état `Purged` est représenté via son tombstone ; Store Desk noffre aucune action darchive, purge ou force-rehydrate.
## Valeurs longues et copie
Les signatures, hashes, pubkeys, owners et autres identifiants longs sont tronqués visuellement au centre tout en restant disponibles via tooltip et bouton de copie. La copie najoute aucune capability Tauri : elle utilise le WebView et le tracing ne transporte que lidentifiant logique du champ, jamais la valeur copiée.
## Config et réseaux
Le composite dédié est :
```text
cfg.composite.ksp-app-store-desk
```
Les profils committed couvrent `mainnet`, `devnet` et `testnet`. Mainnet est le profil par défaut. Le composite contient uniquement Logging + Store ; aucun Transport nest ouvert par lapplication.
Le frontend ne reçoit jamais URI Store, credential, secret Config, SQL, handle pool ou type backend physique.
## Sécurité et ownership
Store Desk est read-only par contrat applicatif : aucun command Tauri décriture Store nest exposé. Les capabilities guest restent limitées à `core:default` et `tracing:default`.
Le frontend ne possède ni accès réseau navigateur direct, ni persistence navigateur des données RAW, ni accès filesystem, ni dialogue natif pour contourner les façades KSP.
Les comptes et counts `u64` susceptibles dexcéder le domaine entier sûr JavaScript traversent IPC sous forme décimale et sont vérifiés avant conversion côté TypeScript.
## Ports et développement
```text
Vite HTTP : 1438
Vite WS : 1439
```
Lancement :
```bash
(cd crates/ksp-app-store-desk && cargo tauri dev)
```
Build production :
```bash
(cd crates/ksp-app-store-desk && cargo tauri build)
```
Voir également [`USAGE.md`](USAGE.md), [`../ksp-store-lib/README.md`](../ksp-store-lib/README.md), [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) et [`../../docs/validation/025-V0_3_8_STORE_DESK.md`](../../docs/validation/025-V0_3_8_STORE_DESK.md).

View File

@@ -0,0 +1,149 @@
<!-- file: crates/ksp-app-store-desk/USAGE.md -->
<!-- version: 1 -->
# Utilisation de `ksp-app-store-desk`
## 1. Lancement
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-store-desk && cargo tauri dev)
```
Le runtime charge le composite Store Desk via `ksp-config-lib`, initialise Logging puis ouvre le Store sélectionné via `ksp-store-lib`.
## 2. Profil et readiness
Le composite `cfg.composite.ksp-app-store-desk` sélectionne un profil Logging et un profil Store. Mainnet est le profil par défaut ; Devnet et Testnet sont également définis dans la configuration committed.
La vue **Overview** expose notamment :
```text
Store open
health state
profile
network
backend kind
migration version / pending migrations
pool size / capacity / available / waiting
```
Ces informations sont des projections portables. Elles ne contiennent jamais URI PostgreSQL, credential, secret ou handle physique.
## 3. RAW Transactions
La vue **RAW Transactions** charge les données côté serveur via la façade dinspection Store.
Filtres disponibles :
```text
slot minimum
slot maximum
direction ascending / descending
```
La table affiche des summaries sans payload RAW complet. Les signatures et hashes longs restent copiables sans que leur valeur soit journalisée.
Le bouton de détail recharge explicitement une seule Transaction. Le modal affiche les metadata canoniques, létat de rétention et, lorsquun payload est encore retenu, un preview hexadécimal limité à 512 bytes avec taille exacte et indicateur de troncature.
Un état `Purged` est présenté à partir du tombstone disponible. Aucune opération darchive, purge ou force-rehydrate nest proposée par lapplication.
## 4. RAW Accounts
La vue **RAW Accounts** utilise le même modèle dinspection server-side.
Filtres disponibles :
```text
pubkey exacte
slot minimum
slot maximum
direction ascending / descending
```
La table ne transporte pas les bytes Account. Elle expose seulement les metadata utiles : pubkey, slot, state hash, owner, lamports, executable, rent epoch et longueur de data.
Le détail recharge exactement lidentité `(pubkey, slot, state_hash)` puis affiche un preview hexadécimal de data limité à 512 bytes, la longueur exacte et lindicateur de troncature.
## 5. Observations
Les modals Transaction et Account contiennent une table **Observations** associée à lentité actuellement ouverte.
La table est server-side et expose uniquement de la provenance sûre : provider logique, protocole, méthode/origine dacquisition, timestamps, endpoint/session/filter/commitment lorsquils existent, hash/taille de source et metadata Account optionnelles admises.
Les bytes du payload source ne traversent jamais IPC.
## 6. Pagination DataTables
Les tables Transactions, Accounts et Observations utilisent DataTables comme unique pager visible.
Tailles disponibles :
```text
25
50
100
```
Le frontend mappe `start` vers loffset Store et `length` vers la limite. Les counts exacts restent des chaînes décimales jusquà vérification du domaine `Number.MAX_SAFE_INTEGER`.
La pagination dinspection est distincte de la pagination cursor/keyset utilisée par les consumers machine du Store. Store Desk naccepte ni cursor, ni search globale DataTables, ni tri arbitraire de colonnes.
## 7. Rafraîchissement et filtres
**Refresh** redessine la table concernée avec ses filtres Store explicites. Un changement de filtre repart de la première page.
Les valeurs de filtre sont validées côté Rust : slots décimaux, pubkey canonique et direction autorisée. Elles ne sont pas injectées dans les logs frontend.
## 8. Valeurs longues et copie
Les valeurs longues sont affichées sous forme tronquée au centre. Le survol permet de consulter la valeur complète et le bouton de copie copie la valeur actuellement affichée par le frontend.
Les événements de tracing de copie transportent uniquement un `fieldId` logique. Ils ne contiennent jamais signature, pubkey, owner, hash ou preview hex.
## 9. Read-only et frontières
Store Desk est un outil dinspection. Il ne fournit aucune commande pour :
```text
écrire une entité RAW
ajouter une observation
archiver
purger
force-rehydrate
démarrer un backfill
démarrer un worker
modifier la configuration Store
```
Ces responsabilités appartiennent aux composants KSP propriétaires.
Le frontend ne reçoit jamais :
```text
URI Store
credentials/secrets
SQL
handles backend/pool
payload RAW complet
data Account complète
source payload bytes
contexte derreur arbitraire
```
## 10. Fermeture
La fermeture de la fenêtre principale bloque les nouvelles admissions puis ferme le Store de manière bornée avant de terminer le process.
Le shutdown applicatif neffectue aucune mutation métier de rétention ou de données.
## 11. Build production
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-store-desk && cargo tauri build)
```
Le build frontend passe par TypeScript puis Vite avant le build Tauri. Les resources Config enregistrées sont packagées selon le contrat commun des applications Desk KSP.

View File

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

View File

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

View 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`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

View File

@@ -0,0 +1,338 @@
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
<!-- version: 6 -->
<!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>Store Desk — Overview</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">Store 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">Overview</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 Store 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>Overview
</button>
<button class="nav-link text-start" type="button" data-view="transactions">
<i class="fa-solid fa-receipt me-2" aria-hidden="true"></i>RAW Transactions
</button>
<button class="nav-link text-start" type="button" data-view="accounts">
<i class="fa-solid fa-box-archive me-2" aria-hidden="true"></i>RAW Accounts
</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="overview">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Overview</h1>
<p class="text-body-secondary mb-0">État backend-neutral du Store sélectionné par le composite Config. Aucune donnée RAW complète n'est chargée ici.</p>
</div>
<div class="d-flex align-items-center gap-2">
<span id="appVersionBadge" class="badge text-bg-secondary"></span>
<button id="refreshOverview" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh
</button>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Store profile</span><div id="overviewStoreProfile" class="fw-semibold mt-2"></div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Target</span><div id="overviewStoreTarget" class="fw-semibold mt-2"></div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Health</span><div id="overviewStoreHealth" class="fw-semibold mt-2"></div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Pool</span><div id="overviewStorePool" class="fw-semibold mt-2"></div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Migration</span><div id="overviewStoreMigration" class="fw-semibold mt-2"></div></div></div></div>
</div>
<div id="overviewStoreDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</section>
<section data-view-panel="transactions" hidden>
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
<div>
<h1 class="h3 mb-1">RAW Transactions</h1>
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
</div>
<button id="refreshTransactions" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label" for="transactionSlotMin">Slot minimum</label>
<input id="transactionSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4">
<label class="form-label" for="transactionSlotMax">Slot maximum</label>
<input id="transactionSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4">
<label class="form-label" for="transactionDirection">Direction canonique</label>
<select id="transactionDirection" class="form-select">
<option value="descending" selected>Descending</option>
<option value="ascending">Ascending</option>
</select>
</div>
</div>
</div>
</div>
<div id="transactionsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive app-store-table-container">
<table id="rawTransactionsTable" class="table table-hover align-middle mb-0 app-store-table">
<caption class="visually-hidden">Inspection server-side des transactions RAW.</caption>
<thead>
<tr>
<th>Signature</th>
<th>Slot</th>
<th>Block time</th>
<th>Format ID</th>
<th>Version</th>
<th>Payload size</th>
<th>Content hash</th>
<th>Retention</th>
<th>Détail</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
<section data-view-panel="accounts" hidden>
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
<div>
<h1 class="h3 mb-1">RAW Accounts</h1>
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
</div>
<button id="refreshAccounts" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<label class="form-label" for="accountPubkey">Pubkey exacte</label>
<input id="accountPubkey" class="form-control font-monospace" type="text" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-2">
<label class="form-label" for="accountSlotMin">Slot minimum</label>
<input id="accountSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-2">
<label class="form-label" for="accountSlotMax">Slot maximum</label>
<input id="accountSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
</div>
<div class="col-md-4 col-lg-3">
<label class="form-label" for="accountDirection">Direction canonique</label>
<select id="accountDirection" class="form-select">
<option value="descending" selected>Descending</option>
<option value="ascending">Ascending</option>
</select>
</div>
</div>
</div>
</div>
<div id="accountsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive app-store-table-container">
<table id="rawAccountsTable" class="table table-hover align-middle mb-0 app-store-table">
<caption class="visually-hidden">Inspection server-side des états RAW Account.</caption>
<thead>
<tr>
<th>Pubkey</th>
<th>Slot</th>
<th>State hash</th>
<th>Owner</th>
<th>Lamports</th>
<th>Executable</th>
<th>Rent epoch</th>
<th>Data length</th>
<th>Détail</th>
</tr>
</thead>
<tbody></tbody>
</table>
</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 Config / Logging / Store. Les URI et détails physiques sensibles restent hors IPC.</p>
</div>
<button id="refreshDiagnostics" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-arrows-rotate me-1" 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>
<dt class="col-sm-5">Store profile</dt><dd id="runtimeStoreProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Store target</dt><dd id="runtimeStoreTarget" class="col-sm-7"></dd>
<dt class="col-sm-5">Store health</dt><dd id="runtimeStoreHealth" class="col-sm-7"></dd>
<dt class="col-sm-5">Migration</dt><dd id="runtimeStoreMigration" class="col-sm-7"></dd>
<dt class="col-sm-5">Pool</dt><dd id="runtimeStorePool" 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 id="runtimeStoreDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>
</div>
</section>
</div>
</div>
</div>
</section>
</div>
</main>
<div id="transactionDetailModal" class="modal fade" tabindex="-1" aria-labelledby="transactionDetailTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 id="transactionDetailTitle" class="modal-title h5">RAW Transaction detail</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div id="transactionDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<dl class="row app-runtime-list mb-4">
<dt class="col-sm-3">Signature</dt><dd id="transactionDetailSignature" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Slot</dt><dd id="transactionDetailSlot" class="col-sm-9"></dd>
<dt class="col-sm-3">Block time</dt><dd id="transactionDetailBlockTime" class="col-sm-9"></dd>
<dt class="col-sm-3">Format</dt><dd id="transactionDetailFormat" class="col-sm-9"></dd>
<dt class="col-sm-3">Content hash</dt><dd id="transactionDetailContentHash" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Retention</dt><dd id="transactionDetailRetention" class="col-sm-9"></dd>
<dt class="col-sm-3">Payload size</dt><dd id="transactionDetailPayloadSize" class="col-sm-9"></dd>
</dl>
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
<h3 class="h6 mb-0">Payload preview hex</h3>
<div class="d-flex align-items-center gap-2">
<button id="transactionDetailPayloadPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
<i class="fa-regular fa-copy" aria-hidden="true"></i>
<span>Copier</span>
</button>
<span id="transactionDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
</div>
</div>
<pre id="transactionDetailPayloadPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0"></pre>
<section class="border-top mt-4 pt-4" aria-labelledby="transactionObservationsTitle">
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
<div>
<h3 id="transactionObservationsTitle" class="h6 mb-1">Observations dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre uniquement ; aucun payload source nest chargé.</p>
</div>
</div>
<div id="transactionObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive">
<table id="transactionObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
<thead>
<tr>
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</div>
</div>
</div>
</div>
<div id="accountDetailModal" class="modal fade" tabindex="-1" aria-labelledby="accountDetailTitle" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 id="accountDetailTitle" class="modal-title h5">RAW Account detail</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div id="accountDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<dl class="row app-runtime-list mb-4">
<dt class="col-sm-3">Pubkey</dt><dd id="accountDetailPubkey" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Slot</dt><dd id="accountDetailSlot" class="col-sm-9"></dd>
<dt class="col-sm-3">State hash</dt><dd id="accountDetailStateHash" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Owner</dt><dd id="accountDetailOwner" class="col-sm-9 text-break"></dd>
<dt class="col-sm-3">Lamports</dt><dd id="accountDetailLamports" class="col-sm-9"></dd>
<dt class="col-sm-3">Executable</dt><dd id="accountDetailExecutable" class="col-sm-9"></dd>
<dt class="col-sm-3">Rent epoch</dt><dd id="accountDetailRentEpoch" class="col-sm-9"></dd>
<dt class="col-sm-3">Data length</dt><dd id="accountDetailDataLength" class="col-sm-9"></dd>
</dl>
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
<h3 class="h6 mb-0">Account data preview hex</h3>
<div class="d-flex align-items-center gap-2">
<button id="accountDetailDataPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
<i class="fa-regular fa-copy" aria-hidden="true"></i>
<span>Copier</span>
</button>
<span id="accountDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
</div>
</div>
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0"></pre>
<section class="border-top mt-4 pt-4" aria-labelledby="accountObservationsTitle">
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
<div>
<h3 id="accountObservationsTitle" class="h6 mb-1">Observations dacquisition</h3>
<p class="small text-body-secondary mb-0">Provenance sûre et métadonnées Yellowstone optionnelles ; aucun byte Account nest chargé.</p>
</div>
</div>
<div id="accountObservationsError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
<div class="table-responsive">
<table id="accountObservationsTable" class="table table-hover align-middle mb-0 app-store-table">
<thead>
<tr>
<th>Received</th><th>Observed</th><th>Provider</th><th>Protocol</th><th>Method</th><th>Origin</th><th>Endpoint</th><th>Commitment</th><th>Session</th><th>Filter</th><th>Source hash</th><th>Source bytes</th><th>Observation key</th><th>Startup</th><th>Write version</th><th>Tx signature</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</div>
</div>
</div>
</div>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,117 @@
// file: crates/ksp-app-store-desk/frontend/sass/_app.scss
// version: 2
$app-header-height: 72px;
$app-footer-height: 42px;
$app-sidebar-width: 250px;
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
overflow: hidden;
background: $gray-100;
}
.app-header {
position: fixed;
inset: 0 0 auto;
height: $app-header-height;
z-index: 1020;
border-bottom: 2px solid rgba($primary, 0.3);
}
.app-footer {
position: fixed;
inset: auto 0 0;
height: $app-footer-height;
z-index: 1020;
border-top: 2px solid rgba($primary, 0.3);
}
.app-main {
position: relative;
height: calc(100vh - $app-header-height - $app-footer-height);
margin-top: $app-header-height;
margin-bottom: $app-footer-height;
overflow: hidden;
}
.app-main > .row {
flex-wrap: nowrap;
min-width: 0;
}
.app-scrollable {
height: 100%;
max-height: 100%;
}
.app-logo {
display: block;
width: auto;
height: 42px;
}
.app-shell-card {
max-width: 1320px;
}
.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-runtime-list dd,
.app-store-table td:first-child,
.app-store-table td:nth-child(2) {
font-family: var(--bs-font-monospace);
}
.app-store-table-container {
min-height: 240px;
}
.app-copyable-long-text {
align-items: center;
display: inline-flex;
gap: .375rem;
max-width: 100%;
min-width: 0;
}
.app-copyable-long-text__value {
cursor: help;
display: inline-block;
max-width: 24ch;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.app-copyable-long-text__copy {
flex: 0 0 auto;
line-height: 1;
padding: .25rem .375rem;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,137 @@
// file: crates/ksp-app-store-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);
}
}

View File

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

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