52 Commits

Author SHA1 Message Date
68678a22c6 v0.3.7-rel.001 2026-09-03 08:17:54 +02:00
4506c2d12d v0.3.7-pre.017 2026-09-03 08:14:42 +02:00
d8f7c9bd8b v0.3.7-pre.016 2026-09-03 06:26:49 +02:00
a266a4d7b6 v0.3.7-pre.015 2026-09-02 22:18:21 +02:00
76f671b8c5 v0.3.7-pre.014 2026-09-02 22:10:06 +02:00
0847edda4c v0.3.7-pre.013-fix.001 2026-09-02 21:14:15 +02:00
fa04bdbcf6 v0.3.7-pre.013 2026-09-02 21:10:07 +02:00
7697f407db v0.3.7-pre.012-fix.001 2026-09-02 20:43:53 +02:00
4c16607e22 v0.3.7-pre.012 2026-09-02 20:29:50 +02:00
618f940310 v0.3.7-pre.011 2026-09-02 19:50:01 +02:00
313d2a1ea6 v0.3.7-pre.010 2026-09-02 18:56:09 +02:00
97f84e5c58 v0.3.7-pre.009-fix.002 2026-09-02 18:24:44 +02:00
db78fea161 v0.3.7-pre.009-fix.001 2026-09-02 18:19:48 +02:00
ec8875e864 v0.3.7-pre.009 2026-09-02 17:40:03 +02:00
ed72aaba67 v0.3.7-pre.008-fix.003 2026-09-02 17:14:00 +02:00
d49d7af0a4 v0.3.7-pre.008-fix.002 2026-09-02 17:09:39 +02:00
d7db8d69e0 v0.3.7-pre.008-fix.001 2026-09-02 17:02:26 +02:00
48d26b43f9 v0.3.7-pre.008 2026-09-02 16:56:11 +02:00
6e406a6b84 v0.3.7-pre.007-fix.001 2026-09-02 16:14:38 +02:00
9c4469248a v0.3.7-pre.007 2026-09-02 16:07:57 +02:00
6fc5b5b607 v0.3.7-pre.006-fix.001 2026-09-02 15:19:08 +02:00
3c4ae51ae2 v0.3.7-pre.006 2026-09-02 15:05:18 +02:00
57ff11b774 v0.3.7-pre.005-fix.001 2026-09-02 12:21:00 +02:00
500b87ef57 v0.3.7-pre.005 2026-09-02 12:01:35 +02:00
46a4ad7487 v0.3.7-pre.004 2026-09-02 11:00:20 +02:00
03a94f4fb6 v0.3.7-pre.003-fix.001 2026-09-02 10:55:57 +02:00
862f40ad26 v0.3.7-pre.003 2026-09-02 10:05:36 +02:00
bc7ccd97a5 v0.3.7-pre.002 2026-09-02 09:31:46 +02:00
dd85ac5e77 v0.3.7-pre.001 2026-09-02 08:47:42 +02:00
dd48ed9140 v0.3.6-rel.001 2026-09-01 23:43:43 +02:00
5a65c28898 v0.3.6-pre.013 2026-09-01 23:38:36 +02:00
42374115c7 v0.3.6-pre.012 2026-09-01 17:42:24 +02:00
9506df9487 v0.3.6-pre.011 2026-09-01 17:18:12 +02:00
7f8ce987a5 v0.3.6-pre.010 2026-09-01 16:55:14 +02:00
8dbd86c297 v0.3.6-pre.009-fix.002 2026-09-01 16:30:51 +02:00
75b2d7e7f1 v0.3.6-pre.009-fix.001 2026-09-01 16:17:22 +02:00
b861a1e3b8 v0.3.6-pre.009 2026-09-01 16:04:58 +02:00
d89aee9910 v0.3.6-pre.008-fix.001 2026-09-01 15:22:54 +02:00
75cbf9df09 v0.3.6-pre.008 2026-09-01 15:14:57 +02:00
138395ac35 v0.3.6-pre.007 2026-09-01 14:47:23 +02:00
be0032e76b v0.3.6-pre.006-fix.002 2026-09-01 14:28:13 +02:00
8096e6f07b v0.3.6-pre.006-fix.001 2026-09-01 14:24:22 +02:00
032919fa07 v0.3.6-pre.006 2026-09-01 14:08:53 +02:00
ea5f756d17 v0.3.6-pre.005-fix.001 2026-09-01 13:42:34 +02:00
4fdeab4d67 v0.3.6-pre.005 2026-09-01 13:00:21 +02:00
478548b7f4 v0.3.6-pre.004 2026-09-01 12:57:25 +02:00
20cf700f22 v0.3.6-pre.003-fix.001 2026-09-01 12:05:28 +02:00
f92c22d55a v0.3.6-pre.003 2026-09-01 11:58:38 +02:00
cb670952e8 v0.3.6-pre.002-fix.001 2026-09-01 10:55:35 +02:00
a25d8dd55a v0.3.6-pre.002 2026-09-01 10:45:39 +02:00
ecfc30a94b v0.3.6-pre.001-fix.001 2026-09-01 10:43:09 +02:00
6f2d0a092a v0.3.6-pre.001 2026-09-01 09:35:02 +02:00
222 changed files with 29640 additions and 543 deletions

View File

@@ -1,8 +1,40 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 25 --> <!-- version: 27 -->
# Changelog KSP # Changelog KSP
## 0.3.7 — Backfill Desk : contrôle, monitoring, Cancel/Resume et autocomplete — 2026-09-03
`0.3.7` introduit `ksp-app-backfill-desk`, première application Tauri KSP spécialisée dans le contrôle d'un `ksp-job-backfill-lib` historique `RawTransaction`. L'application reste une couche de composition : `ksp-config-lib` possède les profils/composites/secrets, `ksp-onchain-transport-lib` le pool HTTP et les politiques provider/retry/rate-limit, `ksp-store-lib` la persistence backend-neutral, `ksp-job-backfill-lib` la découverte/hydratation/frontier/checkpoint/runtime et `ksp-job-api` le lifecycle commun. La Desk ne dépend ni d'un backend Store physique, ni de SQL, ni d'un SDK provider.
Le package reprend le gabarit Tauri KSP courant avec cible lib + bin, launcher mince, splash commun, bridge `tracing`, frontend sous `frontend/`, bindings TS-RS applicatifs, ports stricts `1436/1437` et packaging Config/Schemas distribué. Le composite `cfg.composite.ksp-app-backfill-desk` couvre `mainnet`, `devnet` et `testnet`; Mainnet est le profil par défaut et la cohérence réseau Transport/Store est vérifiée avant ouverture d'une campagne. Les profils Store committed ont été réconciliés sur TLS PostgreSQL `disabled` pour l'environnement local de validation, sans retirer le support `verify_full` du Store/schema.
Le formulaire Backfill couvre `LatestAddress`, `BeforeAddress`, `AfterAddress` et `ExplicitSignatures`, les commitments `finalized`/`confirmed`, la sélection d'un rôle HTTP logique, `min_context_slot` lorsque le scope l'autorise, ainsi que les bornes publiques de page/candidats/concurrence. Le frontend ne choisit ni réseau physique, ni endpoint/provider, ni JobId; le réseau dérive de la composition Store et les validations finales restent déléguées à `BackfillRequest`.
Le Start réel conserve un seul run actif, génère le JobId côté backend et installe le handle de contrôle avant le spawn asynchrone. Le monitoring est latest-value : `BackfillSnapshotSource` est projeté via l'événement `ksp-backfill-status`, avec commande de resynchronisation explicite et rétention du dernier terminal. La projection expose lifecycle, phase, scope, boundary, compteurs de candidats/entités/observations, missing/conflicts/holes, concurrence maximale, frontier contiguë, présence de checkpoint et codes d'échec sûrs, sans adresse/signatures de campagne, payload RAW, URL, credential, contexte d'erreur arbitraire ni cursor/checkpoint concret.
Le Cancel est coopératif, ciblé par le JobId backend et idempotent : un Cancel IPC retardé ne peut pas annuler le run suivant, un terminal gagné reste autoritaire et la fermeture de l'application bloque d'abord les nouveaux Starts puis demande best-effort l'annulation avant le shutdown Store. Le Resume est limité à la session courante : la Desk conserve en mémoire Rust la requête terminale et le checkpoint opaque, `ksp-job-backfill-lib` valide puis réémet le checkpoint pour un nouveau JobId sans changer le scope fingerprint/frontier, et aucun checkpoint durable n'est promis après redémarrage.
Le polish final ajoute une saisie d'adresse libre avec autocomplete HTML `datalist` dérivé exclusivement de `ksp-core-lib::entries()`/`ProgramIdEntry`; aucune liste de Program IDs n'est hardcodée côté frontend et la sélection n'est jamais une allow-list. Les canaris de hardening figent ensuite l'inventaire des dépendances, modules, commandes Tauri, contrôles frontend, profils Config, frontières Store/provider, sécurité IPC et discipline crate-root des DTO partagés.
Le gate final passe `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, arbres Cargo ciblés, `cargo tree --duplicates` et `cargo tauri build`. Le run workspace enregistre 1 494 tests passés, 0 échec et 15 tests ignorés explicitement opt-in/operator-only; le build produit les bundles Linux `.deb`, `.rpm` et `.AppImage`.
`prompts/027-V0_3_8_START_PROMPT.md` ouvre `0.3.8` sur `ksp-app-store-desk` V1. Le gabarit, le visuel, la structure Tauri et les dépendances npm de base viennent exclusivement des Desk KSP actuelles; DataTables reprend le pattern déjà utilisé par Config/Wallet Desk. L'archive kbot3 reste obligatoire uniquement comme référence fonctionnelle/UX des anciens diagnostics et tableaux RAW : aucune source, DTO, commande, SQL, gabarit ou version npm kbot3 n'est reprise. La première tranche doit auditer les capabilities Store existantes et les gaps éventuels de listing/diagnostic avant d'ouvrir des tableaux `RawTransaction`, `RawAccountState`, observations et rétention/tombstones via la seule façade `ksp-store-lib`.
## 0.3.6 — Job API + backfill RAW historique observable — 2026-09-01
`0.3.6` introduit `ksp-job-api` comme contrat passif et runtime-neutral pour les traitements bornés/terminables : identité `JobId`/`JobKindCode`, lifecycle explicite, annulation coopérative partagée, snapshots typés et notifications latest-value consommables par plusieurs listeners sans transformer l'API en scheduler, runtime Tokio ou bus d'événements. La surface reste Core-only et sert immédiatement au premier consumer concret `ksp-job-backfill-lib`.
`ksp-job-backfill-lib` ferme une première verticale historique `RawTransaction` sur les abstractions KSP existantes. Les scopes `LatestAddress`, `BeforeAddress`, `AfterAddress` et `ExplicitSignatures` sont bornés par pages/candidats/concurrence, dédupliqués de manière stable et liés à un fingerprint sémantique réseau/scope qui exclut volontairement provider, endpoint, rôle Transport et `JobId`. La découverte utilise les wrappers typés de `ksp-onchain-transport-lib`; l'hydratation passe exclusivement par `get_transaction_observed`, conserve la provenance du provider/endpoint gagnant et transforme la réponse en RAW v1 canonique avec hash SHA-256 exact, sans SDK provider ni modèle wire dupliqué.
La persistance passe uniquement par `ksp-store-lib` et son écriture atomique `RawTransaction + RawTransactionObservation` en mode normal. Les résultats distinguent insertion, présence idempotente, tombstone purgé, transaction manquante et conflit de contenu ; aucun read-before-write, `ForceRehydrate`, SQL, backend PostgreSQL direct ou décision de rétention n'est déplacé dans Job. Le réseau Store est vérifié avant écriture et l'identité logique de transaction reste `(RawNetworkId, Signature)` indépendamment de la source d'acquisition.
Le runtime Backfill ajoute concurrence bornée, réconciliation des terminaisons hors ordre, frontier strictement contiguë et checkpoint caller-owned. Les reprises `Latest`, `Before`, `After` et `ExplicitSignatures` ne sautent que le préfixe durable réellement prouvé. `BackfillJobRuntime` expose un `BackfillJobHandle` clonable avec annulation coopérative et `BackfillSnapshotSource` latest-value ; les opérations pré-Store peuvent être abandonnées, tandis qu'une persistance déjà soumise est toujours drainée avant la décision terminale. Les snapshots conservent phase, compteurs, holes/conflicts/missing, maximum in-flight, frontier/checkpoint et code d'échec sans projeter payload RAW, secret, URL ou état runtime interne.
Le hardening externe verrouille les codes d'erreur, bornes adversariales, redaction `Debug`, indépendance du fingerprint, absence de fuite Tokio/Futures/backend dans la façade, dépendances backend-neutral et discipline d'import/visibilité crate-root. La réconciliation documentaire finale ajoute les README/USAGE durables de Job API et Backfill et aligne les architectures Jobs/Workers/Apps sans transformer les guides d'utilisation en journal de release.
Les gates de clôture passent `cargo fmt --all -- --check`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace `--all-targets --all-features -- -D warnings`, `cargo test --workspace --all-targets --all-features`, tests ciblés Job/Backfill et graphes Cargo. Les smokes réseau/Tauri déjà explicitement opt-in restent ignorés par défaut ; aucune preuve non exécutée n'est déclarée PASS.
## 0.3.5 — Interface acquisition events partagés — 2026-08-31 ## 0.3.5 — Interface acquisition events partagés — 2026-08-31
`0.3.5` étend `ksp-interface-lib` avec deux familles passives d'acquisition réellement partagées, sans transformer Interface en runtime, en event bus ou en seconde couche RAW. `SlotLifecycleEvent` expose un `slot` et un `SlotLifecycleStage` non exhaustif limité à `Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed` et `Rooted`. La normalisation conserve la sémantique commune : les notifications Solana `optimisticConfirmation` et Yellowstone `Confirmed` convergent vers `OptimisticallyConfirmed`, tandis que Solana `root` et Yellowstone `Finalized` convergent vers `Rooted`; les différences d'ordre, de complétude, de replay et de transport restent la responsabilité du producteur/Transport. `0.3.5` étend `ksp-interface-lib` avec deux familles passives d'acquisition réellement partagées, sans transformer Interface en runtime, en event bus ou en seconde couche RAW. `SlotLifecycleEvent` expose un `slot` et un `SlotLifecycleStage` non exhaustif limité à `Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed` et `Rooted`. La normalisation conserve la sémantique commune : les notifications Solana `optimisticConfirmation` et Yellowstone `Confirmed` convergent vers `OptimisticallyConfirmed`, tandis que Solana `root` et Yellowstone `Finalized` convergent vers `Rooted`; les différences d'ordre, de complétude, de replay et de transport restent la responsabilité du producteur/Transport.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 392 # version: 444
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-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-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"]
[workspace.package] [workspace.package]
version = "0.3.5" version = "0.3.7"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md --> <!-- file: README.md -->
<!-- version: 7 --> <!-- version: 9 -->
# Khadhroony Solana Project # Khadhroony Solana Project
@@ -47,6 +47,14 @@ Les besoins du trading constituent une priorité produit à court terme mais ne
- Aucun `rust-toolchain.toml` n'est utilisé. - Aucun `rust-toolchain.toml` n'est utilisé.
- Les environnements et contraintes des démonstrations doivent être visibles dans leur nomenclature lorsqu'ils ne sont pas sélectionnables. - Les environnements et contraintes des démonstrations doivent être visibles dans leur nomenclature lorsqu'ils ne sont pas sélectionnables.
## Fondations opérationnelles actuelles
La couche RAW dispose d'une façade Store backend-neutral et d'un premier job historique concret. `ksp-job-api` porte les contrats passifs communs des jobs bornés ; `ksp-job-backfill-lib` compose Transport observé et Store pour découvrir, hydrater et persister des transactions historiques RAW avec concurrence bornée, checkpoint contigu caller-owned, annulation coopérative et snapshots latest-value.
`ksp-app-backfill-desk` fournit la surface desktop spécialisée de composition de ce runtime : sélection d'un scope historique et d'un rôle HTTP logique, validation backend des bornes, Start single-run, monitoring latest-value, Cancel ciblé et Resume in-session sur checkpoint Rust-only. L'application ouvre Transport et Store via leurs façades KSP, ne dépend d'aucun backend Store concret et n'expose ni URL/credential, ni payload RAW, ni checkpoint opaque au frontend.
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.
## Points d'entrée ## Points d'entrée
- [`RULES.md`](RULES.md) — index des règles normatives ; - [`RULES.md`](RULES.md) — index des règles normatives ;

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 100 --> <!-- version: 102 -->
# Roadmap KSP # Roadmap KSP
@@ -98,11 +98,17 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [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.
- [ ] `0.3.6` Introduire en parallèle `ksp-job-api` et `ksp-job-backfill-lib` : reprendre fonctionnellement le backfill historique kbot3 sur les abstractions KSP actuelles (`ksp-onchain-transport-lib` + `ksp-store-lib`), fermer une première verticale `RawTransaction` historique par adresse avec directions/anchors, déduplication, hydratation, idempotence, frontier/checkpoint/reprise, cancellation et concurrency bornées, et stabiliser un lifecycle/progress/outcome avec notifications/listeners sûrs pour les couches supérieures. - [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.
- [ ] `0.3.7`Introduire une application spécialisée de backfill/inspection RAW consommant `ksp-job-api` : lancement/annulation contrôlés, état et progression live, compteurs, checkpoint/frontière, outcome terminal et inspection RAW sans parser les logs ni connaître les providers/backends physiques. - [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-worker-api` comme API générique de lifecycle/health/progression pour services continus, en reprenant le pattern de notifications/listeners stabilisé par Job tout en gardant les sémantiques Worker distinctes des jobs terminables et des wake-ups Store post-commit. - [ ] `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.
- [ ] `0.3.9` — Introduire `ksp-worker-live-transactions-retriever-lib` pour l'acquisition continue `RawTransaction` via les surfaces live de `ksp-onchain-transport-lib`, persistence par `ksp-store-lib`, reprise/backpressure/idempotence et notifications `ksp-worker-api`, sans decode Program ni dépendance backend/provider directe. - [ ] `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 stabilisé par Job tout en gardant les sémantiques Worker distinctes des jobs terminables et des wake-ups Store post-commit.
- [ ] `0.3.10` — Introduire une application de monitoring/visualisation Jobs + Workers : vue graphique des lifecycles, health/progression, rates/backpressure/retries sûrs, checkpoints et outcomes ; l'application consomme les APIs publiques Job/Worker et ne devient ni scheduler caché ni source de vérité du backlog. - [ ] `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.
- [ ] `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`.
### 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** — 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/IDEAS — taxonomie N1, processing et rétention ### TODO/IDEAS — taxonomie N1, processing et rétention
@@ -127,7 +133,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [ ] 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 N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné. - [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
- [ ] Ajouter worker/service STRUCTURAL et l'application de contrôle/inspection utile. - [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
## Séries DECODED/DOMAIN/EXECUTION — progression verticale ## Séries DECODED/DOMAIN/EXECUTION — progression verticale

View File

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "mainnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "devnet"
}
]
},
{
"profile_id": "mainnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "supertrace"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "mainnet_backfill_pool"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "mainnet"
}
]
},
{
"profile_id": "testnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "console_info"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "publicnode_testnet"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "testnet"
}
]
}
]
}

View File

@@ -16,7 +16,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,
@@ -41,7 +41,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,
@@ -66,7 +66,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,

View File

@@ -178,6 +178,101 @@
} }
] ]
}, },
{
"profile_id": "mainnet_backfill_pool",
"endpoints": [
{
"name": "solana_mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "backfill_pool",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
},
{
"role": "backfill_solana_public",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "publicnode_solana_mainnet_http",
"enabled": true,
"provider": "publicnode",
"cluster": "mainnet-beta",
"url": "https://solana-rpc.publicnode.com",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "backfill_pool",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
},
{
"role": "backfill_publicnode",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_mainnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
}
]
},
{ {
"profile_id": "publicnode_mainnet", "profile_id": "publicnode_mainnet",
"endpoints": [ "endpoints": [

View File

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

View File

@@ -0,0 +1,105 @@
<!-- file: crates/ksp-app-backfill-desk/README.md -->
<!-- version: 1 -->
# `ksp-app-backfill-desk`
`ksp-app-backfill-desk` est l'application desktop spécialisée de contrôle d'un backfill historique `RawTransaction` KSP.
Elle reste une couche de composition Tauri : Config sélectionne les profils, Transport possède les endpoints/rôles/retry/rate-limit, Store possède la persistence et le backend, et `ksp-job-backfill-lib` possède découverte, hydratation, frontier/checkpoint et lifecycle du job.
## Package
```text
package : ksp-app-backfill-desk
lib : ksp_app_backfill_desk_lib
bin : ksp-app-backfill-desk
```
Le binaire est un launcher mince. La bibliothèque applicative possède le bootstrap, l'état Rust single-run, les DTOs Tauri, les commands et le bridge latest-value vers le frontend.
## Composition
```text
ksp-config-lib -> composite, profils, secrets et runtime packagé
ksp-core-lib -> erreurs, Pubkey et registre Program IDs
ksp-onchain-transport-lib -> HTTP pool, rôles, admission, retry/rate-limit
ksp-store-lib -> façade Store backend-neutral
ksp-job-api -> lifecycle Job commun
ksp-job-backfill-lib -> request/runtime/checkpoint/snapshots Backfill
ksp-logging-lib -> logging/tracing applicatif
ksp-app-backfill-desk -> orchestration Tauri + projections sûres
```
L'application ne dépend pas directement de `ksp-store-api`, `ksp-store-postgres-lib`, `tokio-postgres`, `reqwest`, `tonic` ou `yellowstone-grpc-proto`.
## Capacités
La surface utilisateur comprend :
- quatre scopes : latest address, before address, after address et explicit signatures ;
- engagements `finalized` et `confirmed` ;
- sélection d'un rôle HTTP logique compatible avec `getSignaturesForAddress` et `getTransaction` ;
- bornes page/candidats/concurrence validées côté Rust par le contrat Backfill ;
- validation de la requête avant Start ;
- un seul run actif ;
- monitoring latest-value par événement `ksp-backfill-status` avec resynchronisation explicite ;
- Cancel ciblé par JobId backend et idempotent ;
- Resume in-session lorsqu'un checkpoint terminal est disponible ;
- autocomplete libre des Program IDs provenant du registre canonique `ksp-core-lib`.
L'autocomplete n'est jamais une allow-list : une adresse Solana valide non enregistrée reste saisissable.
## Checkpoint et Resume
Le checkpoint concret ne traverse jamais IPC. Le backend conserve uniquement en mémoire Rust le dernier terminal compatible et la requête nécessaire à une reprise dans la même session.
Un Resume crée un nouveau JobId backend. `ksp-job-backfill-lib` valide le checkpoint contre la requête d'origine puis le réémet pour ce nouveau Job sans modifier le scope fingerprint ni la frontier. Il n'existe pas de checkpoint durable de Backfill Desk après redémarrage de l'application.
## Config et réseaux
Le composite dédié est :
```text
cfg.composite.ksp-app-backfill-desk
```
Les profils committed couvrent `mainnet`, `devnet` et `testnet`. Mainnet est le profil par défaut. Transport et Store doivent sélectionner exactement le même réseau avant que la composition soit déclarée ready.
Les profils Store committed utilisent actuellement TLS PostgreSQL `disabled`; cette politique de profil n'enlève pas le support `verify_full` du Store/schema.
Le frontend ne reçoit jamais URI Store, URL RPC, credential provider, secret Config ou backend physique.
## Monitoring et sécurité
Le monitoring expose des compteurs et codes sûrs : lifecycle, phase, boundary, candidats, entités/observations, missing/conflicts/holes, concurrence maximale, frontier contiguë, présence de checkpoint et éventuel domain/code d'échec.
Il n'expose pas :
- payload RAW ;
- adresse ou signatures de campagne ;
- checkpoint/cursor concret ;
- endpoint URL ou credential ;
- contexte d'erreur arbitraire.
Le frontend ne possède ni accès direct filesystem/réseau, ni persistence navigateur de données Backfill. Les capabilities Tauri guest restent `core:default` et `tracing:default`.
## Ports et développement
```text
Vite HTTP : 1436
Vite WS : 1437
```
Lancement :
```bash
(cd crates/ksp-app-backfill-desk && cargo tauri dev)
```
Build production :
```bash
(cd crates/ksp-app-backfill-desk && cargo tauri build)
```
Voir également [`USAGE.md`](USAGE.md), [`../ksp-job-backfill-lib/README.md`](../ksp-job-backfill-lib/README.md), [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) et [`../../docs/validation/024-V0_3_7_BACKFILL_DESK.md`](../../docs/validation/024-V0_3_7_BACKFILL_DESK.md).

View File

@@ -0,0 +1,145 @@
<!-- file: crates/ksp-app-backfill-desk/USAGE.md -->
<!-- version: 1 -->
# Utilisation de `ksp-app-backfill-desk`
## 1. Lancement
Depuis la racine du workspace :
```bash
(cd crates/ksp-app-backfill-desk && cargo tauri dev)
```
Le runtime charge le composite Backfill Desk via `ksp-config-lib`, construit le pool HTTP, ouvre Store puis vérifie que les deux surfaces utilisent le même réseau avant d'autoriser une campagne.
## 2. Choix du profil et readiness
Le composite `cfg.composite.ksp-app-backfill-desk` utilise Mainnet par défaut. Devnet et Testnet restent disponibles via la sélection Config du profil composite.
Avant Start, l'écran indique séparément :
```text
Transport ready
Store ready
Network coherent
Composition ready
```
Une composition non ready doit être corrigée côté Config/runtime ; le frontend ne peut pas fournir une URL RPC, une URI Store ou un secret pour contourner ces contrôles.
## 3. Adresse et autocomplete Program IDs
Le champ adresse accepte toute Pubkey Solana valide. Le `datalist` propose en complément les Program IDs du registre canonique `ksp-core-lib` avec leurs metadata publiques.
Choisir une proposition remplit simplement le champ ; une adresse absente du dataset reste autorisée. Le dataset n'est pas une allow-list.
## 4. Scopes
Les scopes disponibles sont :
```text
latest_address
before_address
after_address
explicit_signatures
```
`latest_address` recherche l'historique récent d'une adresse. `before_address` et `after_address` ajoutent une signature d'ancrage selon la sémantique Backfill. `explicit_signatures` traite une liste explicite de signatures et n'accepte pas `min_context_slot`.
Les signatures et adresses sont revalidées côté Rust ; leur présence dans le formulaire ne les rend pas persistantes dans le frontend.
## 5. Engagement, route HTTP et bornes
Les engagements disponibles sont `finalized` et `confirmed`.
Le sélecteur HTTP expose uniquement des rôles logiques que le backend a vérifiés compatibles avec les deux méthodes nécessaires au Backfill. Le rôle pool permet au Transport de choisir/rerouter entre endpoints selon ses propres règles ; les rôles ciblés permettent de sélectionner une route logique spécifique sans exposer l'URL physique.
Les bornes de campagne comprennent notamment :
```text
page size
max pages
max candidates
hydration concurrency
min context slot
```
Les limites maximales proviennent de `ksp-job-backfill-lib` et sont revalidées lors de la conversion vers `BackfillRequest`.
## 6. Valider puis démarrer
**Valider la requête** projette un résumé sûr de la campagne sans démarrer le job.
**Démarrer** crée ensuite un JobId backend et installe le handle de contrôle avant le spawn asynchrone. Un second Start est refusé tant qu'un run est actif, y compris pendant `cancelling`.
## 7. Monitoring latest-value
La carte de monitoring est alimentée par les snapshots latest-value du runtime. Le backend émet `ksp-backfill-status`; **Resynchroniser** appelle également `backfill_status` afin de récupérer explicitement le dernier état complet.
Les informations utiles incluent :
- lifecycle et phase ;
- scope et discovery boundary ;
- candidats selected/admitted/finished ;
- entités et observations inserted/existing ;
- purged/missing/conflicts/holes/cancelled candidates ;
- maximum in flight ;
- contiguous completed ;
- présence d'un checkpoint ;
- domain/code stable d'échec éventuel.
Le monitoring ne doit pas être interprété depuis les logs texte.
## 8. Annuler
**Annuler** envoie le JobId actuellement affiché. Cette cible protège un nouveau run contre une requête Cancel retardée destinée au run précédent.
Le premier Cancel pré-terminal demande une annulation coopérative. Une répétition est idempotente. Le lifecycle peut passer par `cancelling`; les opérations déjà admises vers la persistence ne sont pas présentées comme brutalement interrompues.
## 9. Reprendre
**Reprendre** est disponible uniquement lorsqu'un snapshot terminal retenu contient un checkpoint. La commande n'accepte aucun checkpoint ou payload de campagne depuis le frontend.
Le backend :
1. exige qu'aucun run ne soit actif ;
2. récupère la requête terminale Rust-only ;
3. revalide le réseau Store et le rôle HTTP courant ;
4. génère un nouveau JobId ;
5. demande à `ksp-job-backfill-lib` de réémettre le checkpoint pour ce JobId ;
6. relance le même runtime/monitoring.
La reprise est limitée à la session courante. Fermer l'application supprime ce checkpoint applicatif en mémoire.
## 10. Fermeture de l'application
La fermeture de la fenêtre principale bloque d'abord tout nouveau Start, tente une annulation coopérative du run actif puis ferme Store avant l'exit.
Le shutdown applicatif n'est pas un mécanisme de checkpoint durable ou de reprise après crash.
## 11. Données qui ne traversent pas le frontend
Backfill Desk ne projette jamais :
```text
URI Store
URL RPC
credentials/secrets
payload RAW
checkpoint/cursor concret
handles Store/Transport/Job
contexte d'erreur arbitraire
```
Les actions utilisateur et transitions sont instrumentées via Logging KSP sans journaliser adresse, signature ou payload métier.
## 12. Runtime packagé
Le build Tauri embarque les documents Config/schemas enregistrés. `ksp-config-lib` prépare ensuite la racine KSP user-writable et conserve les Config utilisateur existantes ; `.env` n'est jamais embarqué.
Le build production est :
```bash
(cd crates/ksp-app-backfill-desk && cargo tauri build)
```

View File

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

View File

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

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,231 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Backfill Desk — Backfill</title>
</head>
<body>
<header class="app-header">
<nav class="navbar h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 flex-nowrap">
<div class="navbar-brand d-flex align-items-center me-4 flex-nowrap">
<img alt="KSP" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-semibold text-primary text-nowrap">Backfill Desk</span>
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span>
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Backfill</span>
</div>
</div>
</nav>
</header>
<main class="app-main container-fluid p-0">
<div class="row g-0 h-100 flex-nowrap">
<aside class="col-auto app-sidebar border-end bg-body-tertiary">
<div class="app-sidebar-scroll h-100" data-simplebar>
<nav class="nav nav-pills flex-column gap-1 p-3" aria-label="Navigation Backfill Desk">
<button class="nav-link text-start active" type="button" data-view="backfill">
<i class="fa-solid fa-clock-rotate-left me-2" aria-hidden="true"></i>Backfill
</button>
<button class="nav-link text-start" type="button" data-view="diagnostics">
<i class="fa-solid fa-stethoscope me-2" aria-hidden="true"></i>Diagnostics
</button>
</nav>
</div>
</aside>
<section class="col app-content app-scrollable h-100" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="card app-shell-card mx-auto shadow-sm border-0">
<div class="card-body p-4">
<section data-view-panel="backfill">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Backfill</h1>
<p class="text-body-secondary mb-0">Mainnet par défaut, transport HTTP sélectionnable et mapping de campagne validé côté Rust avant tout démarrage de job.</p>
</div>
<span id="appVersionBadge" class="badge text-bg-secondary"></span>
</div>
<div class="row g-4">
<div class="col-xl-4">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Transport HTTP</div>
<div class="card-body">
<label for="backfillHttpRoute" class="form-label">Route de lecture</label>
<select id="backfillHttpRoute" class="form-select" disabled>
<option value="">Chargement des routes…</option>
</select>
<div id="backfillHttpRouteHelp" class="form-text">Le choix reste logique : aucune URL ni clé provider n'est exposée au frontend.</div>
<dl class="row mt-4 mb-0 app-runtime-list">
<dt class="col-sm-5">Réseau</dt><dd id="backfillNetwork" class="col-sm-7"></dd>
<dt class="col-sm-5">Transport</dt><dd id="backfillTransportReady" class="col-sm-7"></dd>
<dt class="col-sm-5">Store</dt><dd id="backfillStoreReady" class="col-sm-7"></dd>
<dt class="col-sm-5">Composition</dt><dd id="backfillCompositionReady" class="col-sm-7"></dd>
</dl>
<div id="backfillOptionsDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>
</div>
</div>
<div class="col-xl-8">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Campagne Backfill</div>
<div class="card-body">
<form id="backfillCampaignForm" novalidate>
<div class="row g-3">
<div class="col-lg-6">
<label for="backfillScopeKind" class="form-label">Scope</label>
<select id="backfillScopeKind" class="form-select" disabled></select>
</div>
<div class="col-lg-6">
<label for="backfillCommitment" class="form-label">Commitment</label>
<select id="backfillCommitment" class="form-select" disabled></select>
</div>
<div id="backfillAddressGroup" class="col-12">
<label for="backfillAddress" class="form-label">Adresse Solana</label>
<input id="backfillAddress" class="form-control font-monospace" type="text" list="backfillProgramIds" autocomplete="off" spellcheck="false" aria-describedby="backfillProgramIdAutocompleteHelp">
<datalist id="backfillProgramIds"></datalist>
<div id="backfillProgramIdAutocompleteHelp" class="form-text">Suggestions issues du registre canonique KSP ; toute adresse Solana valide reste acceptée.</div>
</div>
<div id="backfillAnchorGroup" class="col-12" hidden>
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
<input id="backfillAnchorSignature" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false">
</div>
<div id="backfillExplicitSignaturesGroup" class="col-12" hidden>
<label for="backfillExplicitSignatures" class="form-label">Signatures explicites</label>
<textarea id="backfillExplicitSignatures" class="form-control font-monospace" rows="5" autocomplete="off" spellcheck="false"></textarea>
<div id="backfillExplicitSignaturesHelp" class="form-text">Une signature par ligne.</div>
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillPageSize" class="form-label">Page size</label>
<input id="backfillPageSize" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillMaxPages" class="form-label">Max pages</label>
<input id="backfillMaxPages" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillMaxCandidates" class="form-label">Max candidates</label>
<input id="backfillMaxCandidates" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillHydrationConcurrency" class="form-label">Concurrence</label>
<input id="backfillHydrationConcurrency" class="form-control" type="number" step="1">
</div>
<div id="backfillMinContextSlotGroup" class="col-lg-6">
<label for="backfillMinContextSlot" class="form-label">Min context slot</label>
<input id="backfillMinContextSlot" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" placeholder="optionnel">
<div class="form-text">Chaîne décimale validée côté Rust pour préserver la précision u64.</div>
</div>
</div>
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
<button id="validateBackfillRequest" class="btn btn-outline-primary" type="submit" disabled>
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
</button>
<button id="startBackfillRequest" class="btn btn-primary" type="button" disabled>
<i class="fa-solid fa-play me-2" aria-hidden="true"></i>Démarrer le backfill
</button>
<span class="text-body-secondary small">Un seul run peut être actif ; le monitoring latest-value est coalescé côté backend.</span>
</div>
</form>
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>
<div id="backfillRunStatusCard" class="card shadow-sm mt-4" hidden>
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
<span class="fw-semibold">Monitoring latest-value</span>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button id="cancelBackfillRun" class="btn btn-outline-danger btn-sm" type="button" disabled>
<i class="fa-solid fa-ban me-2" aria-hidden="true"></i>Annuler
</button>
<button id="resumeBackfillRun" class="btn btn-outline-success btn-sm" type="button" disabled>
<i class="fa-solid fa-forward-step me-2" aria-hidden="true"></i>Reprendre
</button>
<button id="refreshBackfillStatus" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Resynchroniser
</button>
</div>
</div>
<div class="card-body">
<div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<div id="backfillResumeFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<div id="backfillTerminalSummary" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Job</dt><dd id="runStatusJobId" class="col-sm-7 font-monospace"></dd>
<dt class="col-sm-5">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runStatusPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Séquence</dt><dd id="runStatusSequence" class="col-sm-7 font-monospace"></dd>
<dt class="col-sm-5">Scope</dt><dd id="runStatusScope" class="col-sm-7"></dd>
<dt class="col-sm-5">Boundary</dt><dd id="runStatusBoundary" class="col-sm-7"></dd>
<dt class="col-sm-5">Candidates sel./adm./fin.</dt><dd id="runStatusCandidates" class="col-sm-7"></dd>
<dt class="col-sm-5">Frontier / holes</dt><dd id="runStatusFrontier" class="col-sm-7"></dd>
<dt class="col-sm-5">RAW ins./exist./purged</dt><dd id="runStatusEntities" class="col-sm-7"></dd>
<dt class="col-sm-5">Observations ins./exist.</dt><dd id="runStatusObservations" class="col-sm-7"></dd>
<dt class="col-sm-5">Anomalies</dt><dd id="runStatusAnomalies" class="col-sm-7"></dd>
<dt class="col-sm-5">Max in-flight</dt><dd id="runStatusConcurrency" class="col-sm-7"></dd>
<dt class="col-sm-5">Checkpoint</dt><dd id="runStatusCheckpoint" class="col-sm-7"></dd>
<dt class="col-sm-5">Failure</dt><dd id="runStatusFailure" class="col-sm-7"></dd>
</dl>
</div>
</div>
<div id="backfillRequestPreview" class="card bg-body-tertiary border-0 mt-3" hidden>
<div class="card-body">
<h2 class="h6 mb-3">Projection backend sûre</h2>
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Réseau</dt><dd id="validatedNetwork" class="col-sm-7"></dd>
<dt class="col-sm-5">Route HTTP</dt><dd id="validatedHttpRole" class="col-sm-7"></dd>
<dt class="col-sm-5">Commitment</dt><dd id="validatedCommitment" class="col-sm-7"></dd>
<dt class="col-sm-5">Scope</dt><dd id="validatedScopeKind" class="col-sm-7"></dd>
<dt class="col-sm-5">Signatures explicites</dt><dd id="validatedExplicitSignatureCount" class="col-sm-7"></dd>
<dt class="col-sm-5">page/pages/candidates/concurrence</dt><dd id="validatedBounds" class="col-sm-7"></dd>
<dt class="col-sm-5">Min context slot</dt><dd id="validatedMinContextSlot" class="col-sm-7"></dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Diagnostics</h1>
<p class="text-body-secondary mb-0">État sûr du scaffold Config, Logging et desktop.</p>
</div>
<button id="refreshRuntimeStatus" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card shadow-sm">
<div class="card-header fw-semibold">Runtime</div>
<div class="card-body">
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Documents Config</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
</dl>
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>
</div>
</section>
</div>
</div>
</div>
</section>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,778 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
import type { BackfillCancelResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts";
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
import type { BackfillResumeResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts";
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts";
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
import type { ProgramIdAutocompleteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
type ViewId = "backfill" | "diagnostics";
const viewTitles: Record<ViewId, string> = {
backfill: "Backfill",
diagnostics: "Diagnostics",
};
const commitmentLabels: Record<string, string> = {
confirmed: "confirmed",
finalized: "finalized",
};
const scopeLabels: Record<string, string> = {
latest_address: "Adresse — historique le plus récent",
before_address: "Adresse — avant une signature",
after_address: "Adresse — après une signature",
explicit_signatures: "Signatures explicites",
};
let compositionReadyForStart = false;
let currentRunStatus: BackfillRunStatusDto | null = null;
let runStartAccepted = false;
let resumeInFlight = false;
function isViewId(value: string | undefined): value is ViewId {
return value === "backfill" || value === "diagnostics";
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
panel.hidden = panel.dataset.viewPanel !== viewId;
});
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
});
const header = document.querySelector<HTMLElement>("#headerViewTitle");
if (header) {
header.textContent = viewTitles[viewId];
}
document.title = `Backfill Desk — ${viewTitles[viewId]}`;
frontendTrace("main", "Backfill Desk view DOM updated", { viewId, source });
}
function bindFrontendInteractions(): void {
document.addEventListener(
"click",
event => {
const source = event.target;
if (!(source instanceof Element)) {
return;
}
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
if (!control) {
return;
}
frontendTrace("main", "Backfill Desk frontend control clicked", {
controlId: control.id || null,
role: control.getAttribute("role"),
tagName: control.tagName.toLowerCase(),
viewId: control.dataset.view ?? null,
});
},
true,
);
frontendTrace("main", "Backfill Desk frontend control interaction logger installed");
}
function bindNavigation(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const viewId = button.dataset.view;
frontendDebug("main", "Backfill Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
if (isViewId(viewId)) {
activateView(viewId, "user");
}
});
});
frontendTrace("main", "Backfill Desk navigation handlers installed");
}
function populateCodeSelect(selectId: string, values: string[], labels: Record<string, string>): void {
const select = document.querySelector<HTMLSelectElement>(`#${selectId}`);
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const value of values) {
const option = document.createElement("option");
option.value = value;
option.textContent = labels[value] ?? value;
select.append(option);
}
select.disabled = values.length === 0;
if (values.includes(previous)) {
select.value = previous;
}
}
function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input) {
return;
}
input.min = "1";
input.max = maximum.toString();
if (input.value === "") {
input.value = defaultValue.toString();
}
}
function refreshStartButton(): void {
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
if (startButton) {
startButton.disabled = !compositionReadyForStart || runStartAccepted;
}
}
function renderProgramIdAutocomplete(options: ProgramIdAutocompleteOptionDto[]): void {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return;
}
datalist.replaceChildren();
for (const program of options) {
const option = document.createElement("option");
option.value = program.programId;
option.label = `${program.name}${program.code}`;
option.dataset.code = program.code;
option.dataset.domain = program.domain;
option.dataset.family = program.family;
option.dataset.name = program.name;
option.dataset.protocol = program.protocol;
datalist.append(option);
}
const help = document.querySelector<HTMLElement>("#backfillProgramIdAutocompleteHelp");
if (help) {
help.textContent = `${options.length} Program ID KSP disponibles comme suggestions ; la saisie reste libre.`;
}
frontendTrace("main", "Backfill Desk Program ID autocomplete dataset rendered", { optionCount: options.length });
}
function selectedProgramAutocompleteEntry(address: string): HTMLOptionElement | null {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return null;
}
for (const option of Array.from(datalist.options)) {
if (option.value === address) {
return option;
}
}
return null;
}
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
return fallback;
}
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
renderProgramIdAutocomplete(options.programIdOptions);
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
const signatureHelp = document.querySelector<HTMLElement>("#backfillExplicitSignaturesHelp");
if (signatureHelp) {
signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
}
const validateButton = document.querySelector<HTMLButtonElement>("#validateBackfillRequest");
if (validateButton) {
validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
}
updateCampaignScopeFields();
frontendTrace("main", "Backfill Desk campaign contract rendered", {
commitmentCount: options.commitments.length,
compositionReady: options.compositionReady,
scopeCount: options.scopeKinds.length,
});
}
function currentScopeKind(): string {
const select = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
return select?.value ?? "";
}
function updateCampaignScopeFields(): void {
const scopeKind = currentScopeKind();
const addressGroup = document.querySelector<HTMLElement>("#backfillAddressGroup");
const anchorGroup = document.querySelector<HTMLElement>("#backfillAnchorGroup");
const signaturesGroup = document.querySelector<HTMLElement>("#backfillExplicitSignaturesGroup");
const minContextSlotGroup = document.querySelector<HTMLElement>("#backfillMinContextSlotGroup");
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
const explicitScope = scopeKind === "explicit_signatures";
if (addressGroup) {
addressGroup.hidden = !addressScope;
}
if (anchorGroup) {
anchorGroup.hidden = !anchoredScope;
}
if (signaturesGroup) {
signaturesGroup.hidden = !explicitScope;
}
if (minContextSlotGroup) {
minContextSlotGroup.hidden = explicitScope;
}
frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
}
function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = true;
status.textContent = "";
status.classList.remove("alert-success", "alert-danger");
status.classList.add("alert-secondary");
}
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
if (summary) {
summary.hidden = true;
}
frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
}
function numericInputValue(inputId: string): number {
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
return 0;
}
return input.valueAsNumber;
}
function rawOptionalInput(inputId: string, enabled: boolean): string | null {
if (!enabled) {
return null;
}
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input || input.value === "") {
return null;
}
return input.value;
}
function explicitSignatureLines(): string[] {
const textarea = document.querySelector<HTMLTextAreaElement>("#backfillExplicitSignatures");
if (!textarea || currentScopeKind() !== "explicit_signatures") {
return [];
}
return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
}
function buildBackfillStartRequest(): BackfillStartRequestDto {
const scopeKind = currentScopeKind();
const role = document.querySelector<HTMLSelectElement>("#backfillHttpRoute")?.value ?? "";
const commitment = document.querySelector<HTMLSelectElement>("#backfillCommitment")?.value ?? "";
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
const minContextSlotEnabled = scopeKind !== "explicit_signatures";
return {
address: rawOptionalInput("backfillAddress", addressScope),
anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
commitment,
explicitSignatures: explicitSignatureLines(),
hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
httpRole: role,
maxCandidates: numericInputValue("backfillMaxCandidates"),
maxPages: numericInputValue("backfillMaxPages"),
minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
pageSize: numericInputValue("backfillPageSize"),
scopeKind,
};
}
function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
const values: Record<string, string> = {
validatedNetwork: preview.network,
validatedHttpRole: preview.httpRole,
validatedCommitment: preview.commitment,
validatedScopeKind: preview.scopeKind,
validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
if (summary) {
summary.hidden = false;
}
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = false;
status.classList.remove("alert-secondary", "alert-danger");
status.classList.add("alert-success");
status.textContent = "Requête valide. Aucun job n'a été démarré.";
}
frontendTrace("main", "Backfill Desk validated request preview rendered", {
commitment: preview.commitment,
httpRole: preview.httpRole,
scopeKind: preview.scopeKind,
});
}
function renderRequestValidationError(caughtError: unknown): void {
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (!status) {
return;
}
const message = safeCommandErrorText(caughtError, "Requête refusée par le backend.");
status.hidden = false;
status.classList.remove("alert-secondary", "alert-success");
status.classList.add("alert-danger");
status.textContent = message;
}
async function startBackfillRequest(): Promise<void> {
const request = buildBackfillStartRequest();
frontendDebug("main", "Backfill Desk campaign Start requested", {
commitment: request.commitment,
httpRole: request.httpRole,
scopeKind: request.scopeKind,
});
try {
const started = await invokeKsp<BackfillStartResponseDto>("main", "backfill_start", { request });
runStartAccepted = true;
refreshStartButton();
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = false;
status.classList.remove("alert-secondary", "alert-danger");
status.classList.add("alert-success");
status.textContent = `Job ${started.jobId} démarré (${started.state}).`;
}
await syncBackfillStatus("start");
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
} catch (caughtError) {
renderRequestValidationError(caughtError);
frontendWarn("main", "Backfill Desk campaign Start failed");
}
}
async function validateBackfillRequest(): Promise<void> {
const request = buildBackfillStartRequest();
frontendDebug("main", "Backfill Desk campaign validation requested", {
commitment: request.commitment,
httpRole: request.httpRole,
scopeKind: request.scopeKind,
});
try {
const preview = await invokeKsp<BackfillRequestPreviewDto>("main", "backfill_validate_request", { request });
renderRequestPreview(preview);
frontendDebug("main", "Backfill Desk campaign validation completed", {
commitment: preview.commitment,
httpRole: preview.httpRole,
scopeKind: preview.scopeKind,
});
} catch (caughtError) {
renderRequestValidationError(caughtError);
frontendWarn("main", "Backfill Desk campaign validation failed");
}
}
function bindCampaignForm(): void {
const scope = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
if (scope) {
scope.addEventListener("change", () => {
frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
updateCampaignScopeFields();
invalidateCampaignValidation("scope");
});
}
const form = document.querySelector<HTMLFormElement>("#backfillCampaignForm");
if (form) {
form.addEventListener("input", () => invalidateCampaignValidation("input"));
form.addEventListener("submit", event => {
event.preventDefault();
void validateBackfillRequest();
});
}
const address = document.querySelector<HTMLInputElement>("#backfillAddress");
if (address) {
address.addEventListener("change", () => {
const selected = selectedProgramAutocompleteEntry(address.value);
frontendTrace("main", "Backfill Desk address autocomplete selection evaluated", {
programCode: selected?.dataset.code ?? null,
registryMatch: selected !== null,
});
});
}
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
if (startButton) {
startButton.addEventListener("click", () => {
void startBackfillRequest();
});
}
frontendTrace("main", "Backfill Desk campaign form handlers installed");
}
function routeLabel(route: BackfillHttpRouteOptionDto): string {
if (route.pooled) {
return `Pool multi-provider — ${route.providers.join(" + ")}`;
}
return route.providers.length === 1 ? `Provider — ${route.providers[0]}` : route.role;
}
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
compositionReadyForStart = options.compositionReady;
renderCampaignContract(options);
refreshStartButton();
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
if (select) {
const previous = select.value;
select.replaceChildren();
for (const route of options.httpRoutes) {
const option = document.createElement("option");
option.value = route.role;
option.textContent = routeLabel(route);
select.append(option);
}
select.disabled = !options.transportReady || options.httpRoutes.length === 0;
const reusable = options.httpRoutes.some(route => route.role === previous);
if (reusable) {
select.value = previous;
}
}
const values: Record<string, string> = {
backfillNetwork: options.configuredNetworks.length === 1 ? options.configuredNetworks[0] : options.configuredNetworks.join(", ") || "indisponible",
backfillTransportReady: options.transportReady ? "ready" : "non ready",
backfillStoreReady: options.storeReady ? "ready" : "non ready",
backfillCompositionReady: options.compositionReady ? "ready" : "non ready",
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
const diagnostic = document.querySelector<HTMLElement>("#backfillOptionsDiagnostic");
if (diagnostic) {
const error = options.transportDiagnostic ?? options.storeDiagnostic;
diagnostic.hidden = error === null;
diagnostic.textContent = error === null ? "" : `${error.domain}/${error.code}: ${error.message}`;
}
frontendTrace("main", "Backfill Desk HTTP route options rendered", {
compositionReady: options.compositionReady,
routeCount: options.httpRoutes.length,
transportReady: options.transportReady,
});
}
async function loadBackfillOptions(source: "startup" | "user"): Promise<void> {
frontendDebug("main", "Backfill Desk HTTP route options load started", { source });
const options = await invokeKsp<BackfillDeskOptionsDto>("main", "backfill_options");
renderBackfillOptions(options);
frontendDebug("main", "Backfill Desk HTTP route options load completed", { source, routeCount: options.httpRoutes.length });
}
function bindBackfillRouteSelection(): void {
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
if (!select) {
return;
}
select.addEventListener("change", () => {
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
invalidateCampaignValidation("route");
});
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
}
function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "cancel" | "event" | "resume" | "startup" | "start" | "user"): void {
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
currentRunStatus = status;
if (!status) {
runStartAccepted = false;
if (cancelButton) {
cancelButton.disabled = true;
}
if (resumeButton) {
resumeButton.disabled = true;
}
resumeInFlight = false;
refreshStartButton();
if (card) {
card.hidden = true;
}
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = true;
terminalSummary.textContent = "";
}
frontendTrace("main", "Backfill Desk latest-value status cleared", { source });
return;
}
runStartAccepted = status.active;
if (cancelButton) {
cancelButton.disabled = !status.active;
}
if (status.active) {
resumeInFlight = false;
}
if (resumeButton) {
resumeButton.disabled = !status.terminal || !status.checkpointPresent || resumeInFlight;
}
refreshStartButton();
if (card) {
card.hidden = false;
}
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = !status.terminal;
terminalSummary.classList.remove("alert-success", "alert-warning", "alert-danger", "alert-secondary");
if (status.terminal) {
const failure = status.failureDomain === null || status.failureCode === null ? null : `${status.failureDomain}/${status.failureCode}`;
if (status.state === "completed") {
terminalSummary.classList.add("alert-success");
terminalSummary.textContent = `Campagne terminée — ${status.contiguousCompleted} candidats contigus durablement traités.`;
} else if (status.state === "cancelled") {
terminalSummary.classList.add("alert-warning");
terminalSummary.textContent = `Campagne annulée coopérativement — checkpoint ${status.checkpointPresent ? "disponible" : "absent"}.`;
} else {
terminalSummary.classList.add("alert-danger");
terminalSummary.textContent = `Campagne terminée en échec${failure === null ? "" : `${failure}`}.`;
}
} else {
terminalSummary.classList.add("alert-secondary");
terminalSummary.textContent = "";
}
}
const values: Record<string, string> = {
runStatusJobId: status.jobId,
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
runStatusPhase: status.phase,
runStatusSequence: status.sequence,
runStatusScope: status.scopeKind,
runStatusBoundary: status.discoveryBoundary ?? "—",
runStatusCandidates: `${status.candidatesSelected} / ${status.candidatesAdmitted} / ${status.candidatesFinished}`,
runStatusFrontier: `${status.contiguousCompleted} (holes ${status.holes})`,
runStatusEntities: `${status.entitiesInserted} / ${status.entitiesExisting} / ${status.entitiesPurged}`,
runStatusObservations: `${status.observationsInserted} / ${status.observationsExisting}`,
runStatusAnomalies: `missing ${status.missing} / conflits ${status.conflicts} / annulés ${status.cancelledCandidates}`,
runStatusConcurrency: status.maximumInFlight.toString(),
runStatusCheckpoint: status.checkpointPresent ? "présent" : "absent",
runStatusFailure: status.failureDomain === null || status.failureCode === null ? "—" : `${status.failureDomain}/${status.failureCode}`,
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
frontendTrace("main", "Backfill Desk latest-value status rendered", {
active: status.active,
phase: status.phase,
sequence: status.sequence,
source,
state: status.state,
terminal: status.terminal,
});
}
async function syncBackfillStatus(source: "cancel" | "resume" | "startup" | "start" | "user"): Promise<void> {
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
renderBackfillRunStatus(status, source);
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
}
async function resumeBackfillRun(): Promise<void> {
const status = currentRunStatus;
if (!status || !status.terminal || !status.checkpointPresent || resumeInFlight) {
return;
}
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
resumeInFlight = true;
if (resumeButton) {
resumeButton.disabled = true;
}
frontendDebug("main", "Backfill Desk in-session Resume requested", { jobId: status.jobId, state: status.state });
try {
const response = await invokeKsp<BackfillResumeResponseDto>("main", "backfill_resume");
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
if (feedback) {
feedback.hidden = false;
feedback.textContent = `Checkpoint repris dans ${response.jobId} (${response.state}).`;
}
frontendInfo("main", "Backfill Desk in-session Resume accepted", {
accepted: response.accepted,
jobId: response.jobId,
state: response.state,
});
await syncBackfillStatus("resume");
} catch (caughtError) {
resumeInFlight = false;
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
resumeButton.disabled = false;
}
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La reprise du checkpoint a été refusée.");
}
frontendWarn("main", "Backfill Desk in-session Resume failed");
}
}
async function bindBackfillStatusMonitoring(): Promise<void> {
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
renderBackfillRunStatus(event.payload, "event");
});
const button = document.querySelector<HTMLButtonElement>("#refreshBackfillStatus");
if (button) {
button.addEventListener("click", () => {
frontendDebug("main", "Backfill Desk latest-value status refresh button clicked");
void syncBackfillStatus("user").catch(() => frontendWarn("main", "Backfill Desk latest-value status refresh failed"));
});
}
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
if (cancelButton) {
cancelButton.addEventListener("click", () => {
const status = currentRunStatus;
if (!status || !status.active) {
return;
}
cancelButton.disabled = true;
frontendDebug("main", "Backfill Desk cooperative cancellation requested", { jobId: status.jobId, state: status.state });
void invokeKsp<BackfillCancelResponseDto>("main", "backfill_cancel", { jobId: status.jobId })
.then(response => {
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
if (feedback) {
feedback.hidden = false;
feedback.textContent = response.accepted
? `Annulation coopérative acceptée pour ${response.jobId}.`
: `Aucune nouvelle annulation acceptée pour ${response.jobId} (${response.state}).`;
}
frontendInfo("main", "Backfill Desk cooperative cancellation response received", {
accepted: response.accepted,
jobId: response.jobId,
state: response.state,
});
return syncBackfillStatus("cancel");
})
.catch(caughtError => {
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La demande d'annulation a été refusée.");
}
frontendWarn("main", "Backfill Desk cooperative cancellation failed");
if (currentRunStatus?.active) {
cancelButton.disabled = false;
}
});
});
}
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
if (resumeButton) {
resumeButton.addEventListener("click", () => {
void resumeBackfillRun();
});
}
frontendTrace("main", "Backfill Desk latest-value monitoring, cancellation and Resume handlers installed");
}
function renderRuntimeStatus(status: ShellStatusDto): void {
const values: Record<string, string> = {
runtimeVersion: status.applicationVersion,
runtimeShellPhase: status.shellPhase,
runtimeConfigDocuments: status.configDocumentCount.toString(),
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback transitoire",
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
const badge = document.querySelector<HTMLElement>("#appVersionBadge");
if (badge) {
badge.textContent = status.applicationVersion;
}
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
if (diagnostic) {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}`;
}
frontendTrace("main", "Backfill Desk runtime status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
configDocumentCount: status.configDocumentCount,
});
}
async function loadRuntimeStatus(source: "startup" | "user"): Promise<void> {
frontendDebug("main", "Backfill Desk runtime status load started", { source });
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
frontendDebug("main", "Backfill Desk runtime status load completed", { source });
}
function bindRuntimeStatusRefresh(): void {
const button = document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus");
if (!button) {
return;
}
button.addEventListener("click", () => {
frontendDebug("main", "Backfill Desk runtime status refresh button clicked");
void loadRuntimeStatus("user").catch(() => frontendWarn("main", "Backfill Desk runtime status refresh failed"));
});
frontendTrace("main", "Backfill Desk runtime status refresh handler installed");
}
async function initializeMain(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Backfill Desk main frontend loaded", { windowLabel });
bindFrontendInteractions();
bindNavigation();
bindRuntimeStatusRefresh();
bindBackfillRouteSelection();
bindCampaignForm();
await bindBackfillStatusMonitoring();
activateView("backfill", "startup");
try {
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup"), syncBackfillStatus("startup")]);
} catch {
frontendWarn("main", "Backfill Desk startup runtime status load failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeMain();
});

View File

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

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-backfill-desk",
"private": true,
"version": "0.3.7",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
"@fortawesome/fontawesome-free": "^7.3",
"@tauri-apps/api": "^2.11",
"bootstrap": "^5.3",
"resize-observer-polyfill": "^1.5",
"simplebar": "^6.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11",
"@types/bootstrap": "^5.2",
"@types/node": "^26.1",
"sass-embedded": "^1.102",
"typescript": "^7.0",
"vite": "^8.2"
}
}

View File

@@ -0,0 +1,448 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 10
//! Shared backend state owned by the Backfill Desk Tauri application.
/// Shared Backfill Desk application state managed by Tauri.
pub(crate) struct AppState {
backfill_runs: crate::BackfillRunState,
config_management: ksp_config_lib::ConfigManagement,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
shutdown_started: std::sync::atomic::AtomicBool,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
store_startup: crate::StoreStartup,
transport_runtime: std::option::Option<crate::TransportRuntime>,
transport_startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
impl crate::AppState {
/// Initializes the scaffold Config ownership, Logging runtime and splash settings.
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
let config_management = crate::config_management(arguments);
let config_management = match config_management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime_identity = crate::launch_identity();
let runtime_identity = match runtime_identity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
let logging_startup = match logging_startup {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_startup = crate::initialize_transport(&config_management);
let (transport_runtime, transport_startup_diagnostic) = match transport_startup {
std::result::Result::Ok(value) => (std::option::Option::Some(value), std::option::Option::None),
std::result::Result::Err(error) => {
let diagnostic = crate::CommandErrorDto::from_error(&error);
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_TRANSPORT,
error_domain = diagnostic.domain.as_str(),
error_code = diagnostic.code.as_str(),
"Backfill Desk Transport readiness is unavailable; keeping desktop shell available"
);
(std::option::Option::None, std::option::Option::Some(diagnostic))
},
};
let store_startup = tauri::async_runtime::block_on(crate::initialize_store(&config_management, transport_runtime.as_ref()));
let splash_settings = crate::SplashSettings::load();
let splash_settings = match splash_settings {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"managed splash timings are invalid; using transient in-memory defaults"
);
crate::SplashSettings::fallback()
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
minimum_ms = splash_settings.minimum_ms(),
minimum_source = splash_settings.minimum_source(),
fade_in_ms = splash_settings.fade_in_ms(),
fade_in_source = splash_settings.fade_in_source(),
fade_out_ms = splash_settings.fade_out_ms(),
fade_out_source = splash_settings.fade_out_source(),
expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(),
"resolved Backfill Desk splash timings"
);
return std::result::Result::Ok(Self {
backfill_runs: crate::BackfillRunState::new(),
config_management,
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
active_profile_id: logging_startup.active_profile_id,
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
}),
shutdown_started: std::sync::atomic::AtomicBool::new(false),
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
store_startup,
transport_runtime,
transport_startup_diagnostic,
});
}
/// Builds the safe scaffold status exposed by the Backfill Desk shell.
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
let document_count = u32::try_from(document_count);
let document_count = match document_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Config registry count cannot be represented by Backfill Desk")
.with_source(error),
);
},
};
let runtime = self.logging_runtime.lock();
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk Logging runtime state lock is poisoned",
));
},
};
let _keep_guard_alive = &runtime.guard;
return std::result::Result::Ok(crate::ShellStatusDto {
active_logging_profile: runtime.active_profile_id.clone(),
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.012-frontend-polish".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
/// Builds the safe Transport-and-Store readiness subset of Backfill Desk options.
pub(crate) fn backfill_options(&self) -> ksp_core_lib::Result<crate::BackfillDeskOptionsDto> {
let runtime = self.transport_runtime.as_ref();
let options = match runtime {
std::option::Option::Some(value) => value.options(),
std::option::Option::None => {
let limits = crate::backfill_request_limits();
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
std::result::Result::Ok(crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: false,
configured_networks: std::vec::Vec::new(),
http_routes: std::vec::Vec::new(),
limits,
network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,
transport_diagnostic: self.transport_startup_diagnostic.clone(),
transport_ready: false,
})
},
};
let mut options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
self.store_startup.apply_to(&mut options);
return std::result::Result::Ok(options);
}
/// Validates and maps one frontend campaign request without starting a Backfill Job.
pub(crate) fn validate_backfill_request(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
let options = self.backfill_options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let job_id = ksp_job_api::JobId::new("backfill-desk-validation");
let job_id = match job_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mapped = crate::map_backfill_request(request, &options, job_id);
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preview = crate::project_backfill_request(&mapped);
let preview = match preview {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_REQUEST,
commitment = preview.commitment.as_str(),
http_role = preview.http_role.as_str(),
scope_kind = preview.scope_kind.as_str(),
"validated Backfill Desk campaign request without starting a Job"
);
return std::result::Result::Ok(preview);
}
/// Prepares one concrete Backfill runtime, installs its control handle atomically and lends execution resources before spawn.
pub(crate) fn prepare_backfill_start(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run while application shutdown is in progress",
));
}
let options = self.backfill_options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let job_id = self.backfill_runs.next_job_id();
let job_id = match job_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mapped = crate::map_backfill_request(request, &options, job_id.clone());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let retained_request = mapped.clone();
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = runtime.handle();
let snapshots = handle.snapshots();
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
if let std::result::Result::Err(error) = installed {
return std::result::Result::Err(error);
}
let transport = self.transport_runtime.as_ref();
let transport = match transport {
std::option::Option::Some(value) => value.pool(),
std::option::Option::None => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(error) = rollback {
return std::result::Result::Err(error);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run without a ready HTTP Transport pool",
));
},
};
let store = self.store_startup.take_for_run();
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(rollback_error) = rollback {
return std::result::Result::Err(rollback_error);
}
return std::result::Result::Err(error);
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
job_id = job_id.as_str(),
"admitted Backfill Desk run and installed control handle before async spawn"
);
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
}
/// Prepares one in-session Resume from the retained Rust-only checkpoint using a new backend Job identity.
pub(crate) fn prepare_backfill_resume(&self) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot resume a Backfill run while application shutdown is in progress",
));
}
let options = self.backfill_options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !options.composition_ready {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot resume without ready Transport and Store composition",
));
}
let job_id = self.backfill_runs.next_job_id();
let job_id = match job_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mapped = self.backfill_runs.resume_request(job_id.clone());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_network_matches = options.store_network.as_deref() == std::option::Option::Some(mapped.network().as_str());
let role_available = options.http_routes.iter().any(|route| return route.role == mapped.role().as_str());
if !store_network_matches || !role_available {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk retained Resume request no longer matches current Transport/Store composition",
));
}
let retained_request = mapped.clone();
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = runtime.handle();
let snapshots = handle.snapshots();
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
if let std::result::Result::Err(error) = installed {
return std::result::Result::Err(error);
}
let transport = self.transport_runtime.as_ref();
let transport = match transport {
std::option::Option::Some(value) => value.pool(),
std::option::Option::None => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(error) = rollback {
return std::result::Result::Err(error);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot resume a Backfill run without a ready HTTP Transport pool",
));
},
};
let store = self.store_startup.take_for_run();
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(rollback_error) = rollback {
return std::result::Result::Err(rollback_error);
}
return std::result::Result::Err(error);
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
job_id = job_id.as_str(),
"admitted Backfill Desk in-session Resume with a reissued Rust-only checkpoint"
);
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
}
/// Returns the current active or retained terminal Backfill status for frontend resynchronization.
pub(crate) fn backfill_status(&self) -> ksp_core_lib::Result<std::option::Option<crate::BackfillRunStatusDto>> {
let notification = self.backfill_runs.current_notification();
let notification = match notification {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match notification {
std::option::Option::Some(value) => crate::project_backfill_status(&value).map(std::option::Option::Some),
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
/// Requests cooperative cancellation of one exact backend-issued Backfill Job identity.
pub(crate) fn cancel_backfill(&self, job_id: &str) -> ksp_core_lib::Result<crate::BackfillCancelResponseDto> {
let cancelled = self.backfill_runs.cancel(job_id);
let cancelled = match cancelled {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
accepted = cancelled.accepted,
job_id = cancelled.job_id.as_str(),
state = cancelled.state.as_str(),
"processed Backfill Desk cooperative cancellation request"
);
return std::result::Result::Ok(cancelled);
}
/// Requests best-effort cooperative cancellation of the active run during application shutdown.
pub(crate) fn cancel_active_backfill_for_shutdown(&self) -> ksp_core_lib::Result<bool> {
return self.backfill_runs.cancel_active_for_shutdown();
}
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.
pub(crate) async fn execute_backfill_run(&self, launch: crate::BackfillRunLaunch) -> ksp_core_lib::Result<()> {
let job_id = launch.job_id.clone();
let result = launch.runtime.run(&launch.transport, &launch.store).await;
let restore = self.store_startup.restore_after_run(launch.store);
let finished = self.backfill_runs.finish(&job_id);
let cancellation_requested = match finished {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = restore {
return std::result::Result::Err(error);
}
return match result {
std::result::Result::Ok(snapshot) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
cancellation_requested = cancellation_requested,
contiguous_completed = snapshot.contiguous_completed(),
job_id = job_id.as_str(),
phase = snapshot.phase().code(),
"Backfill Desk run reached a terminal snapshot and released the single-run slot"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
pub(crate) fn begin_shutdown(&self) -> bool {
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();
}
/// Explicitly closes the retained Store runtime when application shutdown begins.
pub(crate) async fn close_store(&self) -> ksp_core_lib::Result<()> {
return self.store_startup.close().await;
}
/// Returns the resolved splash timings captured during application bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
return self.splash_settings;
}
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
pub(crate) fn begin_splash_sequence(&self) -> bool {
return self
.splash_sequence_started
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
.is_ok();
}
}
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}

View File

@@ -0,0 +1,313 @@
// file: crates/ksp-app-backfill-desk/src/backfill_request.rs
// version: 1
//! Strict application mapping from frontend campaign DTOs to the KSP Backfill request contract.
/// Maps one app-owned campaign request to a validated KSP Backfill request without starting a Job.
pub(crate) fn map_backfill_request(
input: crate::BackfillStartRequestDto,
options: &crate::BackfillDeskOptionsDto,
job_id: ksp_job_api::JobId,
) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
let composition = validate_composition(options);
if let std::result::Result::Err(error) = composition {
return std::result::Result::Err(error);
}
let network = request_network(options);
let network = match network {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = request_role(input.http_role.as_str(), options);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let commitment = request_commitment(input.commitment.as_str());
let commitment = match commitment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let min_context_slot = parse_min_context_slot(input.min_context_slot.as_deref());
let min_context_slot = match min_context_slot {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let scope = request_scope(&input);
let scope = match scope {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let page_size = usize::try_from(input.page_size);
let page_size = match page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
};
let max_pages = usize::try_from(input.max_pages);
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
};
let max_candidates = usize::try_from(input.max_candidates);
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
};
let hydration_concurrency = usize::try_from(input.hydration_concurrency);
let hydration_concurrency = match hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
};
return ksp_job_backfill_lib::BackfillRequest::new(
job_id,
network,
role,
commitment,
scope,
page_size,
max_pages,
max_candidates,
hydration_concurrency,
min_context_slot,
);
}
/// Builds a safe request preview from one already validated KSP Backfill request.
pub(crate) fn project_backfill_request(request: &ksp_job_backfill_lib::BackfillRequest) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
let explicit_signature_count = match request.scope().signatures() {
std::option::Option::Some(signatures) => u32::try_from(signatures.len()),
std::option::Option::None => std::result::Result::Ok(0),
};
let explicit_signature_count = match explicit_signature_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("explicit_signature_count", error)),
};
let hydration_concurrency = u32::try_from(request.hydration_concurrency());
let hydration_concurrency = match hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
};
let max_candidates = u32::try_from(request.max_candidates());
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
};
let max_pages = u32::try_from(request.max_pages());
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
};
let page_size = u32::try_from(request.page_size());
let page_size = match page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
};
return std::result::Result::Ok(crate::BackfillRequestPreviewDto {
address_present: request.scope().address().is_some(),
anchor_present: request.scope().anchor().is_some(),
commitment: request.commitment().code().to_owned(),
explicit_signature_count,
hydration_concurrency,
http_role: request.role().as_str().to_owned(),
max_candidates,
max_pages,
min_context_slot_present: request.min_context_slot().is_some(),
network: request.network().as_str().to_owned(),
page_size,
scope_kind: request.scope().kind().code().to_owned(),
});
}
fn parse_min_context_slot(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error("min_context_slot"));
}
let parsed = value.parse::<u64>();
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("min_context_slot").with_source(error)),
};
}
fn parse_pubkey(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error(field));
}
let parsed = value.parse::<ksp_core_lib::Pubkey>();
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
};
}
fn parse_signature(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error(field));
}
let parsed = ksp_job_backfill_lib::BackfillSignature::new(value.to_owned());
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
};
}
fn request_commitment(value: &str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillCommitment> {
return match value {
"confirmed" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Confirmed),
"finalized" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Finalized),
_ => std::result::Result::Err(request_field_error("commitment")),
};
}
fn request_conversion_error(field: &'static str, source: impl std::error::Error + Send + Sync + 'static) -> ksp_core_lib::Error {
return request_field_error(field).with_source(source);
}
fn request_field_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_REQUEST_INVALID, "Backfill Desk campaign request is invalid").with_context("field", field);
}
fn request_network(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_store_lib::RawNetworkId> {
let network = options.store_network.as_deref();
let network = match network {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(request_field_error("network")),
};
let parsed = ksp_store_lib::RawNetworkId::new(network.to_owned());
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("network").with_source(error)),
};
}
fn request_role(value: &str, options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
let admitted = options.http_routes.iter().any(|route| return route.role == value);
if !admitted || value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error("http_role"));
}
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(value.to_owned()));
}
fn request_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
return match input.scope_kind.as_str() {
"latest_address" => latest_address_scope(input),
"before_address" => before_address_scope(input),
"after_address" => after_address_scope(input),
"explicit_signatures" => explicit_signatures_scope(input),
_ => std::result::Result::Err(request_field_error("scope_kind")),
};
}
fn latest_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_anchor_or_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::latest_address(address));
}
fn before_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_explicit_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let anchor = required_anchor(input.anchor_signature.as_deref());
let anchor = match anchor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::before_address(address, anchor));
}
fn after_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_explicit_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let anchor = required_anchor(input.anchor_signature.as_deref());
let anchor = match anchor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::after_address(address, anchor));
}
fn explicit_signatures_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
if input.address.is_some() || input.anchor_signature.is_some() || input.min_context_slot.is_some() {
return std::result::Result::Err(request_field_error("scope"));
}
if input.explicit_signatures.is_empty() || input.explicit_signatures.len() > ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES {
return std::result::Result::Err(request_field_error("explicit_signatures"));
}
let mut signatures = std::vec::Vec::with_capacity(input.explicit_signatures.len());
for value in &input.explicit_signatures {
let signature = parse_signature(value.as_str(), "explicit_signatures");
let signature = match signature {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
signatures.push(signature);
}
return ksp_job_backfill_lib::BackfillScope::explicit_signatures(signatures);
}
fn require_no_anchor_or_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
if input.anchor_signature.is_some() || !input.explicit_signatures.is_empty() {
return std::result::Result::Err(request_field_error("scope"));
}
return std::result::Result::Ok(());
}
fn require_no_explicit_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
if !input.explicit_signatures.is_empty() {
return std::result::Result::Err(request_field_error("explicit_signatures"));
}
return std::result::Result::Ok(());
}
fn required_address(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
return match value {
std::option::Option::Some(value) => parse_pubkey(value, "address"),
std::option::Option::None => std::result::Result::Err(request_field_error("address")),
};
}
fn required_anchor(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
return match value {
std::option::Option::Some(value) => parse_signature(value, "anchor_signature"),
std::option::Option::None => std::result::Result::Err(request_field_error("anchor_signature")),
};
}
fn validate_composition(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<()> {
if options.composition_ready && options.transport_ready && options.store_ready && options.network_coherent {
return std::result::Result::Ok(());
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot admit a campaign before Transport and Store composition is ready",
));
}
#[cfg(test)]
#[path = "../unit_tests/backfill_request.rs"]
mod tests;

View File

@@ -0,0 +1,333 @@
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
// version: 4
//! Single-active-run admission and launch ownership for Backfill Desk.
/// Fully prepared Backfill execution moved into the Tauri async runtime after admission.
pub(crate) struct BackfillRunLaunch {
/// Backend-generated logical Job identity.
pub(crate) job_id: ksp_job_api::JobId,
/// Concrete Backfill runtime owning discovery, hydration, persistence and latest-value publication.
pub(crate) runtime: ksp_job_backfill_lib::BackfillJobRuntime,
/// Independent latest-value source retained by the monitoring bridge.
pub(crate) snapshots: ksp_job_backfill_lib::BackfillSnapshotSource,
/// Store facade temporarily borrowed from the application Store runtime for the duration of this run.
pub(crate) store: ksp_store_lib::Store,
/// Shareable HTTP Transport pool selected by the active composite profile.
pub(crate) transport: ksp_onchain_transport_lib::HttpTransportPool,
}
impl crate::BackfillRunLaunch {
/// Builds the safe acknowledgement returned immediately after the non-blocking Start admission.
#[must_use]
pub(crate) fn response(&self) -> crate::BackfillStartResponseDto {
return crate::BackfillStartResponseDto { job_id: self.job_id.as_str().to_owned(), state: ksp_job_api::JobState::Created.code().to_owned() };
}
}
/// Single-run control slot retained by the application while one Backfill runtime is active.
pub(crate) struct BackfillRunState {
active: std::sync::Mutex<std::option::Option<ActiveBackfillRun>>,
last_terminal: std::sync::Mutex<std::option::Option<TerminalBackfillRun>>,
next_sequence: std::sync::atomic::AtomicU64,
}
impl crate::BackfillRunState {
/// Creates an empty single-run state for one desktop application session.
#[must_use]
pub(crate) const fn new() -> Self {
return Self {
active: std::sync::Mutex::new(std::option::Option::None),
last_terminal: std::sync::Mutex::new(std::option::Option::None),
next_sequence: std::sync::atomic::AtomicU64::new(1),
};
}
/// Allocates one bounded backend-owned Job identity without accepting caller-supplied identity material.
pub(crate) fn next_job_id(&self) -> ksp_core_lib::Result<ksp_job_api::JobId> {
let sequence = self
.next_sequence
.fetch_update(std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire, |current| return current.checked_add(1));
let sequence = match sequence {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk Job identity sequence is exhausted",
));
},
};
return ksp_job_api::JobId::new(format!("backfill-desk-{sequence}"));
}
/// Installs one control handle atomically and rejects concurrent Starts while another run owns the slot.
pub(crate) fn install(
&self,
job_id: ksp_job_api::JobId,
handle: ksp_job_backfill_lib::BackfillJobHandle,
request: ksp_job_backfill_lib::BackfillRequest,
) -> ksp_core_lib::Result<()> {
let active = self.active.lock();
let mut active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run state lock is poisoned",
));
},
};
if active.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
"Backfill Desk already has an active Backfill run",
));
}
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id, request });
return std::result::Result::Ok(());
}
/// Clears the matching terminal run and reports whether cancellation had been requested on its retained handle.
pub(crate) fn finish(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<bool> {
let active = self.active.lock();
let mut active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run state lock is poisoned",
));
},
};
let current = active.as_ref();
let current = match current {
std::option::Option::Some(value) if &value.job_id == job_id => value,
std::option::Option::Some(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk terminal run does not match the active Job identity",
));
},
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk terminal run has no active admission slot",
));
},
};
let cancellation_requested = current.handle.is_cancellation_requested();
let request = current.request.clone();
let source = current.handle.snapshots();
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
if notification.state().is_terminal() {
let terminal = self.last_terminal.lock();
let mut terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal monitoring state lock is poisoned",
));
},
};
*terminal = std::option::Option::Some(TerminalBackfillRun { notification, request });
}
*active = std::option::Option::None;
return std::result::Result::Ok(cancellation_requested);
}
/// Requests cooperative cancellation for the exact backend-issued active Job identity.
pub(crate) fn cancel(&self, job_id: &str) -> ksp_core_lib::Result<crate::BackfillCancelResponseDto> {
let active = self.active.lock();
let active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run cancellation state lock is poisoned",
));
},
};
if let std::option::Option::Some(current) = active.as_ref() {
if current.job_id.as_str() != job_id {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
"Backfill Desk cancellation target does not match the active Job",
));
}
let source = current.handle.snapshots();
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
let state = notification.state();
if state.is_terminal() {
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
accepted: false,
job_id: current.job_id.as_str().to_owned(),
state: state.code().to_owned(),
});
}
let accepted = current.handle.cancel();
let state_code = if current.handle.is_cancellation_requested() { "cancelling" } else { state.code() };
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
accepted,
job_id: current.job_id.as_str().to_owned(),
state: state_code.to_owned(),
});
}
drop(active);
let terminal = self.last_terminal.lock();
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal cancellation state lock is poisoned",
));
},
};
if let std::option::Option::Some(terminal) = terminal.as_ref() {
let notification = &terminal.notification;
if notification.id().as_str() != job_id {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
"Backfill Desk cancellation target does not match the retained terminal Job",
));
}
return std::result::Result::Ok(crate::BackfillCancelResponseDto {
accepted: false,
job_id: notification.id().as_str().to_owned(),
state: notification.state().code().to_owned(),
});
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE,
"Backfill Desk has no active Backfill run to cancel",
));
}
/// Requests best-effort cooperative cancellation of whichever run is active after shutdown wins admission.
pub(crate) fn cancel_active_for_shutdown(&self) -> ksp_core_lib::Result<bool> {
let active = self.active.lock();
let active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk shutdown cancellation state lock is poisoned",
));
},
};
return match active.as_ref() {
std::option::Option::Some(current) => std::result::Result::Ok(current.handle.cancel()),
std::option::Option::None => std::result::Result::Ok(false),
};
}
/// Returns the complete current active snapshot or the retained latest terminal snapshot for resynchronization.
pub(crate) fn current_notification(
&self,
) -> ksp_core_lib::Result<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>> {
let active = self.active.lock();
let active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run monitoring state lock is poisoned",
));
},
};
if let std::option::Option::Some(current) = active.as_ref() {
let source = current.handle.snapshots();
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
return std::result::Result::Ok(std::option::Option::Some(notification));
}
drop(active);
let terminal = self.last_terminal.lock();
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal monitoring state lock is poisoned",
));
},
};
return std::result::Result::Ok(terminal.as_ref().map(|terminal| return terminal.notification.clone()));
}
/// Rebuilds the last terminal request with its opaque checkpoint reissued onto one new Job identity.
pub(crate) fn resume_request(&self, job_id: ksp_job_api::JobId) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
let active = self.active.lock();
let active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run resume state lock is poisoned",
));
},
};
if active.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
"Backfill Desk cannot resume while another Backfill run owns the single-run slot",
));
}
drop(active);
let terminal = self.last_terminal.lock();
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal resume state lock is poisoned",
));
},
};
let terminal = match terminal.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
"Backfill Desk has no retained terminal run available for in-session Resume",
));
},
};
let checkpoint = terminal.notification.snapshot().checkpoint();
let checkpoint = match checkpoint {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
"Backfill Desk retained terminal run has no safe checkpoint for in-session Resume",
));
},
};
return terminal.request.resume_for_job(job_id, checkpoint);
}
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
pub(crate) fn rollback(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<()> {
let finished = self.finish(job_id);
return match finished {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
struct ActiveBackfillRun {
handle: ksp_job_backfill_lib::BackfillJobHandle,
job_id: ksp_job_api::JobId,
request: ksp_job_backfill_lib::BackfillRequest,
}
#[derive(Clone)]
struct TerminalBackfillRun {
notification: ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
request: ksp_job_backfill_lib::BackfillRequest,
}
#[cfg(test)]
#[path = "../unit_tests/backfill_run.rs"]
mod tests;

View File

@@ -0,0 +1,194 @@
// file: crates/ksp-app-backfill-desk/src/backfill_status.rs
// version: 1
//! Safe latest-value monitoring projection for one Backfill Desk run.
use ts_rs::TS; // rust-rules: trait-import
/// Stable Tauri event carrying the latest complete safe Backfill run status.
pub(crate) const BACKFILL_STATUS_EVENT_NAME: &str = "ksp-backfill-status";
/// Frontend-safe latest-value projection of one concrete Backfill runtime snapshot.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts")]
pub(crate) struct BackfillRunStatusDto {
/// Whether the represented Job is currently non-terminal.
pub(crate) active: bool,
/// Number of candidates admitted into hydration.
pub(crate) candidates_admitted: u32,
/// Number of admitted candidates that reached a known coordinator result.
pub(crate) candidates_finished: u32,
/// Number of candidates selected by discovery.
pub(crate) candidates_selected: u32,
/// Number of candidates cancelled before Store submission.
pub(crate) cancelled_candidates: u32,
/// Whether a safe caller-owned checkpoint currently exists.
pub(crate) checkpoint_present: bool,
/// Normal terminal completion classification, when applicable.
pub(crate) completion: std::option::Option<String>,
/// Safe contiguous completion prefix retained by the current checkpoint frontier.
pub(crate) contiguous_completed: u32,
/// Number of Store content conflicts observed by the run.
pub(crate) conflicts: u32,
/// Safe discovery boundary code after discovery completes.
pub(crate) discovery_boundary: std::option::Option<String>,
/// Number of identical canonical RAW transactions already present.
pub(crate) entities_existing: u32,
/// Number of newly inserted canonical RAW transactions.
pub(crate) entities_inserted: u32,
/// Number of durable purge tombstones respected by normal Backfill persistence.
pub(crate) entities_purged: u32,
/// Stable fatal error code retained by a failed Job, when applicable.
pub(crate) failure_code: std::option::Option<String>,
/// Stable fatal error domain retained by a failed Job, when applicable.
pub(crate) failure_domain: std::option::Option<String>,
/// Number of known candidate outcomes blocking the contiguous frontier.
pub(crate) holes: u32,
/// Backend-generated logical Job identity.
pub(crate) job_id: String,
/// Stable concrete Job family code.
pub(crate) job_kind: String,
/// Greatest observed candidate concurrency.
pub(crate) maximum_in_flight: u32,
/// Number of `getTransaction = null` candidates.
pub(crate) missing: u32,
/// Number of acquisition observations already durable.
pub(crate) observations_existing: u32,
/// Number of newly inserted acquisition observations.
pub(crate) observations_inserted: u32,
/// Stable concrete Backfill runtime phase.
pub(crate) phase: String,
/// Monotone latest-value notification sequence encoded as decimal text.
pub(crate) sequence: String,
/// Stable Backfill scope category without address/signature payloads.
pub(crate) scope_kind: String,
/// Stable generic Job lifecycle state.
pub(crate) state: String,
/// Whether this latest value is terminal.
pub(crate) terminal: bool,
}
/// Projects one complete Backfill latest-value notification to the safe desktop monitoring contract.
pub(crate) fn project_backfill_status(
notification: &ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
) -> ksp_core_lib::Result<BackfillRunStatusDto> {
let snapshot = notification.snapshot();
let state = notification.state();
let failure = snapshot.failure_code();
let candidates_admitted = usize_to_u32(snapshot.candidates_admitted(), "candidates_admitted");
let candidates_admitted = match candidates_admitted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates_finished = usize_to_u32(snapshot.candidates_finished(), "candidates_finished");
let candidates_finished = match candidates_finished {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates_selected = usize_to_u32(snapshot.candidates_selected(), "candidates_selected");
let candidates_selected = match candidates_selected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let cancelled_candidates = usize_to_u32(snapshot.cancelled_candidates(), "cancelled_candidates");
let cancelled_candidates = match cancelled_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let contiguous_completed = usize_to_u32(snapshot.contiguous_completed(), "contiguous_completed");
let contiguous_completed = match contiguous_completed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let conflicts = usize_to_u32(snapshot.conflicts(), "conflicts");
let conflicts = match conflicts {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_existing = usize_to_u32(snapshot.entities_existing(), "entities_existing");
let entities_existing = match entities_existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_inserted = usize_to_u32(snapshot.entities_inserted(), "entities_inserted");
let entities_inserted = match entities_inserted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_purged = usize_to_u32(snapshot.entities_purged(), "entities_purged");
let entities_purged = match entities_purged {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let holes = usize_to_u32(snapshot.holes(), "holes");
let holes = match holes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let maximum_in_flight = usize_to_u32(snapshot.maximum_in_flight(), "maximum_in_flight");
let maximum_in_flight = match maximum_in_flight {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let missing = usize_to_u32(snapshot.missing(), "missing");
let missing = match missing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observations_existing = usize_to_u32(snapshot.observations_existing(), "observations_existing");
let observations_existing = match observations_existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observations_inserted = usize_to_u32(snapshot.observations_inserted(), "observations_inserted");
let observations_inserted = match observations_inserted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(BackfillRunStatusDto {
active: !state.is_terminal(),
candidates_admitted,
candidates_finished,
candidates_selected,
cancelled_candidates,
checkpoint_present: snapshot.checkpoint().is_some(),
completion: state.completion().map(|completion| return completion.code().to_owned()),
contiguous_completed,
conflicts,
discovery_boundary: snapshot.discovery_boundary().map(|boundary| return boundary.code().to_owned()),
entities_existing,
entities_inserted,
entities_purged,
failure_code: failure.map(|code| return code.code().to_owned()),
failure_domain: failure.map(|code| return code.domain().to_owned()),
holes,
job_id: notification.id().as_str().to_owned(),
job_kind: notification.kind().as_str().to_owned(),
maximum_in_flight,
missing,
observations_existing,
observations_inserted,
phase: snapshot.phase().code().to_owned(),
sequence: notification.sequence().value().to_string(),
scope_kind: snapshot.scope_kind().code().to_owned(),
state: state.code().to_owned(),
terminal: state.is_terminal(),
});
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a bounded Job counter to the frontend")
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/backfill_status.rs"]
mod tests;

View File

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

View File

@@ -0,0 +1,43 @@
// file: crates/ksp-app-backfill-desk/src/constants.rs
// version: 6
//! Application-owned tracing targets and domains.
/// Composite component identifier for Logging.
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
/// Composite component identifier for Store.
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
/// Composite component identifier for Transport.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Initial hydration concurrency proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_HYDRATION_CONCURRENCY: u32 = 4;
/// Initial candidate cap proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_MAX_CANDIDATES: u32 = 1_000;
/// Initial discovery-page cap proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_MAX_PAGES: u32 = 10;
/// Initial signature page size proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_PAGE_SIZE: u32 = 100;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
/// Structured domain used by concrete Backfill run admission and execution.
pub(crate) const TRACING_DOMAIN_RUN: &str = "backfill.run";
/// Structured domain used by the Backfill Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
/// Structured domain used by Store readiness and shutdown operations.
pub(crate) const TRACING_DOMAIN_STORE: &str = "backfill.store";
/// Structured domain used by Transport readiness and role inventory operations.
pub(crate) const TRACING_DOMAIN_TRANSPORT: &str = "backfill.transport";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Backfill Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-backfill-desk";
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-backfill-desk.frontend";
/// Owning target for main-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-backfill-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-backfill-desk.frontend.splash";

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
// version: 4
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
use ts_rs::TS; // rust-rules: trait-import
/// Backend-derived limits and application defaults rendered by the Backfill campaign form.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestLimitsDto.ts")]
pub(crate) struct BackfillRequestLimitsDto {
/// Initial hydration concurrency proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_hydration_concurrency: u32,
/// Initial candidate cap proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_max_candidates: u32,
/// Initial discovery-page cap proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_max_pages: u32,
/// Initial signature page size proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_page_size: u32,
/// Maximum hydration concurrency owned by `ksp-job-backfill-lib`.
pub(crate) max_hydration_concurrency: u32,
/// Maximum candidate count owned by `ksp-job-backfill-lib`.
pub(crate) max_candidates: u32,
/// Maximum discovery-page count owned by `ksp-job-backfill-lib`.
pub(crate) max_pages: u32,
/// Maximum signature page size owned by `ksp-job-backfill-lib`.
pub(crate) max_page_size: u32,
/// Maximum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
pub(crate) max_signature_text_bytes: u32,
/// Minimum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
pub(crate) min_signature_text_bytes: u32,
}
/// App-owned request received from the Backfill campaign form before a Job is started.
///
/// Network and physical endpoint information are intentionally absent. The backend derives the
/// network from the configured Store and maps `http_role` only after checking the current safe
/// Transport inventory.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts")]
pub(crate) struct BackfillStartRequestDto {
/// Optional address text used only by address-based scopes.
pub(crate) address: std::option::Option<String>,
/// Optional exclusive anchor signature used only by Before/After scopes.
pub(crate) anchor_signature: std::option::Option<String>,
/// Stable Backfill commitment code.
pub(crate) commitment: String,
/// Explicit signature texts used only by the explicit-signatures scope.
pub(crate) explicit_signatures: std::vec::Vec<String>,
/// Maximum number of concurrent candidate hydrations.
pub(crate) hydration_concurrency: u32,
/// Logical HTTP role selected from `BackfillDeskOptionsDto::http_routes`.
pub(crate) http_role: String,
/// Maximum number of candidates admitted by the campaign.
pub(crate) max_candidates: u32,
/// Maximum number of address-discovery pages admitted by the campaign.
pub(crate) max_pages: u32,
/// Optional minimum context slot encoded as decimal text to avoid JavaScript integer precision loss.
pub(crate) min_context_slot: std::option::Option<String>,
/// Signature page size used by address discovery.
pub(crate) page_size: u32,
/// Stable Backfill scope kind code.
pub(crate) scope_kind: String,
}
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts")]
pub(crate) struct BackfillStartResponseDto {
/// Backend-generated bounded Job identifier for this in-session run.
pub(crate) job_id: String,
/// Initial lifecycle state at the time Start returns to the frontend.
pub(crate) state: String,
}
/// Safe acknowledgement of one targeted cooperative cancellation request.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts")]
pub(crate) struct BackfillCancelResponseDto {
/// Whether this call won the first pre-terminal cancellation request for the targeted run.
pub(crate) accepted: bool,
/// Backend-generated Job identifier that the cancellation request targeted.
pub(crate) job_id: String,
/// Safe current or cancellation-intent lifecycle code at command completion.
pub(crate) state: String,
}
/// Safe acknowledgement returned when one retained checkpoint is resumed into a new backend Job lifecycle.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts")]
pub(crate) struct BackfillResumeResponseDto {
/// Whether the retained in-session checkpoint was accepted for a new run.
pub(crate) accepted: bool,
/// Newly allocated backend Job identifier owning the reissued checkpoint.
pub(crate) job_id: String,
/// Initial lifecycle state at the time Resume returns to the frontend.
pub(crate) state: String,
}
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
///
/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is
/// suitable for request validation feedback before the runtime Start slice exists.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts")]
pub(crate) struct BackfillRequestPreviewDto {
/// Whether an address participates in the validated scope.
pub(crate) address_present: bool,
/// Whether an exclusive anchor participates in the validated scope.
pub(crate) anchor_present: bool,
/// Stable validated commitment code.
pub(crate) commitment: String,
/// Number of explicit signatures after Backfill-owned stable deduplication.
pub(crate) explicit_signature_count: u32,
/// Validated hydration concurrency.
pub(crate) hydration_concurrency: u32,
/// Validated logical HTTP role.
pub(crate) http_role: String,
/// Validated maximum candidate count.
pub(crate) max_candidates: u32,
/// Validated maximum discovery-page count.
pub(crate) max_pages: u32,
/// Whether a minimum context slot participates in the validated request.
pub(crate) min_context_slot_present: bool,
/// Backend-derived logical Store network.
pub(crate) network: String,
/// Validated signature page size.
pub(crate) page_size: u32,
/// Stable validated scope kind code.
pub(crate) scope_kind: String,
}
/// Returns the exact commitment codes currently admitted by the Backfill runtime.
#[must_use]
pub(crate) fn backfill_commitment_codes() -> std::vec::Vec<String> {
return vec![ksp_job_backfill_lib::BackfillCommitment::Finalized.code().to_owned(), ksp_job_backfill_lib::BackfillCommitment::Confirmed.code().to_owned()];
}
/// Returns the exact HTTP scope codes currently admitted by the Backfill runtime.
#[must_use]
pub(crate) fn backfill_scope_kind_codes() -> std::vec::Vec<String> {
return vec![
ksp_job_backfill_lib::BackfillScopeKind::LatestAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::BeforeAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::AfterAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::ExplicitSignatures.code().to_owned(),
];
}
/// Builds frontend-safe request limits directly from the public Backfill constants.
pub(crate) fn backfill_request_limits() -> ksp_core_lib::Result<BackfillRequestLimitsDto> {
let max_hydration_concurrency = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY, "max_hydration_concurrency");
let max_hydration_concurrency = match max_hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_candidates = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES, "max_candidates");
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_pages = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGES, "max_pages");
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_page_size = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE, "max_page_size");
let max_page_size = match max_page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, "max_signature_text_bytes");
let max_signature_text_bytes = match max_signature_text_bytes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let min_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, "min_signature_text_bytes");
let min_signature_text_bytes = match min_signature_text_bytes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY == 0 || crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY > max_hydration_concurrency {
return std::result::Result::Err(limit_contract_error("default_hydration_concurrency"));
}
if crate::DEFAULT_BACKFILL_MAX_CANDIDATES == 0 || crate::DEFAULT_BACKFILL_MAX_CANDIDATES > max_candidates {
return std::result::Result::Err(limit_contract_error("default_max_candidates"));
}
if crate::DEFAULT_BACKFILL_MAX_PAGES == 0 || crate::DEFAULT_BACKFILL_MAX_PAGES > max_pages {
return std::result::Result::Err(limit_contract_error("default_max_pages"));
}
if crate::DEFAULT_BACKFILL_PAGE_SIZE == 0 || crate::DEFAULT_BACKFILL_PAGE_SIZE > max_page_size {
return std::result::Result::Err(limit_contract_error("default_page_size"));
}
return std::result::Result::Ok(BackfillRequestLimitsDto {
default_hydration_concurrency: crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY,
default_max_candidates: crate::DEFAULT_BACKFILL_MAX_CANDIDATES,
default_max_pages: crate::DEFAULT_BACKFILL_MAX_PAGES,
default_page_size: crate::DEFAULT_BACKFILL_PAGE_SIZE,
max_hydration_concurrency,
max_candidates,
max_pages,
max_page_size,
max_signature_text_bytes,
min_signature_text_bytes,
});
}
fn limit_contract_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk request-limit projection is internally inconsistent")
.with_context("field", field);
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a Job-owned bound to the frontend")
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/dto_backfill.rs"]
mod tests;

View File

@@ -0,0 +1,144 @@
// file: crates/ksp-app-backfill-desk/src/dto_common.rs
// version: 7
//! Common Tauri DTOs shared by the Backfill Desk shell.
use ts_rs::TS; // rust-rules: trait-import
/// Safe HTTP route exposed for operator selection before a Backfill run.
///
/// The route is identified only by the logical Transport role. Provider labels are safe metadata used for operator choice; endpoint URLs and credentials never
/// cross IPC.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts")]
pub(crate) struct BackfillHttpRouteOptionDto {
/// Whether more than one configured provider participates in this logical route.
pub(crate) pooled: bool,
/// Safe provider labels participating in the route, sorted and deduplicated.
pub(crate) providers: std::vec::Vec<String>,
/// Logical Transport role later passed to the Backfill runtime.
pub(crate) role: String,
}
/// Safe Program ID entry exposed only to populate the free-form address autocomplete dataset.
///
/// Every value is derived from the canonical `ksp-core-lib` registry. The frontend remains free to submit any valid Solana address; this DTO is advisory only.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts")]
pub(crate) struct ProgramIdAutocompleteOptionDto {
/// Stable KSP machine-readable Program ID code.
pub(crate) code: String,
/// Broad functional domain from the canonical registry.
pub(crate) domain: String,
/// Functional family from the canonical registry.
pub(crate) family: String,
/// Human-readable program name.
pub(crate) name: String,
/// Canonical Base58 Program ID used as the HTML datalist value.
pub(crate) program_id: String,
/// Owning protocol/project label from the canonical registry.
pub(crate) protocol: String,
}
/// Projects the canonical KSP Program ID registry to the safe autocomplete dataset.
#[must_use]
pub(crate) fn program_id_autocomplete_options() -> std::vec::Vec<crate::ProgramIdAutocompleteOptionDto> {
let entries = ksp_core_lib::entries();
let mut options = std::vec::Vec::with_capacity(entries.len());
for entry in entries {
options.push(crate::ProgramIdAutocompleteOptionDto {
code: entry.code().to_owned(),
domain: entry.domain().to_owned(),
family: entry.family().to_owned(),
name: entry.name().to_owned(),
program_id: entry.program_id().to_owned(),
protocol: entry.protocol().to_owned(),
});
}
return options;
}
/// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds.
///
/// Endpoint URLs, credentials and physical routing details are intentionally absent.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts")]
pub(crate) struct BackfillDeskOptionsDto {
/// Stable commitment codes currently admitted by `ksp-job-backfill-lib`.
pub(crate) commitments: std::vec::Vec<String>,
/// Selectable logical HTTP routes that support both RPC methods required by the Backfill runtime.
pub(crate) http_routes: std::vec::Vec<BackfillHttpRouteOptionDto>,
/// Backend-owned maxima plus application defaults used by the campaign form.
pub(crate) limits: crate::BackfillRequestLimitsDto,
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
pub(crate) composition_ready: bool,
/// Canonical KSP Program IDs exposed as a frontend autocomplete dataset; never an allow-list.
pub(crate) program_id_options: std::vec::Vec<crate::ProgramIdAutocompleteOptionDto>,
/// Distinct enabled HTTP cluster labels selected by the active Transport profile.
pub(crate) configured_networks: std::vec::Vec<String>,
/// Whether the selected Store network exactly matches the one coherent Transport network.
pub(crate) network_coherent: bool,
/// Safe startup diagnostic when Store configuration, opening or readiness could not be proven.
pub(crate) store_diagnostic: std::option::Option<CommandErrorDto>,
/// Logical network selected by the active Store profile, without backend connection details.
pub(crate) store_network: std::option::Option<String>,
/// Whether Store opened successfully and its bounded health probe proved readiness.
pub(crate) store_ready: bool,
/// Safe startup diagnostic when Transport configuration could not be resolved or constructed.
pub(crate) transport_diagnostic: std::option::Option<CommandErrorDto>,
/// Stable HTTP scope codes currently admitted by `ksp-job-backfill-lib`.
pub(crate) scope_kinds: std::vec::Vec<String>,
/// Whether Transport currently has one coherent network and at least one compatible available role.
pub(crate) transport_ready: bool,
}
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/CommandErrorDto.ts")]
pub(crate) struct CommandErrorDto {
/// Stable KSP error domain.
pub(crate) domain: String,
/// Stable KSP error code within the domain.
pub(crate) code: String,
/// Human-readable error message without arbitrary context fields.
pub(crate) message: String,
}
impl crate::CommandErrorDto {
/// Builds a bounded safe projection from a KSP error.
#[must_use]
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
return Self {
domain: error.code().domain().to_owned(),
code: error.code().code().to_owned(),
message: error.message().to_owned(),
};
}
}
/// Safe scaffold/runtime snapshot exposed to the Backfill Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts")]
pub(crate) struct ShellStatusDto {
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
pub(crate) active_logging_profile: std::option::Option<String>,
/// Cargo application version.
pub(crate) application_version: String,
/// Number of logical Config resources registered by the current runtime.
pub(crate) config_document_count: u32,
/// Whether Backfill Desk had to install its transient in-memory Logging fallback.
pub(crate) fallback_logging_active: bool,
/// Current implementation phase exposed by the shell.
pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_common.rs"]
mod tests;

View File

@@ -0,0 +1,49 @@
// file: crates/ksp-app-backfill-desk/src/errors.rs
// version: 9
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
/// Shared Backfill Desk runtime state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid");
/// Shared Backfill Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_lock_failed");
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
/// Backfill Desk has no retained safe checkpoint/request pair available for in-session Resume.
pub(crate) const ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_resume_unavailable");
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
pub(crate) const ERROR_CODE_BACKFILL_RUN_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_active");
/// Backfill Desk received a stale or mismatched Job identity for an active-run control operation.
pub(crate) const ERROR_CODE_BACKFILL_RUN_MISMATCH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_mismatch");
/// Backfill Desk has no active or retained matching run for the requested control operation.
pub(crate) const ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_not_active");
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "frontend_log_target_invalid");
/// Backfill Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "logging_bootstrap_failed");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "splash_setting_invalid");
/// Backfill Desk Store and Transport configuration target different logical networks.
pub(crate) const ERROR_CODE_STORE_NETWORK_MISMATCH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_network_mismatch");
/// Backfill Desk cannot prove a single Transport network before Store opening.
pub(crate) const ERROR_CODE_STORE_NETWORK_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_network_unavailable");
/// Backfill Desk cannot complete the bounded Store shutdown lifecycle.
pub(crate) const ERROR_CODE_STORE_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "store_shutdown_failed");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_window_operation_failed");
/// Backfill Desk cannot derive a safe Transport readiness contract from the Transport registry.
pub(crate) const ERROR_CODE_TRANSPORT_READINESS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "transport_readiness_invalid");

View File

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

View File

@@ -0,0 +1,192 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs
// version: 11
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod app_state;
mod backfill_request;
mod backfill_run;
mod backfill_status;
mod bootstrap;
mod constants;
mod dto_backfill;
mod dto_common;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod splash;
mod store_runtime;
mod tauri;
mod transport_runtime;
mod tw_main;
mod tw_splash;
/// Runs the KSP Backfill desktop application.
pub use self::tauri::run;
/// Shared Backfill Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
/// Maps one app-owned campaign DTO to the validated KSP Backfill request contract.
pub(crate) use self::backfill_request::map_backfill_request;
/// Projects one validated Backfill request without returning address or signature values.
pub(crate) use self::backfill_request::project_backfill_request;
/// Fully prepared Backfill launch moved into the async runtime after single-run admission.
pub(crate) use self::backfill_run::BackfillRunLaunch;
/// Single-active-run admission state retained by the application.
pub(crate) use self::backfill_run::BackfillRunState;
/// Stable Tauri event carrying coalesced latest-value Backfill status.
pub(crate) use self::backfill_status::BACKFILL_STATUS_EVENT_NAME;
/// Frontend-safe latest-value Backfill runtime projection.
pub(crate) use self::backfill_status::BackfillRunStatusDto;
/// Projects one complete Job notification to the safe desktop monitoring contract.
pub(crate) use self::backfill_status::project_backfill_status;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) use self::bootstrap::config_management;
/// Initializes Logging from the Backfill Desk composite, with a trace-level development fallback.
pub(crate) use self::bootstrap::initialize_logging;
/// Loads the concrete Backfill Desk Config composite.
pub(crate) use self::bootstrap::load_backfill_desk_composite;
/// Returns one required component profile from the Backfill Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite component identifier for Logging.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite component identifier for Store.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
/// Composite component identifier for Transport.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Initial hydration concurrency proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY;
/// Initial candidate cap proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_CANDIDATES;
/// Initial discovery-page cap proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_PAGES;
/// Initial signature page size proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_PAGE_SIZE;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
/// Structured domain used by concrete Backfill run admission and execution.
pub(crate) use self::constants::TRACING_DOMAIN_RUN;
/// Structured domain used by the Backfill Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Store readiness and shutdown operations.
pub(crate) use self::constants::TRACING_DOMAIN_STORE;
/// Structured domain used by Transport readiness and role inventory operations.
pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Backfill Desk.
pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Safe acknowledgement returned by targeted cooperative cancellation.
pub(crate) use self::dto_backfill::BackfillCancelResponseDto;
/// Backend-derived request bounds and Desk defaults for the campaign form.
pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
/// Safe request preview produced after strict backend mapping.
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
/// Safe acknowledgement returned after in-session checkpoint Resume admission.
pub(crate) use self::dto_backfill::BackfillResumeResponseDto;
/// App-owned campaign request received from the frontend.
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
pub(crate) use self::dto_backfill::BackfillStartResponseDto;
/// Returns commitment codes admitted by the current Backfill contract.
pub(crate) use self::dto_backfill::backfill_commitment_codes;
/// Builds frontend-safe campaign limits from Job-owned public constants.
pub(crate) use self::dto_backfill::backfill_request_limits;
/// Returns scope kind codes admitted by the current Backfill contract.
pub(crate) use self::dto_backfill::backfill_scope_kind_codes;
/// Safe Backfill Desk options contract combining readiness and campaign metadata.
pub(crate) use self::dto_common::BackfillDeskOptionsDto;
/// Safe logical HTTP route exposed for operator selection.
pub(crate) use self::dto_common::BackfillHttpRouteOptionDto;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe canonical Program ID entry exposed only for free-form address autocomplete.
pub(crate) use self::dto_common::ProgramIdAutocompleteOptionDto;
/// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto;
/// Projects the canonical Core Program ID registry to autocomplete entries.
pub(crate) use self::dto_common::program_id_autocomplete_options;
/// Shared Backfill Desk runtime state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Backfill Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
/// Backfill Desk has no retained safe checkpoint available for in-session Resume.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE;
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_ACTIVE;
/// Backfill Desk rejects stale or mismatched Job identity control requests.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_MISMATCH;
/// Backfill Desk has no active or retained matching run for the requested control operation.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_NOT_ACTIVE;
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Backfill Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Backfill Desk Store and Transport configuration target different logical networks.
pub(crate) use self::errors::ERROR_CODE_STORE_NETWORK_MISMATCH;
/// Backfill Desk cannot prove a single Transport network before Store opening.
pub(crate) use self::errors::ERROR_CODE_STORE_NETWORK_UNAVAILABLE;
/// Backfill Desk cannot complete the bounded Store shutdown lifecycle.
pub(crate) use self::errors::ERROR_CODE_STORE_SHUTDOWN_FAILED;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Backfill Desk cannot derive a safe Transport readiness contract.
pub(crate) use self::errors::ERROR_CODE_TRANSPORT_READINESS_INVALID;
/// Log payload sent by Backfill Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Creates the stable runtime identity for this Backfill Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings;
/// Store startup state retained by the application shell.
pub(crate) use self::store_runtime::StoreStartup;
/// Initializes Store only after composite-selected Transport/Store network coherence is proven.
pub(crate) use self::store_runtime::initialize_store;
/// Safe and executable Transport runtime retained by application state.
pub(crate) use self::transport_runtime::TransportRuntime;
/// Initializes the composite-selected HTTP Transport runtime.
pub(crate) use self::transport_runtime::initialize_transport;
/// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_main::require_main_window;
/// Shows and focuses the main Backfill Desk window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,288 @@
// file: crates/ksp-app-backfill-desk/src/store_runtime.rs
// version: 2
//! Composite-selected Store readiness and shutdown lifecycle owned by Backfill Desk.
/// Startup result for the Store layer, including safe diagnostics when the desktop shell remains available without a ready Store.
pub(crate) struct StoreStartup {
diagnostic: std::option::Option<crate::CommandErrorDto>,
network_coherent: bool,
runtime: std::option::Option<StoreRuntime>,
store_network: std::option::Option<String>,
}
impl StoreStartup {
/// Applies Store readiness to the application-owned options DTO without exposing backend connection details.
pub(crate) fn apply_to(&self, options: &mut crate::BackfillDeskOptionsDto) {
options.network_coherent = self.network_coherent;
options.store_diagnostic = self.diagnostic.clone();
options.store_network = self.store_network.clone();
options.store_ready = self.runtime.as_ref().is_some_and(StoreRuntime::health_ready);
options.composition_ready = options.transport_ready && options.network_coherent && options.store_ready;
}
/// Temporarily lends the ready Store facade to one admitted Backfill execution.
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
let runtime = self.runtime.as_ref();
let runtime = match runtime {
std::option::Option::Some(value) if value.health_ready() => value,
_ => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run without a ready Store",
));
},
};
return runtime.take_for_run();
}
/// Restores the Store facade after one Backfill execution reaches its terminal result.
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
let runtime = self.runtime.as_ref();
let runtime = match runtime {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk cannot restore Store after runtime ownership disappeared",
));
},
};
return runtime.restore_after_run(store);
}
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let runtime = self.runtime.as_ref();
return match runtime {
std::option::Option::Some(value) => value.close().await,
std::option::Option::None => std::result::Result::Ok(()),
};
}
}
/// Executable Store runtime retained after network coherence and bounded Store health checks.
pub(crate) struct StoreRuntime {
health_ready: bool,
profile_id: String,
store: std::sync::Mutex<std::option::Option<ksp_store_lib::Store>>,
store_network: String,
}
impl StoreRuntime {
/// Returns whether the startup health probe proved the Store ready.
#[must_use]
pub(crate) const fn health_ready(&self) -> bool {
return self.health_ready;
}
/// Temporarily removes the Store from the application slot for one single-active Backfill execution.
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
let store = take_store(&self.store);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match store {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
"Backfill Desk Store is already owned by an active Backfill run",
)),
};
}
/// Restores the Store after one terminal Backfill execution so a later campaign can be admitted.
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
let locked = self.store.lock();
let mut locked = match locked {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk Store runtime state lock is poisoned",
));
},
};
if locked.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk cannot restore Store into an occupied runtime slot",
));
}
*locked = std::option::Option::Some(store);
return std::result::Result::Ok(());
}
/// Explicitly closes the Store exactly once through its backend-neutral facade.
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let store = take_store(&self.store);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match store {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
let closed = store.close().await;
return match closed {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_STORE,
store_profile = self.profile_id.as_str(),
store_network = self.store_network.as_str(),
"closed Backfill Desk Store runtime"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_SHUTDOWN_FAILED, "Backfill Desk Store shutdown failed").with_source(error),
),
};
}
}
/// Resolves the composite Store target, proves Transport/Store network coherence before Store I/O, opens Store and captures one health probe.
pub(crate) async fn initialize_store(
management: &ksp_config_lib::ConfigManagement,
transport_runtime: std::option::Option<&crate::TransportRuntime>,
) -> StoreStartup {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
};
let composite = crate::load_backfill_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
};
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
};
let resolved = management.engine().resolve_store_config_profile(&profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, false, error),
};
let profile_id = resolved.profile_id().to_owned();
let store_network = resolved.settings().network().as_str().to_owned();
let transport_network = transport_runtime.and_then(crate::TransportRuntime::coherent_network);
let transport_network = match transport_network {
std::option::Option::Some(value) => value,
std::option::Option::None => {
let error = ksp_core_lib::Error::new(
crate::ERROR_CODE_STORE_NETWORK_UNAVAILABLE,
"Backfill Desk cannot open Store before one coherent Transport network is available",
);
return unavailable_startup(std::option::Option::Some(store_network), false, error);
},
};
let coherence = validate_network_coherence(store_network.as_str(), transport_network.as_str());
if let std::result::Result::Err(error) = coherence {
return unavailable_startup(std::option::Option::Some(store_network), false, error);
}
let store = ksp_store_lib::Store::open(resolved.into_settings()).await;
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::Some(store_network), true, error),
};
let health = store.health().await;
let health_ready = store_health_ready(health.state());
let diagnostic = if health_ready { std::option::Option::None } else { std::option::Option::Some(non_ready_health_diagnostic(&health)) };
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_STORE,
store_profile = profile_id.as_str(),
store_network = store_network.as_str(),
store_ready = health_ready,
pending_migration_count = health.pending_migration_count(),
"initialized Backfill Desk Store readiness from composite-managed configuration"
);
return StoreStartup {
diagnostic,
network_coherent: true,
runtime: std::option::Option::Some(StoreRuntime {
health_ready,
profile_id,
store: std::sync::Mutex::new(std::option::Option::Some(store)),
store_network: store_network.clone(),
}),
store_network: std::option::Option::Some(store_network),
};
}
fn non_ready_health_diagnostic(health: &ksp_store_lib::StoreHealthSnapshot) -> crate::CommandErrorDto {
let code = health.last_error_code();
return match code {
std::option::Option::Some(value) => crate::CommandErrorDto {
code: value.code().to_owned(),
domain: value.domain().to_owned(),
message: "Store health probe did not prove readiness".to_owned(),
},
std::option::Option::None => crate::CommandErrorDto {
code: "store_not_ready".to_owned(),
domain: "backfill_desk".to_owned(),
message: "Store health probe did not prove readiness".to_owned(),
},
};
}
fn store_health_ready(state: ksp_store_lib::StoreHealthState) -> bool {
return match state {
ksp_store_lib::StoreHealthState::Ready => true,
ksp_store_lib::StoreHealthState::NotReady => false,
_ => false,
};
}
fn take_store(store: &std::sync::Mutex<std::option::Option<ksp_store_lib::Store>>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::Store>> {
let locked = store.lock();
let mut locked = match locked {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk Store runtime state lock is poisoned",
));
},
};
return std::result::Result::Ok(locked.take());
}
fn unavailable_startup(store_network: std::option::Option<String>, network_coherent: bool, error: ksp_core_lib::Error) -> StoreStartup {
let diagnostic = crate::CommandErrorDto::from_error(&error);
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_STORE,
error_domain = diagnostic.domain.as_str(),
error_code = diagnostic.code.as_str(),
network_coherent = network_coherent,
"Backfill Desk Store readiness is unavailable; keeping desktop shell available"
);
return StoreStartup {
diagnostic: std::option::Option::Some(diagnostic),
network_coherent,
runtime: std::option::Option::None,
store_network,
};
}
fn validate_network_coherence(store_network: &str, transport_network: &str) -> ksp_core_lib::Result<()> {
if store_network == transport_network {
return std::result::Result::Ok(());
}
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_NETWORK_MISMATCH, "Backfill Desk Store and Transport networks do not match")
.with_context("store_network", store_network)
.with_context("transport_network", transport_network),
);
}
#[cfg(test)]
#[path = "../unit_tests/store_runtime.rs"]
mod tests;

View File

@@ -0,0 +1,343 @@
// file: crates/ksp-app-backfill-desk/src/tauri.rs
// version: 9
//! Tauri runtime assembly for the KSP Backfill desktop application.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Runs the Backfill desktop application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
let context = tauri::generate_context!();
let runtime_layout = configure_packaged_runtime(&context);
if let std::result::Result::Err(error) = runtime_layout {
return std::result::Result::Err(error);
}
let app_state = crate::AppState::initialize(arguments);
let app_state = match app_state {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut builder = tauri::Builder::default();
builder = configure_state(builder, app_state);
builder = configure_plugins(builder);
builder = configure_commands(builder);
builder = configure_setup(builder);
builder = configure_window_events(builder);
let run_result = builder.run(context);
return match run_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Backfill Desk Tauri runtime")
.with_context("tauri_error", error.to_string()),
),
};
}
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
if cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
let resource_root = match resource_root {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
.with_context("tauri_error", error.to_string()),
);
},
};
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
let layout = match layout {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let working_directory = std::env::set_current_dir(layout.runtime_root());
return match working_directory {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
.with_source(error),
),
};
}
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
return builder.manage(app_state);
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tracing_plugin);
}
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![
backfill_cancel,
backfill_options,
backfill_resume,
backfill_start,
backfill_status,
backfill_validate_request,
emit_frontend_log,
get_runtime_status,
splash_frontend_ready
]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| {
let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error));
}
let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error));
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Backfill Desk Tauri shell setup completed");
return std::result::Result::Ok(());
});
}
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.on_window_event(|window, event| {
if window.label() != "main" {
return;
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let app_handle = window.app_handle().clone();
let state = app_handle.state::<crate::AppState>();
if !state.begin_shutdown() {
return;
}
let cancellation = state.cancel_active_backfill_for_shutdown();
match cancellation {
std::result::Result::Ok(accepted) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
cancellation_accepted = accepted,
"Backfill Desk main-window close requested; cooperative Backfill cancellation attempted before bounded Store shutdown"
);
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk could not request cooperative cancellation during shutdown; continuing bounded shutdown"
);
},
}
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<crate::AppState>();
let closed = state.close_store().await;
match closed {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Backfill Desk Store shutdown completed; exiting application"
);
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk Store shutdown failed; exiting application after bounded close attempt"
);
},
}
app_handle.exit(0);
});
}
});
}
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = domain,
command = command,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk Tauri command failed"
);
return crate::CommandErrorDto::from_error(error);
}
#[tauri::command]
fn backfill_cancel(job_id: String, state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::BackfillCancelResponseDto, crate::CommandErrorDto> {
let result = state.cancel_backfill(job_id.as_str());
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_cancel", crate::TRACING_DOMAIN_RUN, &error)),
};
}
#[tauri::command]
fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::BackfillDeskOptionsDto, crate::CommandErrorDto> {
let result = state.backfill_options();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_options", crate::TRACING_DOMAIN_TRANSPORT, &error)),
};
}
#[tauri::command]
fn backfill_start(
app: tauri::AppHandle,
request: crate::BackfillStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::BackfillStartResponseDto, crate::CommandErrorDto> {
let launch = state.prepare_backfill_start(request);
let launch = match launch {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(project_command_error("backfill_start", crate::TRACING_DOMAIN_RUN, &error));
},
};
let response = launch.response();
spawn_backfill_launch(app, launch);
return std::result::Result::Ok(response);
}
#[tauri::command]
fn backfill_resume(
app: tauri::AppHandle,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::BackfillResumeResponseDto, crate::CommandErrorDto> {
let launch = state.prepare_backfill_resume();
let launch = match launch {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(project_command_error("backfill_resume", crate::TRACING_DOMAIN_RUN, &error));
},
};
let response = crate::BackfillResumeResponseDto {
accepted: true,
job_id: launch.job_id.as_str().to_owned(),
state: ksp_job_api::JobState::Created.code().to_owned(),
};
spawn_backfill_launch(app, launch);
return std::result::Result::Ok(response);
}
fn spawn_backfill_launch(app: tauri::AppHandle, launch: crate::BackfillRunLaunch) {
let monitor_app = app.clone();
let monitor_source = launch.snapshots.clone();
let _monitor_task = tauri::async_runtime::spawn(async move {
monitor_backfill_status(monitor_app, monitor_source).await;
});
let _run_task = tauri::async_runtime::spawn(async move {
let state = app.state::<crate::AppState>();
let executed = state.execute_backfill_run(launch).await;
if let std::result::Result::Err(error) = executed {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk async run terminated with an error"
);
}
});
return;
}
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
let mut notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
loop {
let projected = crate::project_backfill_status(&notification);
match projected {
std::result::Result::Ok(status) => {
let main = app.get_webview_window("main");
if let std::option::Option::Some(window) = main {
let emitted = window.emit(crate::BACKFILL_STATUS_EVENT_NAME, status);
if emitted.is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
"Backfill Desk could not emit latest-value monitoring status to the main window"
);
}
}
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk could not project latest-value monitoring status"
);
},
}
if notification.state().is_terminal() {
return;
}
let observed = notification.sequence();
notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::wait_for_change(&source, observed).await;
}
}
#[tauri::command]
fn backfill_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::option::Option<crate::BackfillRunStatusDto>, crate::CommandErrorDto> {
let result = state.backfill_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_status", crate::TRACING_DOMAIN_RUN, &error)),
};
}
#[tauri::command]
fn backfill_validate_request(
request: crate::BackfillStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::BackfillRequestPreviewDto, crate::CommandErrorDto> {
let result = state.validate_backfill_request(request);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_validate_request", crate::TRACING_DOMAIN_REQUEST, &error)),
};
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
};
}
#[tauri::command]
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
let result = state.shell_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,
webview_window: tauri::WebviewWindow,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
};
}

View File

@@ -0,0 +1,179 @@
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
// version: 6
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
/// Safe and executable Transport runtime retained by the application state.
pub(crate) struct TransportRuntime {
pool: ksp_onchain_transport_lib::HttpTransportPool,
profile_id: String,
}
impl TransportRuntime {
/// Returns a shareable clone of the Transport-owned HTTP pool for one admitted Backfill execution.
#[must_use]
pub(crate) fn pool(&self) -> ksp_onchain_transport_lib::HttpTransportPool {
return self.pool.clone();
}
/// Returns the composite-selected Transport profile identifier.
#[must_use]
pub(crate) fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the one configured Transport network only when the active HTTP pool is logically coherent.
#[must_use]
pub(crate) fn coherent_network(&self) -> std::option::Option<String> {
let networks = configured_networks(&self.pool.snapshot());
if networks.len() != 1 {
return std::option::Option::None;
}
return networks.into_iter().next();
}
/// Builds the current safe Transport-only projection for the Backfill options surface.
pub(crate) fn options(&self) -> ksp_core_lib::Result<crate::BackfillDeskOptionsDto> {
let snapshot = self.pool.snapshot();
let configured_networks = configured_networks(&snapshot);
let http_routes = compatible_backfill_http_routes(&self.pool, &snapshot);
let http_routes = match http_routes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let limits = crate::backfill_request_limits();
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_ready = configured_networks.len() == 1 && !http_routes.is_empty() && snapshot.available_endpoint_count() > 0;
return std::result::Result::Ok(crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: false,
configured_networks,
http_routes,
limits,
network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,
transport_diagnostic: std::option::Option::None,
transport_ready,
});
}
}
/// Resolves the composite-selected Transport profile and builds the executable HTTP pool.
pub(crate) fn initialize_transport(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<TransportRuntime> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::load_backfill_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().resolve_transport_config_profile(&profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile_id = resolved.profile_id().to_owned();
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings());
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = TransportRuntime { pool, profile_id };
let options = runtime.options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_TRANSPORT,
transport_profile = runtime.profile_id(),
network_count = options.configured_networks.len(),
http_route_count = options.http_routes.len(),
transport_ready = options.transport_ready,
"initialized Backfill Desk HTTP Transport readiness from composite-managed configuration"
);
return std::result::Result::Ok(runtime);
}
fn compatible_backfill_http_routes(
pool: &ksp_onchain_transport_lib::HttpTransportPool,
snapshot: &ksp_onchain_transport_lib::HttpTransportPoolSnapshot,
) -> ksp_core_lib::Result<std::vec::Vec<crate::BackfillHttpRouteOptionDto>> {
let signatures = required_http_rpc_method("getSignaturesForAddress");
let signatures = match signatures {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transaction = required_http_rpc_method("getTransaction");
let transaction = match transaction {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut candidates = std::collections::BTreeMap::<String, std::collections::BTreeSet<String>>::new();
for endpoint in snapshot.endpoints() {
if !endpoint.enabled() {
continue;
}
for role in endpoint.roles() {
if role.enabled() {
candidates.entry(role.role().to_owned()).or_default().insert(endpoint.provider().to_owned());
}
}
}
let mut compatible = std::vec::Vec::new();
for (candidate, providers) in candidates {
let role = ksp_onchain_transport_lib::HttpRoleName::new(candidate.clone());
if pool.select_for_method(&role, signatures).is_ok() && pool.select_for_method(&role, transaction).is_ok() {
let providers = providers.into_iter().collect::<std::vec::Vec<_>>();
compatible.push(crate::BackfillHttpRouteOptionDto { pooled: providers.len() > 1, providers, role: candidate });
}
}
return std::result::Result::Ok(compatible);
}
fn configured_networks(snapshot: &ksp_onchain_transport_lib::HttpTransportPoolSnapshot) -> std::vec::Vec<String> {
let mut values = snapshot
.endpoints()
.iter()
.filter_map(|endpoint| {
if endpoint.enabled() {
return std::option::Option::Some(endpoint.cluster().to_owned());
}
return std::option::Option::None;
})
.collect::<std::vec::Vec<_>>();
values.sort();
values.dedup();
return values;
}
fn required_http_rpc_method(method: &'static str) -> ksp_core_lib::Result<&'static ksp_onchain_transport_lib::HttpRpcMethodDescriptor> {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TRANSPORT_READINESS_INVALID, "Backfill Desk Transport registry is missing a required HTTP RPC method")
.with_context("rpc_method", method),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/transport_runtime.rs"]
mod tests;

View File

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

View File

@@ -0,0 +1,173 @@
// file: crates/ksp-app-backfill-desk/src/tw_splash.rs
// version: 1
//! Tauri-window lifecycle for the Backfill Desk splash window.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal splash window label.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
/// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Backfill Desk splash window is missing")
.with_context("window_label", WINDOW_LABEL_SPLASH),
),
};
}
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
.with_context("window_label", invoking_window.label()),
);
}
if !state.begin_splash_sequence() {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
return std::result::Result::Ok(());
}
let settings = state.splash_settings();
let lifecycle_started = std::time::Instant::now();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
fade_in_ms = settings.fade_in_ms(),
fade_in_source = settings.fade_in_source(),
minimum_ms = settings.minimum_ms(),
minimum_source = settings.minimum_source(),
fade_out_ms = settings.fade_out_ms(),
fade_out_source = settings.fade_out_source(),
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
"splash frontend readiness accepted; starting splash to main lifecycle"
);
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Initialisation de Backfill Desk...", "info");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
let show_main = crate::show_main_window(&app);
if let std::result::Result::Err(error) = show_main {
return std::result::Result::Err(error);
}
let destroyed = invoking_window.destroy();
if let std::result::Result::Err(error) = destroyed {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Backfill Desk splash window")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_source(error),
);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash window destroyed after main activation"
);
return std::result::Result::Ok(());
}
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
return emit_order(
window,
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
);
}
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
if !cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
return emit_order(
window,
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
);
}
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
let wait_started = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = window.label(),
phase,
configured_wait_ms = duration_ms,
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash lifecycle wait completed"
);
return;
}
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
let action = order.action.clone();
let emitted = window.emit(SPLASH_EVENT_NAME, order);
return match emitted {
std::result::Result::Ok(()) => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Backfill Desk splash order")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_context("action", action)
.with_source(error),
),
};
}

View File

@@ -0,0 +1,71 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Backfill Desk",
"identifier": "com.sasedev.ksp-app-backfill-desk",
"build": {
"beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1436",
"beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"
},
"app": {
"windows": [
{
"label": "splash",
"url": "splash.html",
"title": "Chargement — KSP Backfill Desk",
"width": 960,
"height": 637,
"resizable": false,
"decorations": false,
"transparent": true,
"center": true,
"alwaysOnTop": true
},
{
"label": "main",
"url": "main.html",
"title": "KSP Backfill Desk",
"width": 1500,
"height": 960,
"minWidth": 1024,
"minHeight": 768,
"center": true,
"visible": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/favicon.png",
"icons/favicon.ico"
],
"resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-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/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"
}
}
}

View File

@@ -0,0 +1,308 @@
// file: crates/ksp-app-backfill-desk/tests/config_composition.rs
// version: 7
//! Config composite, Transport readiness and Store-network composition contracts for Backfill Desk through pre.006.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn workspace_engine() -> ksp_core_lib::Result<ksp_config_lib::ConfigDocumentEngine> {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn assert_component(composite: &ksp_config_lib::ResolvedConfigComposite, component_id: &str, file_id: &str, profile_id: &str) {
let component = composite.component(component_id);
assert!(component.is_some(), "missing component {component_id}");
if let std::option::Option::Some(component) = component {
assert_eq!(component.resolved().file_id().as_str(), file_id);
assert_eq!(component.resolved().profile_id(), profile_id);
assert_eq!(component.resolved().selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
}
}
#[test]
fn pre_003_devnet_profile_selects_trace_logging_transport_and_store() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
assert!(file_id.is_ok(), "Backfill Desk composite file_id should be valid: {file_id:?}");
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some("devnet"));
assert!(composite.is_ok(), "committed Backfill Desk devnet profile should resolve: {composite:?}");
if let std::result::Result::Ok(composite) = composite {
assert_eq!(composite.profile_id(), "devnet");
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "supertrace");
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "devnet_public");
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, "devnet");
let logging = composite.component("logging");
assert!(logging.is_some(), "Backfill Desk composite should expose Logging");
if let std::option::Option::Some(logging) = logging {
assert_eq!(logging.resolved().profile().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("trace"));
}
}
}
#[test]
fn pre_003_named_profiles_keep_transport_and_store_network_selection_paired() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for (profile_id, logging_profile, transport_profile, store_profile) in
[("mainnet", "supertrace", "mainnet_backfill_pool", "mainnet"), ("testnet", "console_info", "publicnode_testnet", "testnet")]
{
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
assert!(composite.is_ok(), "Backfill Desk composite profile {profile_id} should resolve: {composite:?}");
if let std::result::Result::Ok(composite) = composite {
assert_eq!(composite.profile_id(), profile_id);
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, logging_profile);
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, transport_profile);
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, store_profile);
}
}
}
#[test]
fn pre_004_default_composite_builds_http_pool_for_both_backfill_rpc_methods() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some("devnet"));
assert!(composite.is_ok(), "committed Backfill Desk devnet profile should resolve: {composite:?}");
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let transport = composite.component("transport");
assert!(transport.is_some(), "Backfill Desk composite should expose Transport");
let transport = match transport {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok(), "Config environment should load for committed public devnet profile");
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let resolved = engine.resolve_transport_config_profile(transport.resolved(), &environment);
assert!(resolved.is_ok(), "composite-selected Transport should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.profile_id(), "devnet_public");
assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings());
assert!(pool.is_ok(), "composite-selected Transport settings should construct an HTTP pool");
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let snapshot = pool.snapshot();
let mut clusters = snapshot
.endpoints()
.iter()
.filter_map(|endpoint| {
if endpoint.enabled() {
return std::option::Option::Some(endpoint.cluster().to_owned());
}
return std::option::Option::None;
})
.collect::<std::vec::Vec<_>>();
clusters.sort();
clusters.dedup();
assert_eq!(clusters, vec!["devnet".to_owned()]);
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
for method in ["getSignaturesForAddress", "getTransaction"] {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method);
assert!(descriptor.is_some(), "required Transport method {method} should exist");
if let std::option::Option::Some(descriptor) = descriptor {
let selection = pool.select_for_method(&role, descriptor);
assert!(selection.is_ok(), "role default should route {method}: {selection:?}");
}
}
}
fn declared_http_networks(profile: &ksp_config_lib::ResolvedConfigProfile) -> std::vec::Vec<String> {
let endpoints = profile.profile().get("endpoints").and_then(serde_json::Value::as_array);
let endpoints = match endpoints {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::vec::Vec::new(),
};
let mut networks = endpoints
.iter()
.filter_map(|endpoint| {
let enabled = endpoint.get("enabled").and_then(serde_json::Value::as_bool);
if enabled != std::option::Option::Some(true) {
return std::option::Option::None;
}
return endpoint.get("cluster").and_then(serde_json::Value::as_str).map(str::to_owned);
})
.collect::<std::vec::Vec<_>>();
networks.sort();
networks.dedup();
return networks;
}
#[test]
fn pre_005_all_composite_profiles_keep_declared_http_and_store_networks_identical_without_external_secrets() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for profile_id in ["devnet", "mainnet", "testnet"] {
let composite = engine.load_resolved_composite(&file_id, std::option::Option::Some(profile_id));
assert!(composite.is_ok(), "Backfill Desk composite profile {profile_id} should resolve: {composite:?}");
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let transport = composite.component("transport");
let store = composite.component("store");
assert!(transport.is_some(), "profile {profile_id} should expose Transport");
assert!(store.is_some(), "profile {profile_id} should expose Store");
let (transport, store) = match (transport, store) {
(std::option::Option::Some(transport), std::option::Option::Some(store)) => (transport, store),
_ => continue,
};
let networks = declared_http_networks(transport.resolved());
assert_eq!(networks.len(), 1, "profile {profile_id} must declare exactly one enabled HTTP network");
let store_network = store.resolved().profile().get("network").and_then(serde_json::Value::as_str);
assert!(store_network.is_some(), "profile {profile_id} Store should declare one logical network");
if let (std::option::Option::Some(transport_network), std::option::Option::Some(store_network)) = (networks.first(), store_network) {
assert_eq!(transport_network.as_str(), store_network, "profile {profile_id} Store/Transport network mismatch");
}
}
}
#[test]
fn pre_006_default_mainnet_profile_exposes_pool_and_targeted_http_routes_without_secret_provider_data() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let composite = engine.load_resolved_composite(&file_id, std::option::Option::None);
assert!(composite.is_ok(), "default Backfill Desk composite should resolve: {composite:?}");
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(composite.profile_id(), "mainnet");
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "supertrace");
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "mainnet_backfill_pool");
assert_component(&composite, "store", ksp_config_lib::FILE_ID_STD_STORE, "mainnet");
let transport = match composite.component("transport") {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let profile = transport.resolved().profile();
let endpoints = profile.get("endpoints").and_then(serde_json::Value::as_array);
assert!(endpoints.is_some(), "mainnet Backfill Transport profile should expose HTTP endpoints");
let endpoints = match endpoints {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let providers = endpoints
.iter()
.filter_map(|endpoint| return endpoint.get("provider").and_then(serde_json::Value::as_str))
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(providers, std::collections::BTreeSet::from(["publicnode", "solana-public"]));
let roles = endpoints
.iter()
.flat_map(|endpoint| return endpoint.get("roles").and_then(serde_json::Value::as_array).into_iter().flatten())
.filter_map(|role| return role.get("role").and_then(serde_json::Value::as_str))
.collect::<std::collections::BTreeSet<_>>();
assert!(roles.contains("backfill_pool"));
assert!(roles.contains("backfill_publicnode"));
assert!(roles.contains("backfill_solana_public"));
}
#[test]
fn pre_013_composite_profile_and_component_inventory_is_exact() {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let source = std::fs::read_to_string(workspace.join("config/composite.ksp-app-backfill-desk.json"));
assert!(source.is_ok(), "Backfill Desk composite should be readable");
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let document = serde_json::from_str::<serde_json::Value>(source.as_str());
assert!(document.is_ok(), "Backfill Desk composite should remain valid JSON");
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(document.get("default_profile").and_then(serde_json::Value::as_str), std::option::Option::Some("mainnet"));
let profiles = document.get("profiles").and_then(serde_json::Value::as_array);
assert!(profiles.is_some(), "Backfill Desk composite should expose profiles");
let profiles = match profiles {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let profile_ids = profiles
.iter()
.filter_map(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str))
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(profile_ids, std::collections::BTreeSet::from(["devnet", "mainnet", "testnet"]));
for profile in profiles {
let documents = profile.get("documents").and_then(serde_json::Value::as_array);
assert!(documents.is_some(), "every Backfill Desk profile should expose component documents");
let documents = match documents {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let component_ids = documents
.iter()
.filter_map(|document| return document.get("component_id").and_then(serde_json::Value::as_str))
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(component_ids, std::collections::BTreeSet::from(["logging", "store", "transport"]));
}
}

View File

@@ -0,0 +1,78 @@
// file: crates/ksp-app-backfill-desk/tests/dependency_boundary.rs
// version: 1
//! Final dependency-boundary checks for the KSP Backfill desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn pre_013_manifest_ksp_dependency_surface_is_exact_and_backend_neutral() {
let manifest = read_text(app_root().join("Cargo.toml").as_path());
let dependency_section = manifest.split("[dependencies]").nth(1).and_then(|tail| return tail.split("[dev-dependencies]").next());
assert!(dependency_section.is_some(), "Backfill Desk manifest should expose a dependency section");
let dependency_section = match dependency_section {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let ksp_dependencies = dependency_section
.lines()
.filter_map(|line| {
let name = line.split('=').next().map(str::trim);
return match name {
std::option::Option::Some(value) if value.starts_with("ksp-") => std::option::Option::Some(value),
_ => std::option::Option::None,
};
})
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
ksp_dependencies,
std::collections::BTreeSet::from([
"ksp-config-lib",
"ksp-core-lib",
"ksp-job-api",
"ksp-job-backfill-lib",
"ksp-logging-lib",
"ksp-onchain-transport-lib",
"ksp-store-lib",
])
);
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "tokio-postgres", "reqwest", "tonic", "yellowstone-grpc-proto", "mpl-token-metadata"] {
assert!(!dependency_section.contains(forbidden), "Backfill Desk manifest owns forbidden dependency {forbidden}");
}
}
#[test]
fn pre_013_production_sources_do_not_cross_into_physical_store_or_provider_clients() {
let source_root = app_root().join("src");
let entries = std::fs::read_dir(source_root.as_path());
assert!(entries.is_ok(), "Backfill Desk source directory should be readable");
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") {
continue;
}
let source = read_text(path.as_path());
for forbidden in ["ksp_store_api", "ksp_store_postgres_lib", "tokio_postgres", "reqwest::", "tonic::", "yellowstone_grpc_proto"] {
assert!(!source.contains(forbidden), "{} crosses forbidden dependency boundary via {forbidden}", path.display());
}
}
}

View File

@@ -0,0 +1,464 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 16
//! Structural desktop contract checks for the Backfill Desk scaffold.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn workspace_root() -> std::path::PathBuf {
return app_root().join("../..");
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
fn read_json(path: &std::path::Path) -> serde_json::Value {
let source = read_text(path);
let parsed = serde_json::from_str(source.as_str());
assert!(parsed.is_ok(), "unable to parse {}", path.display());
return match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => serde_json::Value::Null,
};
}
fn read_bytes(path: &std::path::Path) -> std::vec::Vec<u8> {
let source = std::fs::read(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => std::vec::Vec::new(),
};
}
#[test]
fn pre_002_rust_package_is_mixed_lib_bin_and_workspace_member() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("name = \"ksp-app-backfill-desk\""));
assert!(manifest.contains("name = \"ksp_app_backfill_desk_lib\""));
assert!(manifest.contains("[[bin]]"));
let workspace = read_text(workspace_root().join("Cargo.toml").as_path());
assert!(workspace.contains("crates/ksp-app-backfill-desk"));
let launcher = read_text(root.join("src/main.rs").as_path());
assert!(launcher.contains("ksp_app_backfill_desk_lib::run"));
assert!(launcher.contains("try_lock_exclusive"));
}
#[test]
fn pre_002_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() {
let root = app_root();
let vite = read_text(root.join("vite.config.ts").as_path());
assert!(vite.contains("port: 1436"));
assert!(vite.contains("port: 1437"));
assert!(vite.contains("strictPort: true"));
let tauri = read_text(root.join("tauri.conf.json").as_path());
assert!(tauri.contains("KSP Backfill Desk"));
assert!(tauri.contains("com.sasedev.ksp-app-backfill-desk"));
assert!(tauri.contains("http://localhost:1436"));
assert!(tauri.contains("npm run dev"));
assert!(tauri.contains("npm run build"));
assert!(tauri.contains("../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"));
}
#[test]
fn pre_002_package_dependencies_match_desk_baseline() {
let package = read_text(app_root().join("package.json").as_path());
for required in [
"@fltsci/tauri-plugin-tracing",
"@fortawesome/fontawesome-free",
"@tauri-apps/api",
"bootstrap",
"resize-observer-polyfill",
"simplebar",
"@tauri-apps/cli",
"@types/bootstrap",
"@types/node",
"sass-embedded",
"typescript",
"vite",
] {
assert!(package.contains(required), "missing Desk package baseline dependency {required}");
}
assert!(!package.contains("package-lock"));
}
#[test]
fn pre_002_splash_assets_and_styles_match_existing_desk_template() {
let root = app_root();
let reference = workspace_root().join("crates/ksp-app-solprices-desk");
for relative in ["frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png", "icons/favicon.png", "icons/favicon.ico"] {
assert_eq!(read_bytes(root.join(relative).as_path()), read_bytes(reference.join(relative).as_path()), "Desk asset differs: {relative}");
}
let splash = read_text(root.join("frontend/splash.html").as_path());
assert!(splash.contains("imgs/splash.png"));
assert!(splash.contains("Backfill Desk"));
let splash_style = read_text(root.join("frontend/sass/splash.scss").as_path());
let reference_style = read_text(reference.join("frontend/sass/splash.scss").as_path());
let normalized = splash_style.replace("crates/ksp-app-backfill-desk", "crates/ksp-app-solprices-desk").replace("// version: 1", "// version: 2");
assert_eq!(normalized, reference_style);
}
#[test]
fn pre_002_shell_and_splash_use_shared_frontend_tooling() {
let root = app_root();
let shell = read_text(root.join("frontend/main.html").as_path());
for required in ["app-header", "app-sidebar", "app-sidebar-scroll", "app-content", "app-footer", "data-simplebar"] {
assert!(shell.contains(required), "Backfill Desk shell is missing template marker {required}");
}
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(main.contains("resize-observer-polyfill"));
assert!(main.contains("simplebar"));
assert!(main.contains("bootstrap"));
assert!(main.contains("Backfill Desk frontend control clicked"));
assert!(main.contains("Backfill Desk navigation tab clicked"));
assert!(main.contains("Backfill Desk runtime status refresh button clicked"));
let splash = read_text(root.join("frontend/ts/splash.ts").as_path());
assert!(splash.contains("Splash opacity animation started"));
assert!(splash.contains("Backfill Desk splash frontend loaded"));
}
#[test]
fn pre_003_composite_packaging_follows_atomic_config_registry_contract() {
let root = app_root();
let config = read_json(root.join("tauri.conf.json").as_path());
let resources = config
.get("bundle")
.and_then(|value| {
return value.get("resources");
})
.and_then(serde_json::Value::as_object);
assert!(resources.is_some());
let resources = match resources {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(resources.len(), 14);
for required in [
"../../config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json",
"../../config/std.offchain_transport.json",
"../../config/std.store.json",
"../../config/std.transport.json",
"../../config/std.wallet.json",
"../../config/schemas/composite.schema.json",
"../../config/schemas/std.logging.schema.json",
"../../config/schemas/std.offchain_transport.schema.json",
"../../config/schemas/std.store.schema.json",
"../../config/schemas/std.transport.schema.json",
"../../config/schemas/std.wallet.schema.json",
] {
assert!(resources.contains_key(required), "missing packaged Config resource {required}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("prepare_packaged_runtime"));
assert!(tauri.contains("resource_dir"));
assert!(tauri.contains("set_current_dir"));
}
#[test]
fn pre_003_bootstrap_keeps_logging_composition_separate_from_transport_runtime() {
let root = app_root();
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK"));
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_LOGGING"));
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_TRANSPORT"));
assert!(bootstrap.contains("COMPOSITE_COMPONENT_ID_STORE"));
assert!(bootstrap.contains("LogFilterLevel::Trace"));
for forbidden in ["HttpTransportPool", "Store::open", "BackfillJobRuntime", "BackfillRequest"] {
assert!(!bootstrap.contains(forbidden), "bootstrap.rs absorbs another runtime responsibility: {forbidden}");
}
}
#[test]
fn pre_004_transport_runtime_builds_pool_and_validates_both_required_rpc_methods() {
let root = app_root();
let transport = read_text(root.join("src/transport_runtime.rs").as_path());
assert!(transport.contains("HttpTransportPool::new"));
assert!(transport.contains("getSignaturesForAddress"));
assert!(transport.contains("getTransaction"));
assert!(transport.contains("select_for_method"));
assert!(transport.contains("configured_networks"));
assert!(transport.contains("compatible_backfill_http_routes"));
assert!(transport.contains("endpoint.provider()"));
for forbidden in ["url()", "Store::open", "BackfillJobRuntime", "BackfillRequest"] {
assert!(!transport.contains(forbidden), "pre.004 opens or projects forbidden surface {forbidden}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("backfill_options"));
}
#[test]
fn pre_005_store_readiness_proves_network_before_open_and_closes_on_main_window_exit() {
let root = app_root();
let store = read_text(root.join("src/store_runtime.rs").as_path());
let coherence = store.find("validate_network_coherence(store_network.as_str(), transport_network.as_str())");
let open = store.find("ksp_store_lib::Store::open");
assert!(coherence.is_some());
assert!(open.is_some());
if let (std::option::Option::Some(coherence), std::option::Option::Some(open)) = (coherence, open) {
assert!(coherence < open, "Store network coherence must be proven before physical Store open");
}
assert!(store.contains("store.health().await"));
assert!(store.contains("store.close().await"));
assert!(store.contains("StoreHealthState::Ready"));
assert!(store.contains("StoreHealthState::NotReady"));
assert!(!store.contains("BackfillJobRuntime"));
assert!(!store.contains("BackfillRequest"));
let app_state = read_text(root.join("src/app_state.rs").as_path());
assert!(app_state.contains("initialize_store"));
assert!(app_state.contains("composition_ready"));
assert!(app_state.contains("close_store"));
let tauri = read_text(root.join("src/tauri.rs").as_path());
for required in ["on_window_event", "WindowEvent::CloseRequested", "prevent_close", "async_runtime::spawn", "close_store", "app_handle.exit(0)"] {
assert!(tauri.contains(required), "missing bounded Store shutdown lifecycle marker {required}");
}
}
#[test]
fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_runtime() {
let root = app_root();
let workspace = root.join("../..");
let composite = read_text(workspace.join("config/composite.ksp-app-backfill-desk.json").as_path());
assert!(composite.contains("\"default_profile\": \"mainnet\""));
assert!(composite.contains("\"profile_id\": \"mainnet_backfill_pool\""));
let transport = read_text(workspace.join("config/std.transport.json").as_path());
for required in ["mainnet_backfill_pool", "backfill_pool", "backfill_publicnode", "backfill_solana_public", "solana-rpc.publicnode.com"] {
assert!(transport.contains(required), "missing Backfill HTTP route marker {required}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("Backfill Desk HTTP route selection changed"));
assert!(frontend.contains("backfill_options"));
let html = read_text(root.join("frontend/main.html").as_path());
assert!(html.contains("backfillHttpRoute"));
for forbidden in ["grpc", "websocket"] {
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
}
}
#[test]
fn pre_007_campaign_dto_and_backend_mapping_remain_the_authoritative_start_input() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
for required in [
"BackfillStartRequestDto",
"BackfillRequestPreviewDto",
"MAX_BACKFILL_CANDIDATES",
"MAX_BACKFILL_HYDRATION_CONCURRENCY",
"MAX_BACKFILL_PAGES",
"MAX_BACKFILL_PAGE_SIZE",
] {
assert!(dto.contains(required), "missing pre.007 DTO/request bound marker {required}");
}
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
for required in ["BackfillRequest::new", "HttpRoleName::new", "latest_address", "before_address", "after_address", "explicit_signatures"] {
assert!(mapping.contains(required), "missing strict Backfill request mapping marker {required}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("backfill_validate_request"));
let html = read_text(root.join("frontend/main.html").as_path());
for required in [
"backfillCampaignForm",
"backfillScopeKind",
"backfillCommitment",
"backfillAddress",
"backfillAnchorSignature",
"backfillExplicitSignatures",
"backfillMinContextSlot",
"validateBackfillRequest",
"Valider la requête",
] {
assert!(html.contains(required), "missing campaign form marker {required}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("backfill_validate_request"));
assert!(frontend.contains("Backfill Desk campaign validation requested"));
assert!(frontend.contains("Aucun job n'a été démarré"));
assert!(frontend.contains("backfill_validate_request"));
assert!(tauri.contains("backfill_validate_request"));
}
#[test]
fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_backend_owned() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let run = read_text(root.join("src/backfill_run.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
let handle_index = state.find("let handle = runtime.handle();");
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle, retained_request)");
assert!(handle_index.is_some());
assert!(install_index.is_some());
if let (std::option::Option::Some(handle_index), std::option::Option::Some(install_index)) = (handle_index, install_index) {
assert!(handle_index < install_index);
}
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
assert!(run.contains("std::sync::Mutex<std::option::Option<ActiveBackfillRun>>"));
assert!(tauri.contains("backfill_start"));
let prepare_index = tauri.find("state.prepare_backfill_start(request)");
assert!(prepare_index.is_some());
if let std::option::Option::Some(prepare_index) = prepare_index {
let start_tail = &tauri[prepare_index..];
let spawn_index = start_tail.find("tauri::async_runtime::spawn(async move");
assert!(spawn_index.is_some());
}
assert!(html.contains("startBackfillRequest"));
assert!(html.contains("Démarrer le backfill"));
assert!(frontend.contains("Backfill Desk campaign Start requested"));
assert!(frontend.contains("backfill_start"));
}
#[test]
fn pre_009_latest_value_monitoring_uses_snapshot_source_event_bridge_and_resynchronization_command() {
let root = app_root();
let run = read_text(root.join("src/backfill_run.rs").as_path());
let status = read_text(root.join("src/backfill_status.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(run.contains("last_terminal"));
assert!(run.contains("current_notification"));
assert!(status.contains("BackfillRunStatusDto"));
assert!(status.contains("checkpoint_present"));
assert!(status.contains("contiguous_completed"));
assert!(tauri.contains("backfill_status"));
assert!(tauri.contains("wait_for_change"));
assert!(tauri.contains("ksp-backfill-status") || tauri.contains("BACKFILL_STATUS_EVENT_NAME"));
assert!(frontend.contains("listen<BackfillRunStatusDto>"));
assert!(frontend.contains("syncBackfillStatus"));
assert!(!frontend.contains("parseBackfillLog"));
}
#[test]
fn pre_010_cancel_is_targeted_idempotent_and_shutdown_requests_cooperative_cancellation_before_exit() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let run = read_text(root.join("src/backfill_run.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(run.contains("current.job_id.as_str() != job_id"));
assert!(run.contains("current.handle.cancel()"));
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_MISMATCH"));
assert!(state.contains("cancel_backfill"));
assert!(state.contains("cancel_active_backfill_for_shutdown"));
assert!(tauri.contains("backfill_cancel"));
assert!(tauri.contains("cancel_active_backfill_for_shutdown"));
let shutdown_cancel = tauri.find("cancel_active_backfill_for_shutdown");
let shutdown_exit = tauri.find("app_handle.exit(0)");
assert!(shutdown_cancel.is_some());
assert!(shutdown_exit.is_some());
if let (std::option::Option::Some(cancel), std::option::Option::Some(exit)) = (shutdown_cancel, shutdown_exit) {
assert!(cancel < exit);
}
assert!(html.contains("cancelBackfillRun"));
assert!(html.contains("Annuler"));
assert!(frontend.contains("backfill_cancel"));
assert!(frontend.contains("{ jobId: status.jobId }"));
}
#[test]
fn pre_011_resume_reissues_rust_only_checkpoint_for_new_backend_job_without_ipc_checkpoint_material() {
let root = app_root();
let workspace = root.join("../..");
let checkpoint = read_text(workspace.join("crates/ksp-job-backfill-lib/src/checkpoint.rs").as_path());
assert!(checkpoint.contains("reissue_for_job"));
let request = read_text(workspace.join("crates/ksp-job-backfill-lib/src/request.rs").as_path());
assert!(request.contains("pub fn resume_for_job"));
assert!(request.contains("validate_request_checkpoint"));
assert!(request.contains("checkpoint.reissue_for_job(job_id.clone())"));
let run = read_text(root.join("src/backfill_run.rs").as_path());
for required in ["TerminalBackfillRun", "request: ksp_job_backfill_lib::BackfillRequest", "snapshot().checkpoint()", "resume_for_job(job_id, checkpoint)"] {
assert!(run.contains(required), "missing Rust-only Resume ownership marker {required}");
}
let state = read_text(root.join("src/app_state.rs").as_path());
for required in ["prepare_backfill_resume", "options.composition_ready", "store_network_matches", "role_available"] {
assert!(state.contains(required), "missing Resume admission marker {required}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("backfill_resume"));
assert!(tauri.contains("spawn_backfill_launch"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("resumeBackfillRun"));
assert!(frontend.contains("checkpointPresent"));
assert!(frontend.contains("backfill_resume"));
let html = read_text(root.join("frontend/main.html").as_path());
assert!(html.contains("resumeBackfillRun"));
assert!(html.contains("Reprendre"));
}
#[test]
fn pre_012_free_address_autocomplete_is_html_datalist_derived_from_core_registry() {
let root = app_root();
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("ksp_core_lib::entries()"));
assert!(dto.contains("ProgramIdAutocompleteOptionDto"));
assert!(dto.contains("program_id_autocomplete_options"));
let html = read_text(root.join("frontend/main.html").as_path());
assert!(html.contains("list=\"backfillProgramIds\""));
assert!(html.contains("<datalist id=\"backfillProgramIds\"></datalist>"));
assert!(html.contains("toute adresse Solana valide reste acceptée"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for required in [
"renderProgramIdAutocomplete(options.programIdOptions)",
"option.value = program.programId",
"option.dataset.code = program.code",
"option.dataset.domain = program.domain",
"registryMatch: selected !== null",
"Backfill Desk Program ID autocomplete dataset rendered",
] {
assert!(frontend.contains(required), "missing pre.012 autocomplete marker {required}");
}
for forbidden in
["AddressLookupTab1e1111111111111111111111111", "ComputeBudget111111111111111111111111111111", "Vote111111111111111111111111111111111111111"]
{
assert!(!frontend.contains(forbidden), "frontend hardcodes canonical Program ID {forbidden}");
assert!(!html.contains(forbidden), "HTML hardcodes canonical Program ID {forbidden}");
}
}
#[test]
fn pre_013_final_v1_command_and_frontend_control_inventory_is_exact() {
let root = app_root();
let tauri = read_text(root.join("src/tauri.rs").as_path());
let commands = [
"backfill_cancel",
"backfill_options",
"backfill_resume",
"backfill_start",
"backfill_status",
"backfill_validate_request",
"emit_frontend_log",
"get_runtime_status",
"splash_frontend_ready",
];
for command in commands {
assert!(tauri.contains(format!("fn {command}").as_str()), "missing final Tauri command {command}");
}
assert_eq!(tauri.matches("#[tauri::command]").count(), commands.len());
for forbidden in ["backfill_reset", "backfill_delete", "backfill_retry", "backfill_schedule", "backfill_history"] {
assert!(!tauri.contains(forbidden), "unexpected V1 Tauri command marker {forbidden}");
}
let frontend = read_text(root.join("frontend/main.html").as_path());
for required in ["startBackfillRequest", "cancelBackfillRun", "resumeBackfillRun", "refreshBackfillStatus", "backfillProgramIds"] {
assert!(frontend.contains(required), "missing final frontend control {required}");
}
}

View File

@@ -0,0 +1,395 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
// version: 16
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn struct_source<'a>(source: &'a str, marker: &str) -> &'a str {
let start = match source.find(marker) {
std::option::Option::Some(value) => value,
std::option::Option::None => return "",
};
let tail = &source[start..];
let end = match tail.find("\n}") {
std::option::Option::Some(value) => value + 2,
std::option::Option::None => return "",
};
return &tail[..end];
}
fn collect_files(directory: &std::path::Path, extension: &str, files: &mut std::vec::Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(directory);
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
collect_files(path.as_path(), extension, files);
continue;
}
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some(extension) {
files.push(path);
}
}
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn capability_surface_remains_core_plus_tracing_while_backfill_runtime_stays_backend_only() {
let root = app_root();
let capability = read_text(root.join("capabilities/default.json").as_path());
assert!(capability.contains("\"core:default\""));
assert!(capability.contains("\"tracing:default\""));
for forbidden in ["dialog:", "fs:", "http:", "shell:"] {
assert!(!capability.contains(forbidden));
}
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("tauri-plugin-tracing.workspace = true"));
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden direct dependency: {forbidden}");
}
}
#[test]
fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() {
let root = app_root();
let mut typescript = std::vec::Vec::new();
collect_files(root.join("frontend/ts").as_path(), "ts", &mut typescript);
assert!(!typescript.is_empty());
for path in typescript {
let source = read_text(path.as_path());
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert(", "window.confirm(", "window.prompt("] {
assert!(!source.contains(forbidden), "{} contains forbidden scaffold surface {forbidden}", path.display());
}
}
}
#[test]
fn pre_002_tauri_commands_remain_centralized() {
let root = app_root();
let mut rust_files = std::vec::Vec::new();
collect_files(root.join("src").as_path(), "rs", &mut rust_files);
let mut command_count = 0usize;
for path in rust_files {
let source = read_text(path.as_path());
let count = source.matches("#[tauri::command]").count();
if path.file_name().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("tauri.rs") {
command_count += count;
} else {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
}
assert_eq!(command_count, 9);
}
#[test]
fn pre_002_frontend_instrumentation_avoids_business_or_secret_payloads() {
let root = app_root();
let main = read_text(root.join("frontend/ts/main.ts").as_path());
for required in [
"Backfill Desk frontend control clicked",
"Backfill Desk navigation tab clicked",
"Backfill Desk runtime status refresh button clicked",
"Frontend IPC command requested",
] {
if required == "Frontend IPC command requested" {
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
assert!(invoke.contains(required));
} else {
assert!(main.contains(required));
}
}
for forbidden in [
"JSON.stringify(status)",
"apiKey",
"authorization",
"database_url",
"campaign validation requested\", {\n address",
"campaign validation requested\", {\n anchorSignature",
"campaign validation requested\", {\n explicitSignatures",
"campaign validation requested\", {\n minContextSlot",
] {
assert!(!main.contains(forbidden), "frontend tracing includes forbidden request payload marker {forbidden}");
}
}
#[test]
fn pre_005_store_runtime_remains_backend_neutral_after_job_mapping_opens() {
let root = app_root();
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
assert!(bootstrap.contains("LogFilterLevel::Trace"));
assert!(!bootstrap.contains("HttpTransportPool"));
assert!(!bootstrap.contains("Store::open"));
let store = read_text(root.join("src/store_runtime.rs").as_path());
assert!(store.contains("ksp_store_lib::Store::open"));
assert!(store.contains("store.health().await"));
assert!(store.contains("store.close().await"));
for forbidden in ["ksp_job_", "BackfillRequest", "connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
assert!(!store.contains(forbidden), "Store runtime leaks or absorbs another responsibility: {forbidden}");
}
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("BackfillDeskOptionsDto"));
for forbidden in ["endpoint_url", "pub(crate) url", "pub(crate) credential", "pub(crate) token", "connection_uri"] {
assert!(!dto.contains(forbidden), "readiness options DTO source contains forbidden field marker {forbidden}");
}
}
#[test]
fn pre_006_frontend_receives_only_safe_http_route_metadata_and_no_endpoint_material() {
let root = app_root();
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("BackfillHttpRouteOptionDto"));
assert!(dto.contains("pub(crate) providers"));
assert!(dto.contains("pub(crate) role"));
for forbidden in ["endpoint_url", "pub(crate) url", "pub(crate) credential", "pub(crate) api_key", "pub(crate) authorization"] {
assert!(!dto.contains(forbidden), "route DTO leaks forbidden endpoint material marker {forbidden}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for forbidden in ["http://", "https://", "apiKey", "authorization", "token"] {
assert!(!frontend.contains(forbidden), "frontend embeds forbidden provider material marker {forbidden}");
}
}
#[test]
fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_owned() {
let root = app_root();
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
let request_source = struct_source(dto.as_str(), "pub(crate) struct BackfillStartRequestDto");
assert!(!request_source.is_empty());
for forbidden in [
"pub(crate) network:",
"pub(crate) provider",
"pub(crate) endpoint",
"pub(crate) url",
"pub(crate) job_id",
"pub(crate) credential",
"pub(crate) token",
] {
assert!(!request_source.contains(forbidden), "frontend request DTO owns forbidden field marker {forbidden}");
}
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
assert!(mapping.contains("options.store_network.as_deref()"));
assert!(mapping.contains("options.http_routes.iter().any"));
assert!(mapping.contains("BackfillRequest::new"));
assert!(!mapping.contains("HttpTransportPool::new"));
assert!(!mapping.contains("Store::open"));
let state = read_text(root.join("src/app_state.rs").as_path());
assert!(state.contains("backfill-desk-validation"));
assert!(state.contains("validated Backfill Desk campaign request without starting a Job"));
for forbidden in [
"preview.address",
"preview.anchor_signature",
"preview.explicit_signatures",
"request.address.as_str()",
"request.anchor_signature.as_str()",
] {
assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
}
}
#[test]
fn pre_008_start_surface_keeps_payload_and_physical_resources_backend_owned() {
let root = app_root();
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
let response_source = struct_source(dto.as_str(), "pub(crate) struct BackfillStartResponseDto");
assert!(!response_source.is_empty());
assert!(response_source.contains("pub(crate) job_id: String"));
assert!(response_source.contains("pub(crate) state: String"));
for forbidden in ["address", "signature", "provider", "endpoint", "url", "credential", "token", "network"] {
assert!(!response_source.contains(forbidden), "Start acknowledgement leaks forbidden marker {forbidden}");
}
let run = read_text(root.join("src/backfill_run.rs").as_path());
assert!(run.contains("BackfillJobHandle"));
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
assert!(!run.contains("reqwest"));
assert!(!run.contains("tokio_postgres"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
let start_marker = frontend.find("Backfill Desk campaign Start requested");
assert!(start_marker.is_some());
let start_source = match start_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 420, frontend.len())],
std::option::Option::None => "",
};
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
assert!(!start_source.contains(forbidden), "Start tracing includes forbidden request payload marker {forbidden}");
}
}
#[test]
fn pre_009_monitoring_projection_exposes_counters_and_codes_without_checkpoint_or_business_payloads() {
let root = app_root();
let status = read_text(root.join("src/backfill_status.rs").as_path());
let dto = struct_source(status.as_str(), "BackfillRunStatusDto");
for required in ["checkpoint_present", "contiguous_completed", "failure_code", "failure_domain", "sequence", "scope_kind"] {
assert!(dto.contains(required), "monitoring DTO missing required safe marker {required}");
}
for forbidden in [
"pub(crate) address:",
"pub(crate) signature:",
"pub(crate) endpoint:",
"pub(crate) provider:",
"pub(crate) credential:",
"pub(crate) token:",
"pub(crate) checkpoint:",
"pub(crate) payload:",
"pub(crate) raw_transaction:",
] {
assert!(!dto.contains(forbidden), "monitoring DTO leaks forbidden field marker {forbidden}");
}
}
#[test]
fn pre_010_cancel_surface_targets_backend_job_identity_without_business_payloads() {
let root = app_root();
let dto_source = read_text(root.join("src/dto_backfill.rs").as_path());
let dto = struct_source(dto_source.as_str(), "BackfillCancelResponseDto");
for required in ["accepted", "job_id", "state"] {
assert!(dto.contains(required), "Cancel acknowledgement missing safe field {required}");
}
for forbidden in [
"pub(crate) address:",
"pub(crate) signature:",
"pub(crate) endpoint:",
"pub(crate) provider:",
"pub(crate) credential:",
"pub(crate) token:",
"pub(crate) checkpoint:",
"pub(crate) payload:",
] {
assert!(!dto.contains(forbidden), "Cancel acknowledgement leaks forbidden field marker {forbidden}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
let cancel_marker = frontend.find("Backfill Desk cooperative cancellation requested");
assert!(cancel_marker.is_some());
let cancel_source = match cancel_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 700, frontend.len())],
std::option::Option::None => "",
};
assert!(cancel_source.contains("jobId: status.jobId"));
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
assert!(!cancel_source.contains(forbidden), "Cancel tracing includes forbidden campaign payload marker {forbidden}");
}
}
#[test]
fn pre_011_resume_surface_exposes_only_new_job_acknowledgement_and_never_checkpoint_payload() {
let root = app_root();
let dto_source = read_text(root.join("src/dto_backfill.rs").as_path());
let dto = struct_source(dto_source.as_str(), "BackfillResumeResponseDto");
for required in ["accepted", "job_id", "state"] {
assert!(dto.contains(required), "Resume acknowledgement missing safe field {required}");
}
for forbidden in [
"pub(crate) address:",
"pub(crate) signature:",
"pub(crate) endpoint:",
"pub(crate) provider:",
"pub(crate) credential:",
"pub(crate) token:",
"pub(crate) checkpoint:",
"pub(crate) payload:",
"pub(crate) network:",
] {
assert!(!dto.contains(forbidden), "Resume acknowledgement leaks forbidden field marker {forbidden}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
let resume_marker = tauri.find("fn backfill_resume");
assert!(resume_marker.is_some());
let resume_source = match resume_marker {
std::option::Option::Some(index) => &tauri[index..std::cmp::min(index + 1300, tauri.len())],
std::option::Option::None => "",
};
for forbidden in ["BackfillCheckpoint", "checkpoint:", "address:", "signature:", "provider:", "endpoint:"] {
assert!(!resume_source.contains(forbidden), "Resume IPC command leaks forbidden marker {forbidden}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
let resume_marker = frontend.find("Backfill Desk in-session Resume requested");
assert!(resume_marker.is_some());
let resume_source = match resume_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 1100, frontend.len())],
std::option::Option::None => "",
};
assert!(resume_source.contains("backfill_resume"));
for forbidden in ["checkpoint:", "address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
assert!(!resume_source.contains(forbidden), "Resume frontend sends or logs forbidden payload marker {forbidden}");
}
}
#[test]
fn pre_012_program_id_autocomplete_exposes_only_public_registry_metadata_and_no_browser_storage() {
let root = app_root();
let dto_source = read_text(root.join("src/dto_common.rs").as_path());
let dto = struct_source(dto_source.as_str(), "ProgramIdAutocompleteOptionDto");
for required in ["code", "domain", "family", "name", "program_id", "protocol"] {
assert!(dto.contains(required), "Program ID autocomplete DTO missing safe registry field {required}");
}
for forbidden in ["url", "credential", "token", "secret", "endpoint", "connection", "private_key"] {
assert!(!dto.contains(forbidden), "Program ID autocomplete DTO leaks forbidden marker {forbidden}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for forbidden in ["localStorage", "sessionStorage", "indexedDB", "document.cookie"] {
assert!(!frontend.contains(forbidden), "frontend persists campaign/autocomplete material via {forbidden}");
}
let selection_marker = frontend.find("Backfill Desk address autocomplete selection evaluated");
assert!(selection_marker.is_some());
let selection_source = match selection_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 320, frontend.len())],
std::option::Option::None => "",
};
assert!(selection_source.contains("programCode"));
assert!(selection_source.contains("registryMatch"));
assert!(!selection_source.contains("address.value"));
}
#[test]
fn pre_013_final_frontend_and_ipc_surface_keeps_physical_and_checkpoint_material_backend_owned() {
let root = app_root();
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for forbidden in ["http://", "https://", "localStorage", "sessionStorage", "indexedDB", "document.cookie", "privateKey", "secretKey"] {
assert!(!frontend.contains(forbidden), "final frontend contains forbidden marker {forbidden}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
for forbidden in ["BackfillCheckpoint", "HttpTransportPool", "ksp_store_postgres_lib", "tokio_postgres", "reqwest", "tonic"] {
assert!(!tauri.contains(forbidden), "Tauri IPC shell owns forbidden physical/runtime marker {forbidden}");
}
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
for marker in ["BackfillStartResponseDto", "BackfillCancelResponseDto", "BackfillResumeResponseDto"] {
let source = struct_source(dto.as_str(), marker);
assert!(!source.is_empty(), "missing DTO {marker}");
for forbidden in [
"pub(crate) checkpoint:",
"pub(crate) endpoint:",
"pub(crate) provider:",
"pub(crate) credential:",
"pub(crate) token:",
"pub(crate) payload:",
] {
assert!(!source.contains(forbidden), "DTO {marker} leaks forbidden field {forbidden}");
}
}
}

View File

@@ -0,0 +1,14 @@
// file: crates/ksp-app-backfill-desk/tests/public_api.rs
// version: 1
//! Public API checks for the KSP Backfill desktop application library.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#[test]
fn run_entry_point_is_available_from_crate_root() {
let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_backfill_desk_lib::run;
assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of::<fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()>>());
}

View File

@@ -0,0 +1,121 @@
// file: crates/ksp-app-backfill-desk/tests/release_completeness.rs
// version: 1
//! Final V1 completeness canaries for the KSP Backfill desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn pre_013_production_module_inventory_is_exact() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
let modules = lib
.lines()
.filter_map(|line| {
let line = line.trim();
if !line.starts_with("mod ") || !line.ends_with(';') {
return std::option::Option::None;
}
return std::option::Option::Some(line.trim_start_matches("mod ").trim_end_matches(';'));
})
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
modules,
std::collections::BTreeSet::from([
"app_state",
"backfill_request",
"backfill_run",
"backfill_status",
"bootstrap",
"constants",
"dto_backfill",
"dto_common",
"errors",
"frontend_logging",
"logging_runtime",
"splash",
"store_runtime",
"tauri",
"transport_runtime",
"tw_main",
"tw_splash",
])
);
}
#[test]
fn pre_013_integration_suite_inventory_closes_v1_contracts() {
let tests_root = app_root().join("tests");
let entries = std::fs::read_dir(tests_root.as_path());
assert!(entries.is_ok(), "Backfill Desk tests directory should be readable");
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let files = entries
.flatten()
.filter_map(|entry| {
let path = entry.path();
if path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") {
return std::option::Option::None;
}
return path.file_name().and_then(std::ffi::OsStr::to_str).map(str::to_owned);
})
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
files,
std::collections::BTreeSet::from([
"config_composition.rs".to_owned(),
"dependency_boundary.rs".to_owned(),
"desktop_contract.rs".to_owned(),
"desktop_security.rs".to_owned(),
"public_api.rs".to_owned(),
"release_completeness.rs".to_owned(),
])
);
}
#[test]
fn pre_013_final_surface_contains_every_planned_v1_capability_without_deferred_extensions() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in ["backfill_start", "backfill_status", "backfill_cancel", "backfill_resume"] {
assert!(tauri.contains(required), "missing final V1 capability marker {required}");
}
let options = read_text(app_root().join("src/dto_common.rs").as_path());
assert!(options.contains("program_id_options"));
assert!(options.contains("program_id_autocomplete_options"));
let status = read_text(app_root().join("src/backfill_status.rs").as_path());
for required in ["checkpoint_present", "contiguous_completed", "failure_code", "failure_domain"] {
assert!(status.contains(required), "missing final monitoring marker {required}");
}
for forbidden in ["Grpc", "Yellowstone", "WebSocketBackfill", "scheduler", "job_history"] {
assert!(!tauri.contains(forbidden), "deferred capability leaked into V1 Tauri surface via {forbidden}");
}
}
#[test]
fn pre_013_shared_autocomplete_dto_remains_exported_and_consumed_through_crate_root() {
let root = app_root();
let lib = read_text(root.join("src/lib.rs").as_path());
assert!(lib.contains("pub(crate) use self::dto_common::ProgramIdAutocompleteOptionDto;"));
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("pub(crate) struct ProgramIdAutocompleteOptionDto"));
assert!(dto.contains("std::vec::Vec<crate::ProgramIdAutocompleteOptionDto>"));
assert!(dto.contains("crate::ProgramIdAutocompleteOptionDto {"));
let bare_construction = dto.matches("\n ProgramIdAutocompleteOptionDto {").count();
assert_eq!(bare_construction, 0, "shared autocomplete DTO must not bypass the crate-root facade");
}

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,199 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
// version: 3
fn ready_options() -> crate::BackfillDeskOptionsDto {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => crate::BackfillRequestLimitsDto {
default_hydration_concurrency: 1,
default_max_candidates: 1,
default_max_pages: 1,
default_page_size: 1,
max_hydration_concurrency: 1,
max_candidates: 1,
max_pages: 1,
max_page_size: 1,
max_signature_text_bytes: 88,
min_signature_text_bytes: 64,
},
};
return crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: true,
configured_networks: vec!["mainnet-beta".to_owned()],
http_routes: vec![
crate::BackfillHttpRouteOptionDto {
pooled: true,
providers: vec!["publicnode".to_owned(), "solana-public".to_owned()],
role: "backfill_pool".to_owned(),
},
crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["publicnode".to_owned()], role: "backfill_publicnode".to_owned() },
],
limits,
network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("mainnet-beta".to_owned()),
store_ready: true,
transport_diagnostic: std::option::Option::None,
transport_ready: true,
};
}
fn request(scope_kind: &str) -> crate::BackfillStartRequestDto {
return crate::BackfillStartRequestDto {
address: std::option::Option::Some("11111111111111111111111111111111".to_owned()),
anchor_signature: std::option::Option::None,
commitment: "finalized".to_owned(),
explicit_signatures: std::vec::Vec::new(),
hydration_concurrency: 4,
http_role: "backfill_pool".to_owned(),
max_candidates: 100,
max_pages: 10,
min_context_slot: std::option::Option::Some("123456".to_owned()),
page_size: 100,
scope_kind: scope_kind.to_owned(),
};
}
fn job_id() -> std::option::Option<ksp_job_api::JobId> {
let job_id = ksp_job_api::JobId::new("request-mapping-test");
assert!(job_id.is_ok());
return match job_id {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
#[test]
fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(mapped.network().as_str(), "mainnet-beta");
assert_eq!(mapped.role().as_str(), "backfill_pool");
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
assert_eq!(mapped.page_size(), 100);
assert_eq!(mapped.max_pages(), 10);
assert_eq!(mapped.max_candidates(), 100);
assert_eq!(mapped.hydration_concurrency(), 4);
assert_eq!(mapped.min_context_slot(), std::option::Option::Some(123456));
}
#[test]
fn before_and_after_scopes_require_an_exact_anchor_shape() {
for scope_kind in ["before_address", "after_address"] {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut input = request(scope_kind);
input.anchor_signature = std::option::Option::Some("1".repeat(64));
let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
assert!(mapped.is_ok(), "scope {scope_kind} should map with a bounded anchor");
}
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let missing_anchor = crate::map_backfill_request(request("before_address"), &ready_options(), job_id);
assert!(missing_anchor.is_err());
}
#[test]
fn explicit_signatures_reject_address_context_and_deduplicate_in_job_contract() {
let request_job_id = job_id();
let request_job_id = match request_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut input = request("explicit_signatures");
input.address = std::option::Option::None;
input.min_context_slot = std::option::Option::None;
input.explicit_signatures = vec!["1".repeat(64), "1".repeat(64), "2".repeat(64)];
let mapped = crate::map_backfill_request(input, &ready_options(), request_job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let signatures = mapped.scope().signatures();
assert!(signatures.is_some());
if let std::option::Option::Some(signatures) = signatures {
assert_eq!(signatures.len(), 2);
}
let mut hostile = request("explicit_signatures");
hostile.address = std::option::Option::None;
hostile.min_context_slot = std::option::Option::None;
hostile.explicit_signatures = vec![" 1".to_owned()];
let hostile_job_id = job_id();
let hostile_job_id = match hostile_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(crate::map_backfill_request(hostile, &ready_options(), hostile_job_id).is_err());
}
#[test]
fn route_inventory_and_composition_are_revalidated_before_mapping() {
let first_job_id = job_id();
let first_job_id = match first_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut unknown_route = request("latest_address");
unknown_route.http_role = "provider-from-frontend".to_owned();
let rejected = crate::map_backfill_request(unknown_route, &ready_options(), first_job_id);
assert!(rejected.is_err());
let mut unavailable = ready_options();
unavailable.composition_ready = false;
let second_job_id = job_id();
let second_job_id = match second_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(crate::map_backfill_request(request("latest_address"), &unavailable, second_job_id).is_err());
}
#[test]
fn request_preview_never_contains_address_or_signature_values() {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let preview = crate::project_backfill_request(&mapped);
assert!(preview.is_ok());
let preview = match preview {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let serialized = serde_json::to_string(&preview);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(!serialized.contains("11111111111111111111111111111111"));
assert!(!serialized.contains(&"1".repeat(64)));
assert!(serialized.contains("mainnet-beta"));
assert!(serialized.contains("backfill_pool"));
}
}

View File

@@ -0,0 +1,117 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
// version: 3
#[test]
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
let state = crate::BackfillRunState::new();
let first = state.next_job_id();
let second = state.next_job_id();
assert!(first.is_ok());
assert!(second.is_ok());
let first = match first {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let second = match second {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_ne!(first, second);
assert!(first.as_str().starts_with("backfill-desk-"));
assert!(second.as_str().starts_with("backfill-desk-"));
assert!(first.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
}
fn cancellable_runtime(
job_id: &str,
) -> std::option::Option<(ksp_job_api::JobId, ksp_job_backfill_lib::BackfillRequest, ksp_job_backfill_lib::BackfillJobRuntime)> {
let job_id = match ksp_job_api::JobId::new(job_id) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let request = ksp_job_backfill_lib::BackfillRequest::new(
job_id.clone(),
network,
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
ksp_job_backfill_lib::BackfillCommitment::Finalized,
ksp_job_backfill_lib::BackfillScope::latest_address(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])),
1,
1,
1,
1,
std::option::Option::None,
);
let request = match request {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let retained_request = request.clone();
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
return std::option::Option::Some((job_id, retained_request, runtime));
}
#[test]
fn targeted_cancel_is_idempotent_and_rejects_stale_job_identity() {
let state = crate::BackfillRunState::new();
let runtime = cancellable_runtime("backfill-desk-41");
assert!(runtime.is_some());
let (job_id, request, runtime) = match runtime {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let installed = state.install(job_id.clone(), runtime.handle(), request);
assert!(installed.is_ok());
let first = state.cancel(job_id.as_str());
assert!(first.is_ok());
if let std::result::Result::Ok(first) = first {
assert!(first.accepted);
assert_eq!(first.state, "cancelling");
}
let repeated = state.cancel(job_id.as_str());
assert!(repeated.is_ok());
if let std::result::Result::Ok(repeated) = repeated {
assert!(!repeated.accepted);
assert_eq!(repeated.state, "cancelling");
}
let stale = state.cancel("backfill-desk-40");
assert!(stale.is_err());
if let std::result::Result::Err(error) = stale {
assert_eq!(error.code().domain(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.domain());
assert_eq!(error.code().code(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.code());
}
}
#[test]
fn resume_requires_terminal_checkpoint_and_rejects_active_run() {
let state = crate::BackfillRunState::new();
let next = match ksp_job_api::JobId::new("backfill-desk-50") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let unavailable = state.resume_request(next.clone());
assert!(unavailable.is_err());
if let std::result::Result::Err(error) = unavailable {
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE);
}
let runtime = cancellable_runtime("backfill-desk-49");
assert!(runtime.is_some());
let (job_id, request, runtime) = match runtime {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let installed = state.install(job_id, runtime.handle(), request);
assert!(installed.is_ok());
let active = state.resume_request(next);
assert!(active.is_err());
if let std::result::Result::Err(error) = active {
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RUN_ACTIVE);
}
}

View File

@@ -0,0 +1,76 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
// version: 1
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let scope = match ksp_job_backfill_lib::BackfillScope::explicit_signatures(vec![signature]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let job_id = match ksp_job_api::JobId::new("backfill-desk-monitor-test") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let request = ksp_job_backfill_lib::BackfillRequest::new(
job_id,
network,
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
ksp_job_backfill_lib::BackfillCommitment::Confirmed,
scope,
100,
10,
1,
1,
std::option::Option::None,
);
let request = match request {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let source = runtime.handle().snapshots();
return std::option::Option::Some(<ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source));
}
#[test]
fn latest_value_projection_is_safe_complete_and_uses_decimal_sequence_text() {
let notification = match initial_notification() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let status = crate::project_backfill_status(&notification);
assert!(status.is_ok());
let status = match status {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(status.active);
assert!(!status.terminal);
assert_eq!(status.job_id, "backfill-desk-monitor-test");
assert_eq!(status.job_kind, ksp_job_backfill_lib::BACKFILL_JOB_KIND_CODE);
assert_eq!(status.sequence, "0");
assert_eq!(status.state, "created");
assert_eq!(status.phase, "created");
assert_eq!(status.scope_kind, "explicit_signatures");
assert!(!status.checkpoint_present);
assert_eq!(status.candidates_selected, 0);
assert_eq!(status.failure_code, std::option::Option::None);
assert_eq!(status.failure_domain, std::option::Option::None);
let serialized = serde_json::to_string(&status);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
for forbidden in ["address", "signature\"", "endpoint", "credential", "token", "postgres"] {
assert!(!serialized.contains(forbidden));
}
}
}

View File

@@ -0,0 +1,75 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
// version: 4
#[test]
fn request_limits_are_derived_from_job_owned_public_bounds() {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(usize::try_from(limits.max_page_size), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE));
assert_eq!(usize::try_from(limits.max_pages), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGES));
assert_eq!(usize::try_from(limits.max_candidates), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES));
assert_eq!(usize::try_from(limits.max_hydration_concurrency), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY));
assert_eq!(usize::try_from(limits.min_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES));
assert_eq!(usize::try_from(limits.max_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES));
assert!(limits.default_page_size <= limits.max_page_size);
assert!(limits.default_max_pages <= limits.max_pages);
assert!(limits.default_max_candidates <= limits.max_candidates);
assert!(limits.default_hydration_concurrency <= limits.max_hydration_concurrency);
}
#[test]
fn request_options_expose_exact_current_commitments_and_http_scopes() {
assert_eq!(crate::backfill_commitment_codes(), vec!["finalized".to_owned(), "confirmed".to_owned()]);
assert_eq!(
crate::backfill_scope_kind_codes(),
vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
);
}
#[test]
fn start_acknowledgement_contains_only_backend_job_identity_and_initial_state() {
let response = crate::BackfillStartResponseDto { job_id: "backfill-desk-1".to_owned(), state: "created".to_owned() };
let serialized = serde_json::to_string(&response);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(serialized.contains("backfill-desk-1"));
assert!(serialized.contains("created"));
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token"] {
assert!(!serialized.contains(forbidden));
}
}
}
#[test]
fn cancel_acknowledgement_contains_only_backend_job_identity_acceptance_and_state() {
let response = crate::BackfillCancelResponseDto { accepted: true, job_id: "backfill-desk-1".to_owned(), state: "cancelling".to_owned() };
let serialized = serde_json::to_string(&response);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(serialized.contains("backfill-desk-1"));
assert!(serialized.contains("cancelling"));
assert!(serialized.contains("accepted"));
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token", "checkpoint", "payload"] {
assert!(!serialized.contains(forbidden));
}
}
}
#[test]
fn resume_acknowledgement_contains_only_new_backend_job_identity_acceptance_and_state() {
let response = crate::BackfillResumeResponseDto { accepted: true, job_id: "backfill-desk-2".to_owned(), state: "created".to_owned() };
let serialized = serde_json::to_string(&response);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(serialized.contains("backfill-desk-2"));
assert!(serialized.contains("created"));
assert!(serialized.contains("accepted"));
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token", "checkpoint", "payload"] {
assert!(!serialized.contains(forbidden));
}
}
}

View File

@@ -0,0 +1,67 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
// version: 6
#[test]
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
let source = std::io::Error::other("secret-source");
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failed"), "safe message")
.with_context("secret", "hidden")
.with_source(source);
let dto = crate::CommandErrorDto::from_error(&error);
assert_eq!(dto.domain, "test");
assert_eq!(dto.code, "failed");
assert_eq!(dto.message, "safe message");
}
#[test]
fn transport_options_projection_contains_only_safe_transport_metadata() {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let dto = crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
http_routes: vec![crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["solana-public".to_owned()], role: "default".to_owned() }],
limits,
composition_ready: true,
configured_networks: vec!["devnet".to_owned()],
network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("devnet".to_owned()),
store_ready: true,
transport_diagnostic: std::option::Option::None,
transport_ready: true,
};
let value = serde_json::to_value(dto);
assert!(value.is_ok());
if let std::result::Result::Ok(value) = value {
let serialized = value.to_string();
assert!(serialized.contains("default"));
assert!(serialized.contains("devnet"));
assert!(serialized.contains("solana-public"));
assert!(!serialized.contains("url"));
assert!(!serialized.contains("endpoint"));
assert!(!serialized.contains("postgres"));
assert!(!serialized.contains("connection"));
assert!(!serialized.contains("database"));
}
}
#[test]
fn program_id_autocomplete_dataset_is_derived_exactly_from_core_registry() {
let options = crate::program_id_autocomplete_options();
let entries = ksp_core_lib::entries();
assert_eq!(options.len(), entries.len());
for (option, entry) in options.iter().zip(entries.iter()) {
assert_eq!(option.code, entry.code());
assert_eq!(option.name, entry.name());
assert_eq!(option.program_id, entry.program_id());
assert_eq!(option.domain, entry.domain());
assert_eq!(option.family, entry.family());
assert_eq!(option.protocol, entry.protocol());
}
}

View File

@@ -0,0 +1,40 @@
// file: crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs
// version: 1
#[test]
fn frontend_log_levels_are_explicit_and_case_insensitive() {
assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
}
#[test]
fn unsupported_frontend_log_level_is_rejected() {
let result = super::parse_level("notice");
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
}
#[test]
fn frontend_log_targets_are_whitelisted() {
assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
}
#[test]
fn arbitrary_frontend_log_target_is_rejected() {
let result = super::parse_target("ksp-app-backfill-desk.test.logging");
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
}

View File

@@ -0,0 +1,36 @@
// file: crates/ksp-app-backfill-desk/unit_tests/splash.rs
// version: 1
#[test]
fn fallback_splash_settings_are_short_and_ordered() {
let settings = crate::SplashSettings::fallback();
assert_eq!(settings.fade_in_ms(), 300);
assert_eq!(settings.minimum_ms(), 1200);
assert_eq!(settings.fade_out_ms(), 300);
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
assert_eq!(settings.fade_in_source(), "fallback");
assert_eq!(settings.minimum_source(), "fallback");
assert_eq!(settings.fade_out_source(), "fallback");
}
#[test]
fn splash_order_contract_keeps_action_message_status_and_duration() {
let order =
crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
assert_eq!(order.action, "add_message");
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
assert_eq!(order.duration_ms, std::option::Option::Some(300));
}
#[test]
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
assert!(malformed.is_err());
assert!(oversized_minimum.is_err());
assert!(oversized_fade_in.is_err());
assert!(oversized_fade_out.is_err());
}

View File

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

View File

@@ -0,0 +1,133 @@
// file: crates/ksp-app-backfill-desk/unit_tests/transport_runtime.rs
// version: 2
fn endpoint(
name: &str,
cluster: &str,
role_name: &str,
request_kinds: std::vec::Vec<ksp_onchain_transport_lib::HttpRequestKind>,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpEndpointSettings> {
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(format!("https://{name}.example.invalid"));
let url = match url {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
ksp_onchain_transport_lib::HttpRoleName::new(role_name),
true,
request_kinds,
0,
ksp_onchain_transport_lib::HttpRoleLimits::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
),
);
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpEndpointSettings::new(
name,
true,
ksp_onchain_transport_lib::HttpProviderName::new("fixture"),
ksp_onchain_transport_lib::HttpClusterName::new(cluster),
url,
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(2),
std::option::Option::Some(1),
vec![role],
));
}
fn pool(endpoints: std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointSettings>) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
endpoints,
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(10), std::time::Duration::from_millis(10)),
);
return ksp_onchain_transport_lib::HttpTransportPool::new(settings);
}
#[test]
fn http_routes_require_both_backfill_rpc_methods() {
let signatures = ksp_onchain_transport_lib::find_http_rpc_method("getSignaturesForAddress");
let transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction");
assert!(signatures.is_some());
assert!(transaction.is_some());
let signatures = match signatures {
std::option::Option::Some(value) => ksp_onchain_transport_lib::HttpRequestKind::new(value.request_kind()),
std::option::Option::None => return,
};
let transaction = match transaction {
std::option::Option::Some(value) => ksp_onchain_transport_lib::HttpRequestKind::new(value.request_kind()),
std::option::Option::None => return,
};
let complete = endpoint("complete", "devnet", "complete", vec![signatures.clone(), transaction]);
let complete = match complete {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let partial = endpoint("partial", "devnet", "partial", vec![signatures]);
let partial = match partial {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let runtime_pool = pool(vec![complete, partial]);
assert!(runtime_pool.is_ok());
let runtime_pool = match runtime_pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let snapshot = runtime_pool.snapshot();
let roles = super::compatible_backfill_http_routes(&runtime_pool, &snapshot);
assert!(roles.is_ok());
if let std::result::Result::Ok(roles) = roles {
assert_eq!(roles.len(), 1);
if let std::option::Option::Some(route) = roles.first() {
assert_eq!(route.role, "complete");
assert_eq!(route.providers, vec!["fixture".to_owned()]);
assert!(!route.pooled);
}
}
}
#[test]
fn configured_networks_are_safe_sorted_and_deduplicated() {
let one = endpoint("one", "testnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let two = endpoint("two", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let three = endpoint("three", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let (one, two, three) = match (one, two, three) {
(std::result::Result::Ok(one), std::result::Result::Ok(two), std::result::Result::Ok(three)) => (one, two, three),
_ => return,
};
let runtime_pool = pool(vec![one, two, three]);
assert!(runtime_pool.is_ok());
if let std::result::Result::Ok(runtime_pool) = runtime_pool {
let values = super::configured_networks(&runtime_pool.snapshot());
assert_eq!(values, vec!["devnet".to_owned(), "testnet".to_owned()]);
}
}
#[test]
fn coherent_network_requires_exactly_one_enabled_http_cluster() {
let one = endpoint("one", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let one = match one {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let one_pool = pool(vec![one]);
assert!(one_pool.is_ok());
if let std::result::Result::Ok(one_pool) = one_pool {
let runtime = crate::TransportRuntime { pool: one_pool, profile_id: "fixture".to_owned() };
assert_eq!(runtime.coherent_network(), std::option::Option::Some("devnet".to_owned()));
}
let two = endpoint("two", "devnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let three = endpoint("three", "testnet", "default", vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()]);
let (two, three) = match (two, three) {
(std::result::Result::Ok(two), std::result::Result::Ok(three)) => (two, three),
_ => return,
};
let multi_pool = pool(vec![two, three]);
assert!(multi_pool.is_ok());
if let std::result::Result::Ok(multi_pool) = multi_pool {
let runtime = crate::TransportRuntime { pool: multi_pool, profile_id: "fixture".to_owned() };
assert_eq!(runtime.coherent_network(), std::option::Option::None);
}
}

View File

@@ -0,0 +1,72 @@
// file: crates/ksp-app-backfill-desk/vite.config.ts
// version: 1
import { NodePackageImporter } from "sass-embedded";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";
import { defineConfig, normalizePath } from "vite";
const appRoot = fileURLToPath(new URL(".", import.meta.url));
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist"));
const devHost = process.env.TAURI_DEV_HOST;
export default defineConfig({
clearScreen: false,
root: frontendRoot,
publicDir: false,
input: {
main: normalizePath(resolve(frontendRoot, "main.html")),
splash: normalizePath(resolve(frontendRoot, "splash.html")),
},
build: {
outDir: frontendDist,
emptyOutDir: true,
minify: true,
sourcemap: false,
cssCodeSplit: true,
rolldownOptions: {
output: {
entryFileNames: "js/[name]-[hash].js",
chunkFileNames: "js/chunks/[name]-[hash].js",
assetFileNames: assetInfo => {
const originalName = assetInfo.names[0] ?? "";
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
if (extension === "css") {
return "css/[name]-[hash][extname]";
}
if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
return "fonts/[name]-[hash][extname]";
}
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
return "imgs/[name][extname]";
}
return "otherassets/[name][extname]";
},
},
},
},
css: {
preprocessorOptions: {
scss: {
quietDeps: true,
silenceDeprecations: ["import", "color-functions", "global-builtin"],
verbose: false,
importers: [new NodePackageImporter()],
},
},
},
server: {
port: 1436,
strictPort: true,
host: devHost || false,
ws: {
protocol: "ws",
host: devHost || "localhost",
port: 1437,
},
watch: {
ignored: ["**/src/**"],
},
},
});

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/splash.html --> <!-- file: crates/ksp-app-config-desk/frontend/splash.html -->
<!-- version: 3 --> <!-- version: 4 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -14,7 +14,7 @@
<body> <body>
<div id="splash-container" style="opacity: 0;"> <div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Config Desk"> <img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Config Desk">
<div id="app-name">Config Desk</div> <div id="app-name">Config</div>
<div id="debug-info" aria-live="off" hidden></div> <div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div> <div id="messages-container" aria-live="polite"></div>
</div> </div>

View File

@@ -52,6 +52,7 @@
"icons/favicon.ico" "icons/favicon.ico"
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json", "../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json", "../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json", "../../config/std.logging.json": "config/std.logging.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: 11 // version: 12
//! Desktop build/shell contract audits for Config Desk. //! Desktop build/shell contract audits for Config Desk.
@@ -110,7 +110,11 @@ 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(), 13); assert_eq!(resources.len(), 14);
assert_eq!(
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"),
);
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"),

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-solprices-desk/frontend/splash.html --> <!-- file: crates/ksp-app-solprices-desk/frontend/splash.html -->
<!-- version: 2 --> <!-- version: 3 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -14,7 +14,7 @@
<body> <body>
<div id="splash-container" style="opacity: 0;"> <div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP SOL Prices Desk"> <img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP SOL Prices Desk">
<div id="app-name">SOL Prices Desk</div> <div id="app-name">SOL Prices</div>
<div id="debug-info" aria-live="off" hidden></div> <div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div> <div id="messages-container" aria-live="polite"></div>
</div> </div>

View File

@@ -52,6 +52,7 @@
"icons/favicon.ico" "icons/favicon.ico"
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json", "../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json", "../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json", "../../config/std.logging.json": "config/std.logging.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: 10 // version: 11
//! 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,13 +85,17 @@ fn pre_002_package_is_mixed_lib_bin_and_frontend_is_scaffold_only() {
} }
#[test] #[test]
fn pre_004_packaging_contains_current_thirteen_config_resources() { fn pre_004_packaging_contains_current_fourteen_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(), 13); assert_eq!(resources.len(), 14);
assert_eq!(
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"),
);
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"),

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-wallet-desk/frontend/splash.html --> <!-- file: crates/ksp-app-wallet-desk/frontend/splash.html -->
<!-- version: 1 --> <!-- version: 2 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -13,7 +13,7 @@
<body> <body>
<div id="splash-container" style="opacity: 0;"> <div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Wallet Desk"> <img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Wallet Desk">
<div id="app-name">Wallet Desk</div> <div id="app-name">Wallet</div>
<div id="debug-info" aria-live="off" hidden></div> <div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div> <div id="messages-container" aria-live="polite"></div>
</div> </div>

View File

@@ -52,6 +52,7 @@
"icons/favicon.ico" "icons/favicon.ico"
], ],
"resources": { "resources": {
"../../config/composite.ksp-app-backfill-desk.json": "config/composite.ksp-app-backfill-desk.json",
"../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json", "../../config/composite.ksp-app-solprices-desk.json": "config/composite.ksp-app-solprices-desk.json",
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json", "../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
"../../config/std.logging.json": "config/std.logging.json", "../../config/std.logging.json": "config/std.logging.json",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 31 // version: 32
//! Desktop build, shell and Config-status contract audits for Wallet Desk. //! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -432,7 +432,11 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
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 Wallet Desk Config resources map must exist"); assert!(resources.is_some(), "packaged Wallet Desk 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(), 13); assert_eq!(resources.len(), 14);
assert_eq!(
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"),
);
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"),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs // file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
// version: 10 // version: 11
//! Release-wide deterministic compliance canaries for Wallet Desk. //! Release-wide deterministic compliance canaries for Wallet Desk.
@@ -199,7 +199,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
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(), 13); assert_eq!(resources.len(), 14);
for (source, destination) in resources { for (source, destination) in resources {
let destination = destination.as_str(); let destination = destination.as_str();
assert!(destination.is_some(), "resource destination must be textual"); assert!(destination.is_some(), "resource destination must be textual");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 21 // version: 22
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -151,6 +151,8 @@ pub use self::registry::ConfigFileId;
pub use self::registry::ConfigFileKind; pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames. /// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry; pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the Backfill Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME;
/// Default physical filename for the SOL Prices Desk composite configuration document. /// Default physical filename for the SOL Prices Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME; pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME;
/// Default physical filename for the Wallet Desk composite configuration document. /// Default physical filename for the Wallet Desk composite configuration document.
@@ -177,6 +179,8 @@ pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_WALLET_FILENAME; pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
/// Default physical filename for the standard Wallet JSON Schema document. /// Default physical filename for the standard Wallet JSON Schema document.
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
/// Logical file identifier for the Backfill Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK;
/// Logical file identifier for the SOL Prices Desk composite configuration document. /// Logical file identifier for the SOL Prices Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK; pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK;
/// Logical file identifier for the Wallet Desk composite configuration document. /// Logical file identifier for the Wallet Desk composite configuration document.

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 13 // version: 14
/// Bootstrap argument used to replace a known Config filename mapping. /// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap"; pub const ARG_FILE_MAP: &str = "--filemap";
/// Default physical filename for the Backfill Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME: &str = "composite.ksp-app-backfill-desk.json";
/// Default physical filename for the SOL Prices Desk composite configuration document. /// Default physical filename for the SOL Prices Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME: &str = "composite.ksp-app-solprices-desk.json"; pub const DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME: &str = "composite.ksp-app-solprices-desk.json";
/// Default physical filename for the Wallet Desk composite configuration document. /// Default physical filename for the Wallet Desk composite configuration document.
@@ -29,6 +31,8 @@ pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.js
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json"; pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
/// Default physical filename for the standard Wallet JSON Schema document. /// Default physical filename for the standard Wallet JSON Schema document.
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json"; pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
/// Logical file identifier for the Backfill Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK: &str = "cfg.composite.ksp-app-backfill-desk";
/// Logical file identifier for the SOL Prices Desk composite configuration document. /// Logical file identifier for the SOL Prices Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK: &str = "cfg.composite.ksp-app-solprices-desk"; pub const FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK: &str = "cfg.composite.ksp-app-solprices-desk";
/// Logical file identifier for the Wallet Desk composite configuration document. /// Logical file identifier for the Wallet Desk composite configuration document.
@@ -160,6 +164,16 @@ pub struct ConfigFileRegistry {
impl ConfigFileRegistry { impl ConfigFileRegistry {
/// Creates the registry containing the KSP default file mappings known in the current release. /// Creates the registry containing the KSP default file mappings known in the current release.
pub fn defaults() -> ksp_core_lib::Result<Self> { pub fn defaults() -> ksp_core_lib::Result<Self> {
let backfill_composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK,
ConfigFileKind::Config,
DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
);
let backfill_composite = match backfill_composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let solprices_composite = ConfigFileDescriptor::new( let solprices_composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK,
ConfigFileKind::Config, ConfigFileKind::Config,
@@ -271,6 +285,7 @@ impl ConfigFileRegistry {
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return crate::build_registry([ return crate::build_registry([
backfill_composite,
solprices_composite, solprices_composite,
wallet_composite, wallet_composite,
composite_schema, composite_schema,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs // file: crates/ksp-config-lib/tests/public_api.rs
// version: 26 // version: 27
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport/Store adapters and management contracts. //! Logging/Transport/Store adapters and management contracts.
@@ -80,21 +80,22 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}"); assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 13); assert_eq!(descriptors.len(), 14);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK); assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK); assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING); assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT); assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE); assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_STD_STORE);
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET); assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT); assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE); assert_eq!(descriptors[10].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(descriptors[11].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET); assert_eq!(descriptors[12].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[6].schema_file_id(); assert_eq!(descriptors[13].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
let schema_file_id = descriptors[7].schema_file_id();
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association"); assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id { if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET); assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
@@ -163,6 +164,8 @@ fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
assert_eq!(descriptor.kind(), ksp_config_lib::ConfigFileKind::Schema); assert_eq!(descriptor.kind(), ksp_config_lib::ConfigFileKind::Schema);
} }
} }
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK, "cfg.composite.ksp-app-backfill-desk");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME, "composite.ksp-app-backfill-desk.json");
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, "cfg.composite.ksp-app-solprices-desk"); assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK, "cfg.composite.ksp-app-solprices-desk");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME, "composite.ksp-app-solprices-desk.json"); assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME, "composite.ksp-app-solprices-desk.json");
assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit); assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);

View File

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "mainnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"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": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"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": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "secret_test"
},
{
"component_id": "store",
"file_id": "cfg.std.store",
"profile_id": "testnet"
}
]
}
]
}

View File

@@ -16,7 +16,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,
@@ -41,7 +41,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,
@@ -66,7 +66,7 @@
"recycle_timeout_ms": 5000 "recycle_timeout_ms": 5000
}, },
"tls": { "tls": {
"mode": "verify_full" "mode": "disabled"
}, },
"bootstrap": { "bootstrap": {
"schema_autocreate": true, "schema_autocreate": true,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs // file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 11 // version: 12
#[test] #[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
@@ -7,27 +7,27 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 13); assert_eq!(descriptors.len(), 14);
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK); assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME)); assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME));
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE)); assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK); assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME)); assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_LOGGING); assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT); assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_STORE); assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_STORE);
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_STD_WALLET); assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(descriptors[6].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME)); assert_eq!(descriptors[7].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(descriptors[6].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET)); assert_eq!(descriptors[7].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT); assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
assert_eq!(descriptors[10].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE); assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_STORE);
assert_eq!(descriptors[11].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[12].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET); assert_eq!(descriptors[13].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
assert!(descriptors[0..7].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config)); assert!(descriptors[0..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
assert!(descriptors[7..13].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema)); assert!(descriptors[8..14].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
} }
} }
@@ -362,27 +362,41 @@ fn absolute_fixture_path() -> std::path::PathBuf {
} }
#[test] #[test]
fn defaults_register_generic_composite_schema_and_both_runtime_composites() { fn defaults_register_generic_composite_schema_and_three_runtime_composites() {
let registry = crate::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
let backfill_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_BACKFILL_DESK);
let solprices_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK); let solprices_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_SOLPRICES_DESK);
let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK); let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
assert!(backfill_id.is_ok(), "Backfill Desk composite runtime file_id should be valid: {backfill_id:?}");
assert!(solprices_id.is_ok(), "SOL Prices Desk composite runtime file_id should be valid: {solprices_id:?}"); assert!(solprices_id.is_ok(), "SOL Prices Desk composite runtime file_id should be valid: {solprices_id:?}");
assert!(wallet_id.is_ok(), "Wallet Desk composite runtime file_id should be valid: {wallet_id:?}"); assert!(wallet_id.is_ok(), "Wallet Desk composite runtime file_id should be valid: {wallet_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id), std::result::Result::Ok(solprices_id), std::result::Result::Ok(wallet_id)) = if let (
(registry, schema_id, solprices_id, wallet_id) std::result::Result::Ok(registry),
std::result::Result::Ok(schema_id),
std::result::Result::Ok(backfill_id),
std::result::Result::Ok(solprices_id),
std::result::Result::Ok(wallet_id),
) = (registry, schema_id, backfill_id, solprices_id, wallet_id)
{ {
let schema = registry.descriptor(&schema_id); let schema = registry.descriptor(&schema_id);
let backfill = registry.descriptor(&backfill_id);
let solprices = registry.descriptor(&solprices_id); let solprices = registry.descriptor(&solprices_id);
let wallet = registry.descriptor(&wallet_id); let wallet = registry.descriptor(&wallet_id);
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}"); assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(backfill.is_ok(), "Backfill Desk runtime composite should be registered: {backfill:?}");
assert!(solprices.is_ok(), "SOL Prices Desk runtime composite should be registered: {solprices:?}"); assert!(solprices.is_ok(), "SOL Prices Desk runtime composite should be registered: {solprices:?}");
assert!(wallet.is_ok(), "Wallet Desk runtime composite should be registered: {wallet:?}"); assert!(wallet.is_ok(), "Wallet Desk runtime composite should be registered: {wallet:?}");
if let (std::result::Result::Ok(schema), std::result::Result::Ok(solprices), std::result::Result::Ok(wallet)) = (schema, solprices, wallet) { if let (std::result::Result::Ok(schema), std::result::Result::Ok(backfill), std::result::Result::Ok(solprices), std::result::Result::Ok(wallet)) =
(schema, backfill, solprices, wallet)
{
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
assert_eq!(backfill.kind(), crate::ConfigFileKind::Config);
assert_eq!(backfill.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_BACKFILL_DESK_FILENAME));
assert_eq!(backfill.schema_file_id(), std::option::Option::Some(&schema_id));
assert_eq!(solprices.kind(), crate::ConfigFileKind::Config); assert_eq!(solprices.kind(), crate::ConfigFileKind::Config);
assert_eq!(solprices.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME)); assert_eq!(solprices.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_SOLPRICES_DESK_FILENAME));
assert_eq!(solprices.schema_file_id(), std::option::Option::Some(&schema_id)); assert_eq!(solprices.schema_file_id(), std::option::Option::Some(&schema_id));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/store.rs // file: crates/ksp-config-lib/unit_tests/store.rs
// version: 3 // version: 4
#[test] #[test]
fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() { fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
@@ -30,7 +30,7 @@ fn committed_store_profile_maps_exact_runtime_settings_and_secret_fallback() {
assert_eq!(postgres.pool().wait_timeout(), std::time::Duration::from_millis(5_000)); assert_eq!(postgres.pool().wait_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(postgres.pool().create_timeout(), std::time::Duration::from_millis(10_000)); assert_eq!(postgres.pool().create_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000)); assert_eq!(postgres.pool().recycle_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::VerifyFull); assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::Disabled);
assert!(postgres.bootstrap().auto_migrate()); assert!(postgres.bootstrap().auto_migrate());
assert!(postgres.bootstrap().schema_autocreate()); assert!(postgres.bootstrap().schema_autocreate());
assert!(postgres.bootstrap().schema_autoupdate()); assert!(postgres.bootstrap().schema_autoupdate());
@@ -132,6 +132,14 @@ fn named_store_targets_select_one_network_and_database_without_runtime_multiplex
assert_eq!(resolved.profile_id(), target_id); assert_eq!(resolved.profile_id(), target_id);
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
assert_eq!(resolved.settings().network().as_str(), network); assert_eq!(resolved.settings().network().as_str(), network);
let postgres = match resolved.settings().backend() {
ksp_store_lib::StoreBackendSettings::Postgres(postgres) => std::option::Option::Some(postgres),
_ => std::option::Option::None,
};
assert!(postgres.is_some(), "committed Store target should remain PostgreSQL: {target_id}");
if let std::option::Option::Some(postgres) = postgres {
assert_eq!(postgres.tls_mode(), ksp_store_lib::PostgresTlsMode::Disabled);
}
assert_eq!( assert_eq!(
resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str), resolved.effective().value().pointer("/postgres/connection_uri").and_then(serde_json::Value::as_str),
std::option::Option::Some(expected_uri), std::option::Option::Some(expected_uri),

View File

@@ -0,0 +1,14 @@
# file: crates/ksp-job-api/Cargo.toml
# version: 1
[package]
name = "ksp-job-api"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
[lints]
workspace = true

View File

@@ -0,0 +1,72 @@
<!-- file: crates/ksp-job-api/README.md -->
<!-- version: 1 -->
# ksp-job-api
`ksp-job-api` fournit les contrats passifs et runtime-neutral communs aux jobs bornés de KSP.
La crate possède l'identité logique d'un job, son lifecycle terminable, l'intention d'annulation coopérative et le contrat latest-value utilisé pour observer un snapshot complet. Elle ne possède aucun runtime concret, aucune politique métier de backfill, aucun Transport, aucun Store et aucun Worker.
## Responsabilités
La façade crate-root expose :
- `JobId` et `JobKindCode`, bornés et validés ;
- `JobState` et `JobCompletion` ;
- `JobLifecycle`, propriétaire des transitions admises ;
- `JobCancellationToken`, cloneable et idempotent ;
- `JobNotificationSequence`, strictement monotone et sans wrap silencieux ;
- `JobNotification<S>`, valeur observable complète à une position donnée ;
- `JobSnapshotSource`, contrat runtime-neutral de lecture courante et attente d'une valeur plus récente ;
- les codes d'erreur Job et les types `Error`/`Result` communs de Core.
## Lifecycle
Le lifecycle admis reste explicitement borné :
```text
Created
| | +----------------------> Cancelled
v
Running
| | +----> Cancelling -----> Cancelled
| | | | +---------> Failed
| +-------------> Completed(Complete|Partial)
|
+------------------------> Completed(Complete|Partial)
+------------------------> Failed
```
Tout état terminal est immuable. Une transition invalide retourne `ERROR_CODE_JOB_TRANSITION_INVALID` sans modifier l'état source.
## Observation latest-value
`JobSnapshotSource` n'impose ni callback, ni queue d'événements, ni runtime async particulier. Un listener peut :
1. lire la valeur complète courante ;
2. mémoriser sa `JobNotificationSequence` ;
3. attendre une valeur plus récente ;
4. recevoir directement la dernière valeur complète, même si plusieurs mises à jour intermédiaires ont été coalescées.
Le snapshot concret appartient au job qui implémente la source. `ksp-job-api` ne connaît pas son contenu.
## Annulation
`JobCancellationToken` représente uniquement une intention coopérative partagée. Il ne tue pas une tâche, n'annule pas une I/O par lui-même et ne décide pas du résultat terminal. Le runtime concret reste responsable d'observer le token aux frontières sûres et de publier son état final.
## Firewall
La dépendance normale est volontairement minimale :
```text
ksp-job-api
-> ksp-core-lib
```
La crate ne dépend pas de Tokio, Futures, Transport, Store, Config, Logging, serde ni d'une crate de job concret.
## Documentation
- [`USAGE.md`](USAGE.md) — utilisation durable des contrats Job ;
- [`../../docs/architecture/003-COMPONENT_CONTRACTS.md`](../../docs/architecture/003-COMPONENT_CONTRACTS.md) — contrats de composants ;
- [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) — séparation workers/jobs et premier backfill RAW.

141
crates/ksp-job-api/USAGE.md Normal file
View File

@@ -0,0 +1,141 @@
<!-- file: crates/ksp-job-api/USAGE.md -->
<!-- version: 1 -->
# Utilisation de ksp-job-api
Cette page décrit la façade publique durable de `ksp-job-api`. Les consumers utilisent uniquement les exports du crate-root.
## Construire une identité de job
```rust
fn job_identity() -> ksp_job_api::Result<(ksp_job_api::JobId, ksp_job_api::JobKindCode)> {
let id = match ksp_job_api::JobId::new("raw-backfill-mainnet-0001") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kind = match ksp_job_api::JobKindCode::new("raw_transaction_backfill") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok((id, kind));
}
```
`JobId` identifie un job logique et ses reprises contrôlées. `JobKindCode` identifie une famille de jobs. Les deux sont bornés et utilisent un alphabet sûr ; leur `Debug` n'est pas une surface destinée à transporter un payload métier.
## Piloter un lifecycle passif
```rust
fn lifecycle(id: ksp_job_api::JobId, kind: ksp_job_api::JobKindCode) -> ksp_job_api::Result<ksp_job_api::JobLifecycle> {
let mut lifecycle = ksp_job_api::JobLifecycle::new(id, kind);
if let std::result::Result::Err(error) = lifecycle.start() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = lifecycle.complete(ksp_job_api::JobCompletion::Complete) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(lifecycle);
}
```
Le producer ne doit pas forcer un état directement. Il utilise les opérations de `JobLifecycle`, qui refusent les transitions hors matrice.
Pour une annulation observée pendant l'exécution :
```rust
if let std::result::Result::Err(error) = lifecycle.mark_cancelling() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = lifecycle.mark_cancelled() {
return std::result::Result::Err(error);
}
```
## Partager une intention d'annulation
```rust
let token = ksp_job_api::JobCancellationToken::new();
let listener = token.clone();
assert!(!listener.is_cancellation_requested());
assert!(token.request_cancellation());
assert!(listener.is_cancellation_requested());
assert!(!token.request_cancellation());
```
Le premier appel qui change l'état retourne `true`. Les demandes suivantes sont idempotentes et retournent `false`.
Le token ne doit pas être interprété comme une primitive de kill : le runtime concret décide où l'annulation peut interrompre l'admission ou une attente et quelles opérations déjà engagées doivent être drainées.
## Publier une valeur latest-value
Un producer concret peut construire une notification complète :
```rust
#[derive(Clone)]
struct Snapshot {
completed: usize,
}
let id = ksp_job_api::JobId::new("job-0001")?;
let kind = ksp_job_api::JobKindCode::new("example")?;
let sequence = ksp_job_api::JobNotificationSequence::initial();
let notification = ksp_job_api::JobNotification::new(
id,
kind,
sequence,
ksp_job_api::JobState::Running,
Snapshot { completed: 0 },
);
assert_eq!(notification.sequence().value(), 0);
assert_eq!(notification.state(), ksp_job_api::JobState::Running);
```
Le `Debug` de `JobNotification<S>` masque volontairement le snapshot. Le type concret `S` doit lui-même rester sûr à exposer lorsque le consumer accède explicitement à `snapshot()`.
## Implémenter une source de snapshots
Une implémentation concrète possède son runtime et expose seulement le contrat `JobSnapshotSource` :
```rust
async fn observe<S>(source: &S)
where
S: ksp_job_api::JobSnapshotSource,
{
let current = source.current();
let observed = current.sequence();
let newer = source.wait_for_change(observed).await;
assert!(newer.sequence().is_after(observed));
}
```
`wait_for_change` retourne la dernière valeur complète connue après coalescence éventuelle. Un consumer ne doit donc pas supposer qu'il recevra chaque mise à jour intermédiaire.
## Faire avancer une séquence
```rust
let first = ksp_job_api::JobNotificationSequence::initial();
let second = match first.next() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(second.is_after(first));
```
L'épuisement de `u64` est une erreur explicite ; la séquence ne wrappe jamais silencieusement.
## Frontières à respecter
Ne pas ajouter à `ksp-job-api` :
```text
Tokio/Futures runtime concret
Transport ou Store
Config/Logging
DTO métier d'un job précis
scheduler, worker ou control plane
persistence de checkpoint
```
Ces responsabilités appartiennent aux crates concrètes et à la composition supérieure.

View File

@@ -0,0 +1,44 @@
// file: crates/ksp-job-api/src/cancellation.rs
// version: 1
/// Runtime-neutral cloneable token carrying cooperative cancellation intent.
#[derive(Clone)]
pub struct JobCancellationToken {
requested: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl JobCancellationToken {
/// Creates a token with no cancellation request.
#[must_use]
pub fn new() -> Self {
return Self { requested: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)) };
}
/// Requests cancellation and returns `true` only for the first request shared by all clones.
#[must_use]
pub fn cancel(&self) -> bool {
return !self.requested.swap(true, std::sync::atomic::Ordering::AcqRel);
}
/// Reports whether cancellation has been requested through any clone.
#[must_use]
pub fn is_cancellation_requested(&self) -> bool {
return self.requested.load(std::sync::atomic::Ordering::Acquire);
}
}
impl std::default::Default for JobCancellationToken {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for JobCancellationToken {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("JobCancellationToken").field("cancellation_requested", &self.is_cancellation_requested()).finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/cancellation.rs"]
mod tests;

View File

@@ -0,0 +1,12 @@
// file: crates/ksp-job-api/src/error.rs
// version: 2
/// Error code used when a Job identifier violates its bounded safe-code contract.
pub const ERROR_CODE_JOB_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_id_invalid");
/// Error code used when a Job kind code violates its bounded safe-code contract.
pub const ERROR_CODE_JOB_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_kind_invalid");
/// Error code used when a Job notification sequence cannot advance without wrapping.
pub const ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("job_api", "job_notification_sequence_exhausted");
/// Error code used when a requested Job lifecycle transition is not allowed.
pub const ERROR_CODE_JOB_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_transition_invalid");

View File

@@ -0,0 +1,76 @@
// file: crates/ksp-job-api/src/identity.rs
// version: 1
/// Maximum UTF-8 byte length admitted for one Job identifier.
pub const MAX_JOB_ID_BYTES: usize = 128;
/// Maximum UTF-8 byte length admitted for one Job kind code.
pub const MAX_JOB_KIND_CODE_BYTES: usize = 128;
/// Bounded caller-supplied identity of one logical Job and its controlled resumptions.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JobId(std::string::String);
impl JobId {
/// Creates one non-empty Job identifier using the KSP safe-code alphabet.
pub fn new(value: impl std::convert::Into<std::string::String>) -> crate::Result<Self> {
let value = value.into();
if !valid_job_code(value.as_str(), crate::MAX_JOB_ID_BYTES) {
return std::result::Result::Err(identity_error(crate::ERROR_CODE_JOB_ID_INVALID, "job_id"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the validated Job identifier.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::fmt::Debug for JobId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("JobId(..)");
}
}
/// Bounded stable code identifying one concrete family of Jobs.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JobKindCode(std::string::String);
impl JobKindCode {
/// Creates one non-empty Job kind using the KSP safe-code alphabet.
pub fn new(value: impl std::convert::Into<std::string::String>) -> crate::Result<Self> {
let value = value.into();
if !valid_job_code(value.as_str(), crate::MAX_JOB_KIND_CODE_BYTES) {
return std::result::Result::Err(identity_error(crate::ERROR_CODE_JOB_KIND_INVALID, "job_kind"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the validated stable Job kind code.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::fmt::Debug for JobKindCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_tuple("JobKindCode").field(&self.0).finish();
}
}
fn identity_error(code: crate::ErrorCode, field: &'static str) -> crate::Error {
return crate::Error::new(code, "invalid bounded Job identity").with_context("field", field);
}
fn valid_job_code(value: &str, maximum_len: usize) -> bool {
if value.is_empty() || value.len() > maximum_len {
return false;
}
return value.bytes().all(|byte| return byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':'));
}
#[cfg(test)]
#[path = "../unit_tests/identity.rs"]
mod tests;

View File

@@ -0,0 +1,60 @@
// file: crates/ksp-job-api/src/lib.rs
// version: 2
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Passive runtime-neutral lifecycle contracts for bounded KSP Jobs.
//!
//! This foundation owns validated Job identity, explicit lifecycle transitions
//! cooperative cancellation intent and runtime-neutral latest-value observation.
//! Concrete Job behavior, runtime spawning, Transport, Store and Worker contracts
//! remain outside this crate.
mod cancellation;
mod error;
mod identity;
mod lifecycle;
mod notification;
/// Runtime-neutral cloneable token carrying cooperative cancellation intent.
pub use self::cancellation::JobCancellationToken;
/// Error code used when a Job identifier violates its bounded safe-code contract.
pub use self::error::ERROR_CODE_JOB_ID_INVALID;
/// Error code used when a Job kind code violates its bounded safe-code contract.
pub use self::error::ERROR_CODE_JOB_KIND_INVALID;
/// Error code used when a Job notification sequence cannot advance without wrapping.
pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;
/// Error code used when a requested Job lifecycle transition is not allowed.
pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;
/// Bounded caller-supplied identity of one logical Job and its controlled resumptions.
pub use self::identity::JobId;
/// Bounded stable code identifying one concrete family of Jobs.
pub use self::identity::JobKindCode;
/// Maximum UTF-8 byte length admitted for one Job identifier.
pub use self::identity::MAX_JOB_ID_BYTES;
/// Maximum UTF-8 byte length admitted for one Job kind code.
pub use self::identity::MAX_JOB_KIND_CODE_BYTES;
/// Completion classification of a Job that reached its normal terminal state.
pub use self::lifecycle::JobCompletion;
/// Passive owner of one Job identity and its validated lifecycle state.
pub use self::lifecycle::JobLifecycle;
/// Current lifecycle state of one bounded Job.
pub use self::lifecycle::JobState;
/// Latest complete observable value for one Job at a monotone sequence position.
pub use self::notification::JobNotification;
/// Monotone sequence attached to one latest-value Job notification stream.
pub use self::notification::JobNotificationSequence;
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
pub use self::notification::JobSnapshotFuture;
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
pub use self::notification::JobSnapshotSource;
/// Common KSP error type used by Job-facing contracts.
pub use ksp_core_lib::Error;
/// Stable structured code identifying a KSP error category and condition.
pub use ksp_core_lib::ErrorCode;
/// Structured contextual field attached to a KSP error.
pub use ksp_core_lib::ErrorContext;
/// Common KSP result alias using [`Error`].
pub use ksp_core_lib::Result;

View File

@@ -0,0 +1,168 @@
// file: crates/ksp-job-api/src/lifecycle.rs
// version: 1
/// Completion classification of a Job that reached its normal terminal state.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum JobCompletion {
/// Every selected candidate or unit of work reached a durable complete result.
Complete,
/// The Job terminated normally while retaining one or more explicitly observable gaps.
Partial,
}
impl JobCompletion {
/// Returns the stable safe code for this completion classification.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Complete => "complete",
Self::Partial => "partial",
};
}
}
/// Current lifecycle state of one bounded Job.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum JobState {
/// The Job was admitted but has not started producing work.
Created,
/// The Job is actively producing or draining normal work.
Running,
/// Cancellation was observed and new work must no longer be admitted.
Cancelling,
/// The Job reached a normal terminal state with an explicit completion classification.
Completed(crate::JobCompletion),
/// The Job reached its cooperative cancellation terminal state.
Cancelled,
/// The Job reached a terminal failure state.
Failed,
}
impl JobState {
/// Returns the stable safe lifecycle code without rendering Job data.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Created => "created",
Self::Running => "running",
Self::Cancelling => "cancelling",
Self::Completed(_) => "completed",
Self::Cancelled => "cancelled",
Self::Failed => "failed",
};
}
/// Returns the normal completion classification when the state is [`Self::Completed`].
#[must_use]
pub const fn completion(&self) -> std::option::Option<crate::JobCompletion> {
return match self {
Self::Completed(completion) => std::option::Option::Some(*completion),
_ => std::option::Option::None,
};
}
/// Reports whether no later lifecycle transition is permitted.
#[must_use]
pub const fn is_terminal(&self) -> bool {
return matches!(self, Self::Completed(_) | Self::Cancelled | Self::Failed);
}
}
/// Passive owner of one Job identity and its validated lifecycle state.
#[derive(Eq, PartialEq)]
pub struct JobLifecycle {
id: crate::JobId,
kind: crate::JobKindCode,
state: crate::JobState,
}
impl JobLifecycle {
/// Creates one lifecycle in [`JobState::Created`] state.
#[must_use]
pub const fn new(id: crate::JobId, kind: crate::JobKindCode) -> Self {
return Self { id, kind, state: crate::JobState::Created };
}
/// Returns the logical Job identity.
#[must_use]
pub const fn id(&self) -> &crate::JobId {
return &self.id;
}
/// Returns the stable Job family code.
#[must_use]
pub const fn kind(&self) -> &crate::JobKindCode {
return &self.kind;
}
/// Returns the current lifecycle state.
#[must_use]
pub const fn state(&self) -> crate::JobState {
return self.state;
}
/// Transitions a newly created Job into its running state.
pub fn start(&mut self) -> crate::Result<()> {
return self.transition(crate::JobState::Running);
}
/// Records that a running Job observed cooperative cancellation intent.
pub fn mark_cancelling(&mut self) -> crate::Result<()> {
return self.transition(crate::JobState::Cancelling);
}
/// Completes a running or cancelling Job with its explicit normal outcome.
pub fn complete(&mut self, completion: crate::JobCompletion) -> crate::Result<()> {
return self.transition(crate::JobState::Completed(completion));
}
/// Marks a created or cancelling Job as cooperatively cancelled.
pub fn mark_cancelled(&mut self) -> crate::Result<()> {
return self.transition(crate::JobState::Cancelled);
}
/// Marks a running or cancelling Job as failed.
pub fn fail(&mut self) -> crate::Result<()> {
return self.transition(crate::JobState::Failed);
}
fn transition(&mut self, target: crate::JobState) -> crate::Result<()> {
if !allowed_transition(self.state, target) {
return std::result::Result::Err(transition_error(self.state, target));
}
self.state = target;
return std::result::Result::Ok(());
}
}
impl std::fmt::Debug for JobLifecycle {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("JobLifecycle").field("id", &self.id).field("kind", &self.kind).field("state", &self.state).finish();
}
}
fn allowed_transition(source: crate::JobState, target: crate::JobState) -> bool {
return matches!(
(source, target),
(crate::JobState::Created, crate::JobState::Running)
| (crate::JobState::Created, crate::JobState::Cancelled)
| (crate::JobState::Running, crate::JobState::Cancelling)
| (crate::JobState::Running, crate::JobState::Completed(_))
| (crate::JobState::Running, crate::JobState::Failed)
| (crate::JobState::Cancelling, crate::JobState::Completed(_))
| (crate::JobState::Cancelling, crate::JobState::Cancelled)
| (crate::JobState::Cancelling, crate::JobState::Failed)
);
}
fn transition_error(source: crate::JobState, target: crate::JobState) -> crate::Error {
return crate::Error::new(crate::ERROR_CODE_JOB_TRANSITION_INVALID, "invalid Job lifecycle transition")
.with_context("source_state", source.code())
.with_context("target_state", target.code());
}
#[cfg(test)]
#[path = "../unit_tests/lifecycle.rs"]
mod tests;

View File

@@ -0,0 +1,126 @@
// file: crates/ksp-job-api/src/notification.rs
// version: 2
/// Monotone sequence attached to one latest-value Job notification stream.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JobNotificationSequence(u64);
impl JobNotificationSequence {
/// Creates the initial sequence position for one Job snapshot stream.
#[must_use]
pub const fn initial() -> Self {
return Self(0);
}
/// Returns the opaque numeric position carried by this sequence.
#[must_use]
pub const fn value(&self) -> u64 {
return self.0;
}
/// Advances the sequence exactly once or reports exhaustion without wrapping.
pub fn next(&self) -> crate::Result<Self> {
let next = match self.0.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
crate::Error::new(crate::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED, "Job notification sequence exhausted")
.with_context("sequence", self.0.to_string()),
);
},
};
return std::result::Result::Ok(Self(next));
}
/// Reports whether this sequence is strictly newer than an observed sequence.
#[must_use]
pub const fn is_after(&self, observed: Self) -> bool {
return self.0 > observed.0;
}
}
/// Latest complete observable value for one Job at a monotone sequence position.
#[derive(Clone, Eq, PartialEq)]
pub struct JobNotification<S> {
id: crate::JobId,
kind: crate::JobKindCode,
sequence: crate::JobNotificationSequence,
snapshot: S,
state: crate::JobState,
}
impl<S> JobNotification<S> {
/// Creates one immutable latest-value notification from an already validated Job identity and snapshot.
#[must_use]
pub const fn new(id: crate::JobId, kind: crate::JobKindCode, sequence: crate::JobNotificationSequence, state: crate::JobState, snapshot: S) -> Self {
return Self { id, kind, sequence, snapshot, state };
}
/// Returns the logical Job identity.
#[must_use]
pub const fn id(&self) -> &crate::JobId {
return &self.id;
}
/// Returns the stable Job family code.
#[must_use]
pub const fn kind(&self) -> &crate::JobKindCode {
return &self.kind;
}
/// Returns the monotone sequence of this latest value.
#[must_use]
pub const fn sequence(&self) -> crate::JobNotificationSequence {
return self.sequence;
}
/// Returns the complete safe snapshot owned by the concrete Job contract.
#[must_use]
pub const fn snapshot(&self) -> &S {
return &self.snapshot;
}
/// Returns the lifecycle state represented by this snapshot.
#[must_use]
pub const fn state(&self) -> crate::JobState {
return self.state;
}
}
impl<S> std::fmt::Debug for JobNotification<S> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("JobNotification")
.field("id", &self.id)
.field("kind", &self.kind)
.field("sequence", &self.sequence)
.field("state", &self.state)
.field("snapshot", &"<redacted>")
.finish();
}
}
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
pub type JobSnapshotFuture<'a, S> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = crate::JobNotification<S>> + std::marker::Send + 'a>>;
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
pub trait JobSnapshotSource: std::marker::Send + std::marker::Sync {
/// Complete snapshot type retained by the concrete source.
type Snapshot: std::clone::Clone + std::marker::Send + std::marker::Sync + 'static;
/// Returns the complete current value without requiring replay of prior notifications.
#[must_use]
fn current(&self) -> crate::JobNotification<Self::Snapshot>;
/// Waits for a value newer than `observed`, returning the complete current snapshot after coalescing any intermediate updates.
fn wait_for_change(&self, observed: crate::JobNotificationSequence) -> crate::JobSnapshotFuture<'_, Self::Snapshot>;
}
#[cfg(test)]
fn exhausted_notification_sequence() -> crate::JobNotificationSequence {
return JobNotificationSequence(u64::MAX);
}
#[cfg(test)]
#[path = "../unit_tests/notification.rs"]
mod tests;

View File

@@ -0,0 +1,82 @@
// file: crates/ksp-job-api/tests/dependency_boundary.rs
// version: 2
//! Dependency and runtime-neutrality canaries for the Job API foundation.
#[test]
fn pre_002_manifest_has_exact_core_only_dependency_graph() {
let manifest = include_str!("../Cargo.toml");
assert!(!manifest.contains("[features]"));
assert!(!manifest.contains("[dev-dependencies]"));
assert!(!manifest.contains("[build-dependencies]"));
assert_eq!(manifest.matches("[dependencies]").count(), 1);
let dependencies_tail = manifest.split("[dependencies]").nth(1);
assert!(dependencies_tail.is_some());
let dependencies_tail = match dependencies_tail {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dependencies = match dependencies_tail.split("[lints]").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(manifest_dependency_names(dependencies), std::vec!["ksp-core-lib"]);
return;
}
#[test]
fn pre_003_production_sources_forbid_runtime_domain_and_wire_dependencies() {
let sources = [
include_str!("../src/cancellation.rs"),
include_str!("../src/error.rs"),
include_str!("../src/identity.rs"),
include_str!("../src/lib.rs"),
include_str!("../src/lifecycle.rs"),
include_str!("../src/notification.rs"),
];
for source in sources {
for forbidden in [
"ksp_config_lib::",
"ksp_interface_lib::",
"ksp_logging_lib::",
"ksp_offchain_transport_lib::",
"ksp_onchain_transport_lib::",
"ksp_store_api::",
"ksp_store_lib::",
"ksp_worker",
"reqwest::",
"serde::",
"serde_json::",
"solana_",
"tauri::",
"tokio::",
"tonic::",
concat!("tracing", "::"),
] {
assert!(!source.contains(forbidden), "forbidden Job API dependency path detected: {forbidden}");
}
}
return;
}
fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> {
let mut names = std::vec::Vec::new();
for line in section.lines() {
let content = match line.split('#').next() {
std::option::Option::Some(value) => value.trim(),
std::option::Option::None => continue,
};
if content.is_empty() {
continue;
}
let name = match content.split('=').next() {
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
std::option::Option::None => continue,
};
if !name.is_empty() {
names.push(name);
}
}
names.sort_unstable();
return names;
}

View File

@@ -0,0 +1,148 @@
// file: crates/ksp-job-api/tests/notifications.rs
// version: 1
//! External-consumer canaries for latest-value Job observation.
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestSnapshot {
completed: u64,
}
#[derive(Clone)]
struct TestSnapshotSource {
current: std::sync::Arc<ksp_job_api::JobNotification<TestSnapshot>>,
}
impl TestSnapshotSource {
fn new(current: ksp_job_api::JobNotification<TestSnapshot>) -> Self {
return Self { current: std::sync::Arc::new(current) };
}
}
impl ksp_job_api::JobSnapshotSource for TestSnapshotSource {
type Snapshot = TestSnapshot;
fn current(&self) -> ksp_job_api::JobNotification<Self::Snapshot> {
return self.current.as_ref().clone();
}
fn wait_for_change(&self, observed: ksp_job_api::JobNotificationSequence) -> ksp_job_api::JobSnapshotFuture<'_, Self::Snapshot> {
let current = ksp_job_api::JobSnapshotSource::current(self);
assert!(current.sequence().is_after(observed));
return std::boxed::Box::pin(std::future::ready(current));
}
}
struct TestWake;
impl std::task::Wake for TestWake {
fn wake(self: std::sync::Arc<Self>) {
return;
}
}
fn poll_ready<S>(mut future: ksp_job_api::JobSnapshotFuture<'_, S>) -> std::option::Option<ksp_job_api::JobNotification<S>> {
let waker = std::task::Waker::from(std::sync::Arc::new(TestWake));
let mut context = std::task::Context::from_waker(&waker);
return match std::future::Future::poll(future.as_mut(), &mut context) {
std::task::Poll::Ready(value) => std::option::Option::Some(value),
std::task::Poll::Pending => std::option::Option::None,
};
}
fn notification_sequence(value: u64) -> std::option::Option<ksp_job_api::JobNotificationSequence> {
let mut sequence = ksp_job_api::JobNotificationSequence::initial();
for _ in 0..value {
sequence = match sequence.next() {
std::result::Result::Ok(next) => next,
std::result::Result::Err(_) => return std::option::Option::None,
};
}
return std::option::Option::Some(sequence);
}
fn notification(sequence_value: u64, state: ksp_job_api::JobState, completed: u64) -> std::option::Option<ksp_job_api::JobNotification<TestSnapshot>> {
let id = match ksp_job_api::JobId::new("external-job-001") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let kind = match ksp_job_api::JobKindCode::new("backfill_raw") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let sequence = match notification_sequence(sequence_value) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
return std::option::Option::Some(ksp_job_api::JobNotification::new(id, kind, sequence, state, TestSnapshot { completed }));
}
#[test]
fn pre_003_external_notification_contract_is_consumable_from_crate_root() {
let notification = notification(3, ksp_job_api::JobState::Running, 2);
assert!(notification.is_some());
let notification = match notification {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(notification.id().as_str(), "external-job-001");
assert_eq!(notification.kind().as_str(), "backfill_raw");
assert_eq!(notification.sequence().value(), 3);
assert_eq!(notification.state(), ksp_job_api::JobState::Running);
assert_eq!(notification.snapshot().completed, 2);
return;
}
#[test]
fn pre_003_slow_and_independent_listeners_resynchronize_to_latest_value() {
let latest = notification(8, ksp_job_api::JobState::Running, 7);
assert!(latest.is_some());
let source = match latest {
std::option::Option::Some(value) => TestSnapshotSource::new(value),
std::option::Option::None => return,
};
let listener_a = source.clone();
let listener_b = source.clone();
let observed_a = match notification_sequence(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let observed_b = match notification_sequence(6) {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let changed_a = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_a, observed_a));
let changed_b = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_b, observed_b));
assert!(changed_a.is_some());
assert!(changed_b.is_some());
let changed_a = match changed_a {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let changed_b = match changed_b {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(changed_a.sequence().value(), 8);
assert_eq!(changed_b.sequence().value(), 8);
assert_eq!(changed_a.snapshot().completed, 7);
assert_eq!(changed_b.snapshot().completed, 7);
return;
}
#[test]
fn pre_003_terminal_snapshot_remains_readable_from_shared_source() {
let terminal = notification(9, ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial), 8);
assert!(terminal.is_some());
let source = match terminal {
std::option::Option::Some(value) => TestSnapshotSource::new(value),
std::option::Option::None => return,
};
let cloned = source.clone();
let current = ksp_job_api::JobSnapshotSource::current(&cloned);
assert_eq!(current.sequence().value(), 9);
assert_eq!(current.state(), ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial));
assert!(current.state().is_terminal());
assert_eq!(current.snapshot().completed, 8);
return;
}

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-job-api/tests/public_api.rs
// version: 1
//! External-consumer canaries for the public Job API foundation.
#[test]
fn pre_002_identity_lifecycle_and_cancellation_are_consumable_from_crate_root() {
let id = ksp_job_api::JobId::new("campaign-2026.09.01:001");
let kind = ksp_job_api::JobKindCode::new("backfill_raw");
assert!(id.is_ok());
assert!(kind.is_ok());
let id = match id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut lifecycle = ksp_job_api::JobLifecycle::new(id.clone(), kind.clone());
assert_eq!(lifecycle.id(), &id);
assert_eq!(lifecycle.kind(), &kind);
assert_eq!(lifecycle.state(), ksp_job_api::JobState::Created);
assert!(lifecycle.start().is_ok());
assert!(lifecycle.complete(ksp_job_api::JobCompletion::Partial).is_ok());
assert_eq!(lifecycle.state().completion(), std::option::Option::Some(ksp_job_api::JobCompletion::Partial));
let token = ksp_job_api::JobCancellationToken::new();
let clone = token.clone();
assert!(token.cancel());
assert!(clone.is_cancellation_requested());
assert!(!clone.cancel());
return;
}
#[test]
fn pre_002_error_codes_are_stable_and_core_owned() {
let codes: [ksp_core_lib::ErrorCode; 3] =
[ksp_job_api::ERROR_CODE_JOB_ID_INVALID, ksp_job_api::ERROR_CODE_JOB_KIND_INVALID, ksp_job_api::ERROR_CODE_JOB_TRANSITION_INVALID];
assert_eq!(codes[0].domain(), "job_api");
assert_eq!(codes[0].code(), "job_id_invalid");
assert_eq!(codes[1].domain(), "job_api");
assert_eq!(codes[1].code(), "job_kind_invalid");
assert_eq!(codes[2].domain(), "job_api");
assert_eq!(codes[2].code(), "job_transition_invalid");
return;
}
#[test]
fn pre_002_public_bounds_are_exact() {
assert_eq!(ksp_job_api::MAX_JOB_ID_BYTES, 128);
assert_eq!(ksp_job_api::MAX_JOB_KIND_CODE_BYTES, 128);
return;
}

View File

@@ -0,0 +1,101 @@
// file: crates/ksp-job-api/tests/release_completeness.rs
// version: 2
//! Completeness canaries for the current Job API foundation.
#[test]
fn pre_003_crate_root_export_inventory_is_exact() {
let crate_root = include_str!("../src/lib.rs");
let mut actual = std::vec::Vec::new();
for line in crate_root.lines() {
let trimmed = line.trim();
if trimmed.starts_with("pub use ") {
actual.push(trimmed);
}
}
actual.sort_unstable();
let mut expected = std::vec![
"pub use self::cancellation::JobCancellationToken;",
"pub use self::error::ERROR_CODE_JOB_ID_INVALID;",
"pub use self::error::ERROR_CODE_JOB_KIND_INVALID;",
"pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;",
"pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;",
"pub use self::identity::JobId;",
"pub use self::identity::JobKindCode;",
"pub use self::identity::MAX_JOB_ID_BYTES;",
"pub use self::identity::MAX_JOB_KIND_CODE_BYTES;",
"pub use self::lifecycle::JobCompletion;",
"pub use self::lifecycle::JobLifecycle;",
"pub use self::lifecycle::JobState;",
"pub use self::notification::JobNotification;",
"pub use self::notification::JobNotificationSequence;",
"pub use self::notification::JobSnapshotFuture;",
"pub use self::notification::JobSnapshotSource;",
"pub use ksp_core_lib::Error;",
"pub use ksp_core_lib::ErrorCode;",
"pub use ksp_core_lib::ErrorContext;",
"pub use ksp_core_lib::Result;",
];
expected.sort_unstable();
assert_eq!(actual, expected);
assert!(!crate_root.contains("pub mod "));
return;
}
#[test]
fn pre_003_production_module_inventory_is_exact() -> std::io::Result<()> {
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let entries = match std::fs::read_dir(source_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut names = std::vec::Vec::new();
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let file_type = match entry.file_type() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !file_type.is_file() {
continue;
}
let name = match entry.file_name().into_string() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
if name.ends_with(".rs") {
names.push(name);
}
}
names.sort_unstable();
assert_eq!(names, std::vec!["cancellation.rs", "error.rs", "identity.rs", "lib.rs", "lifecycle.rs", "notification.rs"]);
return std::result::Result::Ok(());
}
#[test]
fn pre_003_surface_opens_only_generic_notifications_without_backfill_or_worker_contracts() {
let sources = [
include_str!("../src/cancellation.rs"),
include_str!("../src/identity.rs"),
include_str!("../src/lib.rs"),
include_str!("../src/lifecycle.rs"),
include_str!("../src/notification.rs"),
];
for source in sources {
for forbidden in ["BackfillRequest", "WorkerControl", "WorkerState", "spawn(", "RawTransaction", "provider", "endpoint"] {
assert!(!source.contains(forbidden), "future or domain-specific Job contract leaked early: {forbidden}");
}
}
let notification_source = include_str!("../src/notification.rs");
assert!(notification_source.contains("pub struct JobNotification<S>"));
assert!(notification_source.contains("pub trait JobSnapshotSource"));
assert!(notification_source.contains("std::future::Future"));
assert!(!notification_source.contains("tokio::"));
let lifecycle_source = include_str!("../src/lifecycle.rs");
assert!(lifecycle_source.contains("#[derive(Eq, PartialEq)]\npub struct JobLifecycle"));
assert!(!lifecycle_source.contains("#[derive(Clone, Eq, PartialEq)]\npub struct JobLifecycle"));
return;
}

View File

@@ -0,0 +1,72 @@
// file: crates/ksp-job-api/tests/security_hardening.rs
// version: 1
//! Adversarial lifecycle, cancellation and redaction canaries.
const HOSTILE_MARKER: &str = "JOB-IDENTITY-SECRET-CANARY";
#[test]
fn pre_002_job_id_and_lifecycle_debug_redact_caller_identity() {
let id = ksp_job_api::JobId::new(HOSTILE_MARKER);
let kind = ksp_job_api::JobKindCode::new("backfill_raw");
assert!(id.is_ok());
assert!(kind.is_ok());
let id = match id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(std::format!("{id:?}"), "JobId(..)");
let lifecycle = ksp_job_api::JobLifecycle::new(id, kind);
let debug = std::format!("{lifecycle:?}");
assert!(debug.contains("JobLifecycle"));
assert!(debug.contains("backfill_raw"));
assert!(debug.contains("Created"));
assert!(!debug.contains(HOSTILE_MARKER));
return;
}
#[test]
fn pre_002_terminal_states_are_immutable_under_all_public_mutators() {
let id = ksp_job_api::JobId::new("terminal-job");
let kind = ksp_job_api::JobKindCode::new("test_job");
assert!(id.is_ok());
assert!(kind.is_ok());
let id = match id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut lifecycle = ksp_job_api::JobLifecycle::new(id, kind);
assert!(lifecycle.start().is_ok());
assert!(lifecycle.complete(ksp_job_api::JobCompletion::Complete).is_ok());
assert!(lifecycle.start().is_err());
assert!(lifecycle.mark_cancelling().is_err());
assert!(lifecycle.complete(ksp_job_api::JobCompletion::Partial).is_err());
assert!(lifecycle.mark_cancelled().is_err());
assert!(lifecycle.fail().is_err());
assert_eq!(lifecycle.state(), ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Complete));
return;
}
#[test]
fn pre_002_cancellation_token_is_send_sync_and_cross_thread_visible() {
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
require_send_sync::<ksp_job_api::JobCancellationToken>();
let token = ksp_job_api::JobCancellationToken::new();
let worker_token = token.clone();
let thread = std::thread::spawn(move || return worker_token.cancel());
let request = match thread.join() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(request);
assert!(token.is_cancellation_requested());
return;
}

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