v0.1.0-pre.058

This commit is contained in:
2026-07-27 02:02:11 +02:00
parent e56f2561c5
commit e828677201
22 changed files with 378 additions and 192 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: KHADHROONY_BOT3_MIGRATION_CLOSURE_TODO.md -->
<!-- version: 4 -->
<!-- version: 7 -->
# Checklist détaillée de clôture de la migration `khadhroony-bot3`
@@ -86,16 +86,28 @@
- [x] Garantir exactement un bouton Effacer pour la sortie principale.
- [x] Vider le buffer TypeScript lors de leffacement.
- [ ] Tester JSON, scalaire, texte, erreur et résultat vide.
- [x] Restaurer le gabarit desktop à deux colonnes : requête à gauche, endpoints et résultat à droite.
### 2.2 WebSocket
- [x] Afficher les messages dans un composant texte/log.
- [x] Fixer la hauteur et le scroll interne.
- [x] Garantir une seule paire Copier/Effacer pour la sortie Messages.
- [ ] Conserver les messages pendant la session active.
- [ ] Restaurer laffichage après fermeture/réouverture de la fenêtre.
- [x] Borner le débit et la taille des notifications envoyées à lUI pour éviter les freezes.
- [x] Conserver le payload complet sous forme JSON compacte dans les logs de debug avant formatage et troncage UI.
- [x] Conserver les messages pendant la session active.
- [x] Restaurer laffichage après fermeture/réouverture de la fenêtre.
- [x] Restaurer le gabarit desktop à deux colonnes : souscription à gauche, statut/endpoints/messages à droite.
- [x] Appliquer `base64` par défaut aux souscriptions `accountSubscribe` et `programSubscribe` lorsque lencodage est absent, sans écraser un encodage explicite.
- [ ] Décider après test runtime si Statut et Endpoints WebSocket restent textuels ou deviennent des viewers JSON structurés ; supprimer alors tout bouton Copier redondant fourni par le viewer.
### 2.3 Journaux généraux et viewers
### 2.3 Backfill et autocomplétion
- [x] Garder Program ID librement éditable.
- [x] Ajouter un autocomplétion non bloquant depuis le registre public `kb-program-ids`.
- [ ] Ajouter ultérieurement les autocomplétions Token et Pool lorsque leurs tables de référence existeront.
### 2.4 Journaux généraux et viewers
- [ ] Vérifier Backfill HTTP.
- [ ] Vérifier Extraction core.
@@ -110,7 +122,7 @@
- [ ] Utiliser des textareas pour texte brut, logs et diagnostics concaténés.
- [ ] Fixer la hauteur de tous les viewers JSON.
### 2.4 Fenêtres SQL
### 2.5 Fenêtres SQL
- [ ] Mettre toutes les fenêtres SQL, sauf Replay Candidates, sur une seule colonne pleine largeur.
- [ ] Mettre les tables sur toute la largeur.
@@ -155,35 +167,44 @@
- [ ] Marquer les variables limitées aux scénarios de démonstration.
- [ ] Documenter les futurs remplacements par configuration ou structure typée.
## 5.1 Wrappers Tauri
- [x] Déplacer la logique complète de `splash_frontend_ready` dans `splash.rs` et conserver un wrapper privé mince dans `tauri.rs`.
- [ ] Inventorier toutes les fonctions `#[tauri::command]` de `tauri.rs`.
- [ ] Déplacer progressivement toute logique métier restante dans son module fonctionnel `pub(crate)`.
- [ ] Vérifier que chaque wrapper Tauri ne fait que ladaptation des arguments et la délégation.
- [ ] Ajouter ou conserver les tests unitaires dans les modules fonctionnels, pas dans les wrappers.
## 6. Cycle de vie WebSocket
- [ ] Vérifier quaucun pool WebSocket nest créé au démarrage.
- [ ] Vérifier la création paresseuse au premier accès.
- [ ] Vérifier le stockage du pool dans `AppState`.
- [ ] Vérifier la survie de la session après fermeture de `demo_ws`.
- [ ] Vérifier la récupération de létat à la réouverture.
- [x] Vérifier quaucun pool WebSocket nest créé au démarrage.
- [x] Vérifier la création paresseuse au premier accès.
- [x] Vérifier le stockage du pool dans `AppState`.
- [x] Vérifier la survie de la session après fermeture de `demo_ws`.
- [x] Vérifier la récupération de létat à la réouverture.
- [ ] Vérifier la fermeture explicite.
- [ ] Vérifier le timeout.
- [ ] Vérifier larrêt global de lapplication.
- [ ] Vérifier que la fermeture de la fenêtre nappelle jamais la déconnexion globale.
- [x] Vérifier que la fermeture de la fenêtre nappelle jamais la déconnexion globale.
## 7. Initialisation PostgreSQL et splash
- [ ] Charger lenvironnement.
- [ ] Charger et valider la configuration.
- [ ] Initialiser le logging.
- [ ] Initialiser le pool HTTP.
- [x] Charger lenvironnement.
- [x] Charger et valider la configuration.
- [x] Initialiser le logging.
- [x] Initialiser le pool HTTP.
- [ ] Se connecter à PostgreSQL.
- [ ] Appeler `initialize_postgres_schema_for_startup`.
- [ ] Appeler `emit_sql_startup_table_report`.
- [ ] Relier `emit_sql_startup_error`.
- [ ] Relier `emit_sql_startup_splash`.
- [ ] Ouvrir `main` seulement après le rapport PostgreSQL.
- [ ] Fermer le splash après le statut final.
- [ ] Ne pas initialiser WebSocket dans cette séquence.
- [ ] Rendre la zone de messages du splash scrollable.
- [ ] Garder le dernier message visible.
- [ ] Afficher connexion, schéma, nombre de tables, tables manquantes et statut final.
- [x] Appeler `initialize_postgres_schema_for_startup` après disponibilité du frontend splash.
- [x] Appeler `emit_sql_startup_table_report`.
- [x] Relier `emit_sql_startup_error`.
- [x] Relier `emit_sql_startup_splash`.
- [x] Ouvrir `main` seulement après le rapport PostgreSQL et le fade-out.
- [x] Fermer le splash après le statut final.
- [x] Ne pas initialiser WebSocket dans cette séquence.
- [x] Rendre les zones du splash scrollables sans afficher leurs barres de défilement.
- [x] Garder automatiquement le dernier log et le dernier message visibles.
- [x] Afficher connexion, schéma, nombre de tables, tables manquantes et statut final.
- [ ] Valider visuellement le fade-in, les messages intermédiaires, le fade-out et louverture de `main` sur le workspace réel.
## 8. Purge contrôlée des données dérivées
@@ -295,7 +316,7 @@
- [ ] `cargo fmt --all`.
- [ ] `cargo check -p kb-config`.
- [ ] `cargo test -p kb-config`.
- [ ] `cargo test -p kb-config` (44/45 passaient ; contrat de route groupée `kb-pipeline` corrigé dans la prochaine prerelease).
- [ ] `cargo check -p kb-pipeline-demo-scenarios`.
- [ ] `cargo test -p kb-pipeline-demo-scenarios`.
- [ ] `cargo check -p kb-store`.
@@ -303,7 +324,7 @@
- [ ] `cargo check -p kb-lib`.
- [ ] `cargo test -p kb-lib`.
- [x] `cargo check -p kb-app-demo-desktop`.
- [ ] `cargo test -p kb-app-demo-desktop` (112 tests passaient, 1 contrat dordre de décodeurs corrigé dans `pre.057`; relance requise).
- [ ] `cargo test -p kb-app-demo-desktop` (112/113 passaient ; contrat dordre de décodeurs corrigé, relance requise).
- [x] `cargo check --workspace`.
- [ ] `cargo test --workspace`.
- [ ] `python3 scripts/audit_rust_workspace_rules.py`.

View File

@@ -1,5 +1,5 @@
<!-- file: RULES_GENERAL.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Règles générales du projet
@@ -85,3 +85,9 @@ python3 scripts/audit_rust_workspace_rules.py
```
Les validations supplémentaires dépendent du périmètre du delta et sont consignées dans `delta.md`.
### Fin de fichier et formatters
- La contrainte de fin de fichier avec exactement un saut de ligne terminal sapplique aux sources Rust, TypeScript, JavaScript, Python, shell, TOML, YAML et Markdown lorsquelle est vérifiée par les outils du projet.
- Elle ne sapplique pas aux fichiers HTML ni JSON : leur formatter peut supprimer automatiquement la ligne vide terminale.
- Aucun audit ne doit réintroduire artificiellement une ligne vide dans un fichier HTML ou JSON après formatage.

View File

@@ -1,5 +1,5 @@
<!-- file: RULES_RUST.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Règles Rust générales
@@ -70,6 +70,12 @@ implicit_saturating_sub = "allow"
- Le groupe `pub use` précède le groupe `pub(crate) use` dans une même façade.
- Lordre interne suit lordre naturel produit par `rustfmt` : les segments et mots séparés sont comparés avant leurs suffixes numériques, par exemple `U8` avant `U16` et `TOKEN` avant `TOKEN2022`.
## Assertions sur les collections
- Un test qui vérifie uniquement lappartenance à un ensemble peut trier la valeur obtenue et la valeur attendue avant `assert_eq!`, afin de ne pas dépendre dun ordre denregistrement non contractuel.
- Le tri doit être explicite dans le test et appliqué aux deux collections comparées.
- Ne jamais trier une collection lorsque lordre fait partie du contrat : comptes Solana, metas, instructions, signers, événements, étapes de pipeline, priorités, journaux ou toute autre séquence ordonnée.
## Imports et chemins
- `use` est interdit pour les constantes, fonctions, structures, énumérations, unions, alias de types, modules et macros.

View File

@@ -1,5 +1,5 @@
<!-- file: RULES_SPECIFIC_KHADHROONY.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Règles spécifiques à `khadhroony-bot3`
@@ -27,6 +27,8 @@ Toute divergence avec une règle générale doit être explicitement documentée
- Les bindings générés doivent être produits dans un dossier dédié, généralement `../frontend/ts/bindings` ou `#[ts(export, export_to = "../frontend/ts/bindings/MyStruct.ts")]`.
- Les types purement internes au backend ne doivent pas être exportés vers TypeScript par défaut.
- Corriger le type Rust/TS-rs source puis régénérer les bindings ; ne pas considérer une modification manuelle isolée dun fichier généré comme un correctif durable.
- `kb-app-demo-desktop/src/tauri.rs` contient les attributs `#[tauri::command]`, lenregistrement des commandes et des wrappers privés minces ; la logique complète doit vivre dans le module fonctionnel correspondant sous une fonction `pub(crate)` testable.
- Un wrapper Tauri ne doit effectuer que ladaptation des handles/states/arguments, lappel de la fonction de module et le retour du résultat ; toute validation métier, orchestration ou construction de payload doit rester hors de `tauri.rs`.
## Règles d'architecture

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_backfill.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
@@ -97,7 +97,7 @@
<div aria-labelledby="programBackfillHeading" class="accordion-collapse collapse" data-bs-parent="#backfillAccordion" id="programBackfillCollapse">
<div class="accordion-body">
<div class="row g-3 mb-3">
<div class="col-12"><label class="form-label" for="programAddressInput">Program ID</label><input class="form-control font-monospace" id="programAddressInput" /></div>
<div class="col-12"><label class="form-label" for="programAddressInput">Program ID</label><input class="form-control font-monospace" id="programAddressInput" list="programAddressOptions" autocomplete="off" /><datalist id="programAddressOptions"></datalist></div>
<div class="col-12"><label class="form-label" for="programAnchorInput">Signature d'ancrage</label><input class="form-control font-monospace" id="programAnchorInput" />
<div class="form-text">Optionnelle pour « Avant, plus anciennes » : vide, la recherche commence aux transactions les plus récentes. Obligatoire pour « Après, plus récentes ».</div>
</div>

View File

@@ -35,7 +35,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h1 class="h3 card-title">Extraction transactionnelle canonical vers core</h1>

View File

@@ -32,7 +32,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h1 class="h4 mb-3">Sélection bornée</h1>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_http.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
@@ -27,7 +27,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0">
<div class="card-body">
<h1 class="h4 card-title">Requête HTTP Solana standard</h1>

View File

@@ -202,6 +202,7 @@
<div class="col-12 col-lg-4">
<label for="programContainsInput" class="form-label">Program ID contient</label>
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
<datalist id="knownProgramDatalist"></datalist>
</div>
<div class="col-6 col-lg-1">
<label for="programLimitInput" class="form-label">Limite</label>
@@ -439,7 +440,6 @@
</div>
</div>
</footer>
<datalist id="knownProgramDatalist"></datalist>
<script type="module" src="ts/demo_sql_replay_candidates.ts" defer></script>
</body>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_ws.html -->
<!-- version: 7 -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
@@ -27,7 +27,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0">
<div class="card-body">
<h1 class="h4 card-title">Souscription WebSocket Solana standard</h1>

View File

@@ -1,5 +1,5 @@
// file: kb_app_demo/frontend/sass/splash.scss
// version: 3
// version: 4
@font-face {
font-family: 'Dos Amazigh';
@@ -66,6 +66,7 @@ body {
width: 50%;
max-height: 30%;
overflow-y: auto;
scrollbar-width: none;
color: white;
font-family: monospace;
font-size: 12px;
@@ -77,17 +78,25 @@ body {
#messages-container {
position: absolute;
font-family: Arial, sans-serif;
font-size: 14px;
bottom: 0;
left: 0;
width: 100%;
max-height: 30%;
overflow-y: auto;
scrollbar-width: none;
background-color: rgba(0, 0, 0, 0.1);
padding: 10px 10px 40px;
z-index: 2;
box-sizing: border-box;
}
#debug-info::-webkit-scrollbar,
#messages-container::-webkit-scrollbar {
display: none;
}
.splash-message {
margin-bottom: 8px;
padding: 6px 10px;

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_backfill.ts
// version: 7
// version: 8
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -210,6 +210,17 @@ async function cancelBackfill(): Promise<void> {
async function loadOptions(): Promise<void> {
const options = await invoke<DemoBackfillOptionsPayload>("demo_backfill_options");
const optionsWithPrograms = options as DemoBackfillOptionsPayload & {
programs: Array<{ code: string; programId: string }>;
};
const programOptions = element<HTMLDataListElement>("#programAddressOptions");
programOptions.replaceChildren();
for (const program of optionsWithPrograms.programs) {
const option = document.createElement("option");
option.value = program.programId;
option.label = program.code;
programOptions.append(option);
}
const roleSelect = element<HTMLSelectElement>("#backfillRoleSelect");
roleSelect.replaceChildren();
for (const role of options.roles) {

View File

@@ -1,6 +1,7 @@
// file: kb-app-demo-desktop/frontend/ts/splash.ts
// version: 2
// version: 3
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { SplashOrder } from "./bindings/kb_app_demo_desktop/splash/SplashOrder.ts";
@@ -105,6 +106,7 @@ async function initializeSplash(): Promise<void> {
await listen<SplashOrder>("splash", event => {
void handleSplashOrder(event.payload);
});
await invoke("splash_frontend_ready");
}
document.addEventListener("DOMContentLoaded", () => {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 9
// version: 10
//! Shared Tauri application state and startup initialization.
@@ -23,6 +23,7 @@ pub(crate) struct AppState {
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
startup_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
@@ -68,6 +69,7 @@ impl crate::AppState {
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
@@ -172,6 +174,19 @@ impl crate::AppState {
return &self.demo_execution_solana_core_cancel_requested;
}
/// Starts the splash startup sequence at most once.
pub(crate) fn begin_startup_sequence(&self) -> bool {
return self
.startup_sequence_started
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
)
.is_ok();
}
/// Returns the number of logging routes held by the logging guard.
pub(crate) fn logging_route_count(&self) -> usize {
let lock_result = self.logging_guard.lock();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_backfill.rs
// version: 11
// version: 12
//! Tauri commands and UI payloads for bounded HTTP transaction backfills.
@@ -20,6 +20,20 @@ pub(crate) struct DemoBackfillRoleOption {
pub(crate) providers: std::vec::Vec<std::string::String>,
}
/// One known program suggested by the free-form Program ID field.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillProgramOption.ts"
)]
pub(crate) struct DemoBackfillProgramOption {
/// Stable program code.
pub(crate) code: std::string::String,
/// Canonical program identifier.
pub(crate) program_id: std::string::String,
}
/// Initial options shown by the backfill demo.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -30,6 +44,8 @@ pub(crate) struct DemoBackfillRoleOption {
pub(crate) struct DemoBackfillOptionsPayload {
/// Selectable endpoint roles.
pub(crate) roles: std::vec::Vec<crate::DemoBackfillRoleOption>,
/// Known programs offered as non-blocking autocomplete suggestions.
pub(crate) programs: std::vec::Vec<crate::DemoBackfillProgramOption>,
/// Preferred role when configured.
pub(crate) default_role: std::option::Option<std::string::String>,
/// Default transaction commitment.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
// version: 28
// version: 29
//! Tauri commands and UI payloads for contextual instruction decode replay.
@@ -720,22 +720,22 @@ mod tests {
fn native_memo_token2022_elgamal_classic_token_and_ata_decoders_are_registered() {
let decoders = crate::available_decoders();
assert_eq!(decoders.len(), 7);
let names = decoders
let mut names = decoders
.iter()
.map(|decoder| return decoder.identity().name)
.collect::<std::vec::Vec<_>>();
assert_eq!(
names,
std::vec![
"solana_native_classifier".to_string(),
"spl_associated_token_account".to_string(),
"spl_elgamal_registry".to_string(),
"spl_memo".to_string(),
"spl_token".to_string(),
"spl_token2022".to_string(),
"metadata_metaplex_token_metadata".to_string(),
]
);
names.sort();
let mut expected_names = std::vec![
"metadata_metaplex_token_metadata".to_string(),
"solana_native_classifier".to_string(),
"spl_associated_token_account".to_string(),
"spl_elgamal_registry".to_string(),
"spl_memo".to_string(),
"spl_token".to_string(),
"spl_token2022".to_string(),
];
expected_names.sort();
assert_eq!(names, expected_names);
}
#[test]

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_sql_common.rs
// version: 8
// version: 9
//! Shared SQL demo helpers and serializable payloads.
@@ -201,25 +201,23 @@ pub(crate) async fn initialize_postgres_schema_for_startup(
return;
},
};
if !options.auto_initialize_schema {
emit_sql_startup_splash(
splash_window,
"PostgreSQL schema auto-initialization disabled.",
"info",
false,
);
let masked_dsn = options.masked_dsn();
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "PostgreSQL schema auto-initialization disabled");
return;
}
let initialize_schema = options.auto_initialize_schema;
let masked_dsn = options.masked_dsn();
options.auto_initialize_schema = false;
emit_sql_startup_splash(
splash_window,
"Checking PostgreSQL raw/core/decode tables...",
"Connexion PostgreSQL et vérification des tables raw/core/decode...",
"info",
true,
false,
);
if !initialize_schema {
emit_sql_startup_splash(
splash_window,
"Auto-initialisation désactivée : vérification sans création de table.",
"info",
true,
);
}
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization");
let store_result = kb_store::PostgresStore::connect(options).await;
let store = match store_result {
@@ -241,12 +239,14 @@ pub(crate) async fn initialize_postgres_schema_for_startup(
return;
},
};
let initialize_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = initialize_result {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed");
return;
if initialize_schema {
let initialize_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = initialize_result {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed");
return;
}
}
let after_result = store.known_table_diagnostics().await;
let after_tables = match after_result {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_ws.rs
// version: 14
// version: 16
//! Standard Solana WebSocket demo commands backed by `kb_onchain_transport::WsSession`.
@@ -431,17 +431,26 @@ fn spawn_demo_ws_event_bridge(
};
match event {
kb_onchain_transport::WsSessionEvent::Notification { notification, .. } => {
let compact_payload = match serde_json::to_string(&notification) {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(error) => {
format!("cannot serialize typed WebSocket notification: {error}")
},
};
tracing::debug!(
target: crate::TRACING_TARGET,
payload = %compact_payload,
"demo WebSocket notification received"
);
if rate_limiter.should_emit(&app_handle) {
let payload = match serde_json::to_string_pretty(&notification) {
let ui_payload = match serde_json::to_string_pretty(&notification) {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(error) => {
format!("cannot serialize typed WebSocket notification: {error}")
},
std::result::Result::Err(_) => compact_payload,
};
emit_demo_ws_message(
&app_handle,
"notification",
truncate_payload(payload),
truncate_payload(ui_payload),
);
}
},
@@ -640,20 +649,27 @@ fn build_standard_ws_request(
std::result::Result::Ok(target) => target,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let config = match parse_optional_json_as::<
let mut config = match parse_optional_json_as::<
kb_onchain_transport::WsAccountSubscribeConfig,
>(config_json, "configJson")
{
std::result::Result::Ok(config) => config,
std::result::Result::Ok(config) => config.unwrap_or_default(),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if config.encoding.is_none() {
config.encoding =
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64);
}
if let std::result::Result::Err(error) =
reject_present(filter_json, "filterJson", method)
{
return std::result::Result::Err(error);
}
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Account(
kb_onchain_transport::AccountSubscribeRequest { pubkey: target, config },
kb_onchain_transport::AccountSubscribeRequest {
pubkey: target,
config: std::option::Option::Some(config),
},
))
},
"blockSubscribe" => {
@@ -710,15 +726,22 @@ fn build_standard_ws_request(
{
return std::result::Result::Err(error);
}
let config = match parse_optional_json_as::<
let mut config = match parse_optional_json_as::<
kb_onchain_transport::WsProgramSubscribeConfig,
>(config_json, "configJson")
{
std::result::Result::Ok(config) => config,
std::result::Result::Ok(config) => config.unwrap_or_default(),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if config.encoding.is_none() {
config.encoding =
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64);
}
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Program(
kb_onchain_transport::ProgramSubscribeRequest { program_id: target, config },
kb_onchain_transport::ProgramSubscribeRequest {
program_id: target,
config: std::option::Option::Some(config),
},
))
},
"signatureSubscribe" => {
@@ -879,6 +902,42 @@ fn truncate_payload(payload: std::string::String) -> std::string::String {
mod tests {
use ts_rs::TS; // rust-rules: derive-import
#[test]
fn account_and_program_subscriptions_default_to_base64_without_overriding_explicit_encoding() {
let account = super::build_standard_ws_request(
"accountSubscribe",
std::option::Option::Some("11111111111111111111111111111111".to_string()),
std::option::Option::None,
std::option::Option::None,
);
let account = match account {
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Account(value)) => {
value
},
_ => panic!("accountSubscribe request was not built"),
};
assert_eq!(
account.config.and_then(|value| return value.encoding),
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64)
);
let program = super::build_standard_ws_request(
"programSubscribe",
std::option::Option::Some(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string()),
std::option::Option::None,
std::option::Option::Some(r#"{"encoding":"jsonParsed"}"#.to_string()),
);
let program = match program {
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Program(value)) => {
value
},
_ => panic!("programSubscribe request was not built"),
};
assert_eq!(
program.config.and_then(|value| return value.encoding),
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::JsonParsed)
);
}
#[test]
fn tauri_numeric_bindings_use_json_compatible_numbers() {
let config = ts_rs::Config::default();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 9
// version: 10
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -39,6 +39,8 @@ pub(crate) use self::app_state::AppState;
pub(crate) use self::demo_backfill::DemoBackfillObserver;
/// Initial options shown by the backfill window.
pub(crate) use self::demo_backfill::DemoBackfillOptionsPayload;
/// One known program suggested by the backfill Program ID autocomplete.
pub(crate) use self::demo_backfill::DemoBackfillProgramOption;
/// One progress event emitted to the backfill window.
pub(crate) use self::demo_backfill::DemoBackfillProgressPayload;
/// UI request for one bounded backfill campaign.
@@ -327,6 +329,8 @@ pub(crate) use self::splash::SPLASH_MINIMUM_MS;
pub(crate) use self::splash::SplashOrder;
/// Emits one order to the splash frontend.
pub(crate) use self::splash::emit_splash_order;
/// Runs the startup sequence after the splash frontend is ready.
pub(crate) use self::splash::splash_frontend_ready_inner;
/// Waits for the minimum splash duration.
pub(crate) use self::splash::wait_until_minimum;

View File

@@ -1,17 +1,18 @@
// file: kb-app-demo-desktop/src/splash.rs
// version: 3
// version: 4
//! Splash-window payloads and startup sequencing helpers.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: derive-import
/// Minimum splash duration before the main window is shown.
pub(crate) const SPLASH_MINIMUM_MS: u64 = 3100;
pub(crate) const SPLASH_MINIMUM_MS: u64 = 12100;
/// Splash fade duration.
pub(crate) const SPLASH_FADE_MS: u32 = 3000;
pub(crate) const SPLASH_FADE_MS: u32 = 12000;
/// Delay after fade-out before destroying the splash window.
pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 3100;
pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 12100;
/// Command sent from Rust to the splash frontend.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -50,6 +51,96 @@ pub(crate) fn emit_splash_order(
}
}
/// Runs the startup sequence after the splash frontend installed its listener.
pub(crate) async fn splash_frontend_ready_inner(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
state: &crate::AppState,
) -> std::result::Result<(), std::string::String> {
if window.label() != "splash" {
return std::result::Result::Err(
"splash readiness may only originate from splash".to_string(),
);
}
if !state.begin_startup_sequence() {
return std::result::Result::Ok(());
}
let main_window = match app.get_webview_window("main") {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("main window is missing".to_string());
},
};
let started_at = tokio::time::Instant::now();
crate::emit_splash_order(
&window,
"fadein",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Chargement de la configuration terminé."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_log",
std::option::Option::Some(
format!("Profil actif : {}", state.active_profile().name).as_str(),
),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some(
format!("Logging initialisé : {} routes actives.", state.logging_route_count())
.as_str(),
),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Pool HTTP initialisé."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::initialize_postgres_schema_for_startup(state, &window).await;
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Initialisation terminée."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
crate::emit_splash_order(
&window,
"fadeout",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
if let std::result::Result::Err(error) = main_window.show() {
return std::result::Result::Err(format!("cannot show main window: {error:?}"));
}
if let std::result::Result::Err(error) = main_window.set_focus() {
return std::result::Result::Err(format!("cannot focus main window: {error:?}"));
}
if let std::result::Result::Err(error) = window.destroy() {
return std::result::Result::Err(format!("cannot destroy splash window: {error:?}"));
}
return std::result::Result::Ok(());
}
/// Waits until the configured minimum splash duration has elapsed.
pub(crate) async fn wait_until_minimum(start: tokio::time::Instant, minimum_ms: u64) {
let minimum = std::time::Duration::from_millis(minimum_ms);

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 10
// version: 12
//! Tauri runtime assembly and private command wrappers.
@@ -36,6 +36,7 @@ pub fn run() -> kb_core::Result<()> {
builder = builder.manage(app_state);
builder = builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
splash_frontend_ready,
load_project_readme,
open_demo_backfill_window,
demo_backfill_options,
@@ -104,99 +105,18 @@ pub fn run() -> kb_core::Result<()> {
});
builder = builder.plugin(tracing_builder.build::<tauri::Wry>());
builder = builder.setup(|app| {
let splash_window = match app.get_webview_window("splash") {
std::option::Option::Some(window) => window,
std::option::Option::None => {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"splash window is missing",
)));
},
};
let main_window = match app.get_webview_window("main") {
std::option::Option::Some(window) => window,
std::option::Option::None => {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"main window is missing",
)));
},
};
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let started_at = tokio::time::Instant::now();
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Configuration chargée"),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Sous-système de logs initialisé"),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Pool HTTP initialisé"),
std::option::Option::None,
std::option::Option::None,
);
let state = app_handle.state::<crate::AppState>();
crate::initialize_postgres_schema_for_startup(&state, &splash_window).await;
crate::emit_splash_order(
&splash_window,
"fadein",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
crate::emit_splash_order(
&splash_window,
"add_msg",
std::option::Option::Some("Initialisation..."),
std::option::Option::Some("info"),
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_msg",
std::option::Option::Some("Loading complete..."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
if cfg!(debug_assertions) {
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Start Fade-out"),
std::option::Option::None,
std::option::Option::None,
);
}
crate::emit_splash_order(
&splash_window,
"fadeout",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
if let std::result::Result::Err(error) = splash_window.destroy() {
tracing::error!(target: crate::TRACING_TARGET, "cannot destroy splash window: {error:?}");
}
if let std::result::Result::Err(error) = main_window.show() {
tracing::error!(target: crate::TRACING_TARGET, "cannot show main window: {error:?}");
}
if let std::result::Result::Err(error) = main_window.set_focus() {
tracing::error!(target: crate::TRACING_TARGET, "cannot focus main window: {error:?}");
}
});
if app.get_webview_window("splash").is_none() {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"splash window is missing",
)));
}
if app.get_webview_window("main").is_none() {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"main window is missing",
)));
}
return std::result::Result::Ok(());
});
let run_result = builder.run(tauri::generate_context!());
@@ -208,6 +128,15 @@ pub fn run() -> kb_core::Result<()> {
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<(), std::string::String> {
return crate::splash_frontend_ready_inner(app, window, &state).await;
}
fn install_default_rustls_provider() -> kb_core::Result<()> {
if rustls::crypto::CryptoProvider::get_default().is_some() {
return std::result::Result::Ok(());
@@ -759,8 +688,18 @@ fn demo_backfill_options(
} else {
roles.first().map(|item| return item.role.clone())
};
let programs = kb_program_ids::registered_program_ids()
.iter()
.map(|entry| {
return crate::DemoBackfillProgramOption {
code: entry.code().to_string(),
program_id: entry.program_id().to_string(),
};
})
.collect();
return crate::DemoBackfillOptionsPayload {
roles,
programs,
default_role,
default_commitment: "confirmed".to_string(),
default_page_size: 100,

View File

@@ -1,5 +1,5 @@
// file: kb-config/src/settings.rs
// version: 15
// version: 16
//! Typed configuration models shared by applications and workers.
@@ -1349,13 +1349,18 @@ mod tests {
("info.log", "info", "human"),
("error.jsonl", "error", "json"),
] {
let expected_path = format!("logs/{directory}/{crate_name}/{suffix}");
let route_directory = if crate_name == "kb-pipeline-demo-scenarios" {
"kb-pipeline"
} else {
crate_name.as_str()
};
let expected_path = format!("logs/{directory}/{route_directory}/{suffix}");
let route_exists = profile.logging.targets.iter().any(|target| {
return target.enabled
&& target.path == expected_path
&& target.level == level
&& target.format == format
&& target.targets == std::vec![crate_name.clone()];
&& target.targets.iter().any(|value| return value == crate_name);
});
assert!(
route_exists,