diff --git a/Cargo.toml b/Cargo.toml
index 5fb36d1..dee766b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
# file: Cargo.toml
-# version: 4
+# version: 5
[workspace]
resolver = "3"
@@ -14,7 +14,7 @@ members = [
"kb-onchain-transport",
"kb-store",
"kb-wallet",
- "kb-app-demo",
+ "kb-app-demo-desktop",
]
[workspace.package]
diff --git a/config/example.config.json b/config/example.config.json
index 4a87581..f0f0239 100644
--- a/config/example.config.json
+++ b/config/example.config.json
@@ -47,7 +47,8 @@
"kb-onchain-transport",
"kb-wallet",
"kb-pipeline",
- "kb_app_demo",
+ "kb-pipeline-demo-scenarios",
+ "kb-app-demo-desktop",
"kb_executor_metadata_metaplex_token_metadata",
"kb_executor_metadata_spl_name_service",
"kb_executor_spl_account_compression",
@@ -104,7 +105,7 @@
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -147,42 +148,42 @@
]
},
{
- "name": "file_kb_app_demo_debug",
+ "name": "file_kb-app-demo-desktop_debug",
"enabled": true,
"sink": "file",
"level": "debug",
- "path": "logs/devnet/kb_app_demo/debug.log",
+ "path": "logs/devnet/kb-app-demo-desktop/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_info",
+ "name": "file_kb-app-demo-desktop_info",
"enabled": true,
"sink": "file",
"level": "info",
- "path": "logs/devnet/kb_app_demo/info.log",
+ "path": "logs/devnet/kb-app-demo-desktop/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_error",
+ "name": "file_kb-app-demo-desktop_error",
"enabled": true,
"sink": "file",
"level": "error",
- "path": "logs/devnet/kb_app_demo/error.jsonl",
+ "path": "logs/devnet/kb-app-demo-desktop/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -936,7 +937,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -949,7 +951,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -962,7 +965,8 @@
"format": "json",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -1662,7 +1666,7 @@
"format": "compact",
"ansi": true,
"targets": [
- "kb_app_demo",
+ "kb-app-demo-desktop",
"kb-lib.decoder.metadata.metaplex_token_metadata",
"kb-lib.decoder.solana.core",
"kb-lib.decoder.spl.associated_token_account",
@@ -1691,6 +1695,7 @@
"kb-lib.materializer.token",
"kb-lib.materializer.transaction",
"kb-pipeline",
+ "kb-pipeline-demo-scenarios",
"kb-onchain-transport",
"kb-store",
"kb-wallet"
@@ -1745,7 +1750,7 @@
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -1788,42 +1793,42 @@
]
},
{
- "name": "file_kb_app_demo_debug",
+ "name": "file_kb-app-demo-desktop_debug",
"enabled": true,
"sink": "file",
"level": "debug",
- "path": "logs/mainnet_research/kb_app_demo/debug.log",
+ "path": "logs/mainnet_research/kb-app-demo-desktop/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_info",
+ "name": "file_kb-app-demo-desktop_info",
"enabled": true,
"sink": "file",
"level": "info",
- "path": "logs/mainnet_research/kb_app_demo/info.log",
+ "path": "logs/mainnet_research/kb-app-demo-desktop/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_error",
+ "name": "file_kb-app-demo-desktop_error",
"enabled": true,
"sink": "file",
"level": "error",
- "path": "logs/mainnet_research/kb_app_demo/error.jsonl",
+ "path": "logs/mainnet_research/kb-app-demo-desktop/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -2577,7 +2582,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -2590,7 +2596,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -2603,7 +2610,8 @@
"format": "json",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -3503,7 +3511,7 @@
"format": "compact",
"ansi": true,
"targets": [
- "kb_app_demo",
+ "kb-app-demo-desktop",
"kb-lib.decoder.metadata.metaplex_token_metadata",
"kb-lib.decoder.solana.core",
"kb-lib.decoder.spl.associated_token_account",
@@ -3532,6 +3540,7 @@
"kb-lib.materializer.token",
"kb-lib.materializer.transaction",
"kb-pipeline",
+ "kb-pipeline-demo-scenarios",
"kb-onchain-transport",
"kb-store",
"kb-wallet"
@@ -3586,7 +3595,7 @@
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -3629,42 +3638,42 @@
]
},
{
- "name": "file_kb_app_demo_debug",
+ "name": "file_kb-app-demo-desktop_debug",
"enabled": true,
"sink": "file",
"level": "debug",
- "path": "logs/mainnet/kb_app_demo/debug.log",
+ "path": "logs/mainnet/kb-app-demo-desktop/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_info",
+ "name": "file_kb-app-demo-desktop_info",
"enabled": true,
"sink": "file",
"level": "info",
- "path": "logs/mainnet/kb_app_demo/info.log",
+ "path": "logs/mainnet/kb-app-demo-desktop/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
- "name": "file_kb_app_demo_error",
+ "name": "file_kb-app-demo-desktop_error",
"enabled": true,
"sink": "file",
"level": "error",
- "path": "logs/mainnet/kb_app_demo/error.jsonl",
+ "path": "logs/mainnet/kb-app-demo-desktop/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
- "kb_app_demo"
+ "kb-app-demo-desktop"
]
},
{
@@ -4418,7 +4427,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -4431,7 +4441,8 @@
"format": "human",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
@@ -4444,7 +4455,8 @@
"format": "json",
"ansi": false,
"targets": [
- "kb-pipeline"
+ "kb-pipeline",
+ "kb-pipeline-demo-scenarios"
]
},
{
diff --git a/kb-app-demo-desktop/Cargo.toml b/kb-app-demo-desktop/Cargo.toml
new file mode 100644
index 0000000..564488c
--- /dev/null
+++ b/kb-app-demo-desktop/Cargo.toml
@@ -0,0 +1,33 @@
+# file: kb-app-demo-desktop/Cargo.toml
+# version: 1
+
+[package]
+name = "kb-app-demo-desktop"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[lib]
+name = "kb_app_demo_desktop"
+path = "src/lib.rs"
+
+[[bin]]
+name = "kb-app-demo-desktop"
+path = "src/main.rs"
+
+[build-dependencies]
+tauri-build.workspace = true
+
+[dependencies]
+fs2.workspace = true
+kb-core = { path = "../kb-core" }
+serde.workspace = true
+serde_json.workspace = true
+tauri.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+ts-rs.workspace = true
+
+[lints]
+workspace = true
diff --git a/kb-app-demo-desktop/build.rs b/kb-app-demo-desktop/build.rs
new file mode 100644
index 0000000..b44d4c8
--- /dev/null
+++ b/kb-app-demo-desktop/build.rs
@@ -0,0 +1,12 @@
+// file: kb-app-demo-desktop/build.rs
+// version: 1
+
+//! Build script for the desktop demo application.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+
+fn main() {
+ tauri_build::build()
+}
diff --git a/kb-app-demo-desktop/capabilities/default.json b/kb-app-demo-desktop/capabilities/default.json
new file mode 100644
index 0000000..b9e4a9f
--- /dev/null
+++ b/kb-app-demo-desktop/capabilities/default.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Default desktop demo capability",
+ "windows": [
+ "splash",
+ "main"
+ ],
+ "permissions": [
+ "core:default"
+ ]
+}
\ No newline at end of file
diff --git a/kb-app-demo-desktop/frontend/fonts/DOS_Amazigh.ttf b/kb-app-demo-desktop/frontend/fonts/DOS_Amazigh.ttf
new file mode 100644
index 0000000..e558a29
Binary files /dev/null and b/kb-app-demo-desktop/frontend/fonts/DOS_Amazigh.ttf differ
diff --git a/kb-app-demo-desktop/frontend/imgs/logo.png b/kb-app-demo-desktop/frontend/imgs/logo.png
new file mode 100644
index 0000000..94e89ca
Binary files /dev/null and b/kb-app-demo-desktop/frontend/imgs/logo.png differ
diff --git a/kb-app-demo-desktop/frontend/imgs/splash.png b/kb-app-demo-desktop/frontend/imgs/splash.png
new file mode 100644
index 0000000..a27d1eb
Binary files /dev/null and b/kb-app-demo-desktop/frontend/imgs/splash.png differ
diff --git a/kb-app-demo-desktop/frontend/main.html b/kb-app-demo-desktop/frontend/main.html
new file mode 100644
index 0000000..fd5d845
--- /dev/null
+++ b/kb-app-demo-desktop/frontend/main.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ Khadhroony Bot3
+
+
+
+
+ Khadhroony Bot3
+ La fenêtre splash de kb-app-demo-desktop est migrée.
+ La fenêtre principale sera complétée dans la prochaine tranche.
+
+
+
diff --git a/kb-app-demo-desktop/frontend/sass/splash.scss b/kb-app-demo-desktop/frontend/sass/splash.scss
new file mode 100644
index 0000000..a593bb0
--- /dev/null
+++ b/kb-app-demo-desktop/frontend/sass/splash.scss
@@ -0,0 +1,8 @@
+/* file: kb-app-demo-desktop/frontend/sass/splash.scss */
+/* version: 1 */
+html, body { width: 100%; height: 100%; margin: 0; background: transparent; overflow: hidden; }
+body { display: grid; place-items: center; font-family: sans-serif; }
+#splash-container { width: 100%; height: 100%; display: grid; place-content: center; justify-items: center; gap: 1rem; opacity: 0; background: radial-gradient(circle, #1f2937, #030712); color: #f9fafb; }
+#splash-container img { width: min(55vw, 420px); max-height: 55vh; object-fit: contain; }
+#splash-container h1 { margin: 0; letter-spacing: .08em; }
+#messages-container { min-height: 1.5rem; color: #d1d5db; }
diff --git a/kb-app-demo-desktop/frontend/splash.html b/kb-app-demo-desktop/frontend/splash.html
new file mode 100644
index 0000000..70c476a
--- /dev/null
+++ b/kb-app-demo-desktop/frontend/splash.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ Chargement — Khadhroony Bot3
+
+
+
+
+
+ Khadhroony Bot3
+
+
+
+
+
diff --git a/kb-app-demo-desktop/frontend/ts/splash.ts b/kb-app-demo-desktop/frontend/ts/splash.ts
new file mode 100644
index 0000000..40089d9
--- /dev/null
+++ b/kb-app-demo-desktop/frontend/ts/splash.ts
@@ -0,0 +1,40 @@
+// file: kb-app-demo-desktop/frontend/ts/splash.ts
+// version: 1
+
+import { listen } from "@tauri-apps/api/event";
+
+type SplashOrder = {
+ order: string;
+ msg: string | null;
+ status: string | null;
+ duration_ms: number | null;
+};
+
+const container = document.querySelector("#splash-container");
+const messages = document.querySelector("#messages-container");
+
+function duration(order: SplashOrder): number {
+ if (typeof order.duration_ms !== "number" || !Number.isFinite(order.duration_ms) || order.duration_ms <= 0) {
+ return 500;
+ }
+ return order.duration_ms;
+}
+
+async function applyOrder(order: SplashOrder): Promise {
+ if (!container) {
+ return;
+ }
+ if (order.order === "add_msg" && messages && order.msg) {
+ messages.textContent = order.msg;
+ messages.dataset.status = order.status ?? "info";
+ return;
+ }
+ if (order.order === "fadein" || order.order === "fadeout") {
+ container.style.transition = `opacity ${duration(order)}ms ease-in-out`;
+ container.style.opacity = order.order === "fadein" ? "1" : "0";
+ }
+}
+
+void listen("splash", event => {
+ void applyOrder(event.payload);
+});
diff --git a/kb-app-demo-desktop/icons/favicon.ico b/kb-app-demo-desktop/icons/favicon.ico
new file mode 100644
index 0000000..9dfa172
Binary files /dev/null and b/kb-app-demo-desktop/icons/favicon.ico differ
diff --git a/kb-app-demo-desktop/icons/favicon.png b/kb-app-demo-desktop/icons/favicon.png
new file mode 100644
index 0000000..94e89ca
Binary files /dev/null and b/kb-app-demo-desktop/icons/favicon.png differ
diff --git a/kb-app-demo-desktop/package.json b/kb-app-demo-desktop/package.json
new file mode 100644
index 0000000..cd722b4
--- /dev/null
+++ b/kb-app-demo-desktop/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "kb-app-demo-desktop",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "tauri": "tauri"
+ },
+ "dependencies": {
+ "@andypf/json-viewer": "^2.8",
+ "@fltsci/tauri-plugin-tracing": "^0.3",
+ "@fortawesome/fontawesome-free": "^7.2",
+ "@tauri-apps/api": "^2.11",
+ "bootstrap": "^5.3",
+ "datatables.net-bs5": "^2.3",
+ "datatables.net-select-bs5": "^3.1",
+ "echarts": "^6.1",
+ "markdown-it": "^14.2",
+ "resize-observer-polyfill": "^1.5",
+ "simplebar": "^6.3"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.11",
+ "@types/bootstrap": "^5.2",
+ "@types/markdown-it": "^14.1",
+ "@types/node": "^25.9",
+ "sass-embedded": "^1.100",
+ "typescript": "^5.9",
+ "vite": "^8.1"
+ }
+}
\ No newline at end of file
diff --git a/kb-app-demo-desktop/src/constants.rs b/kb-app-demo-desktop/src/constants.rs
new file mode 100644
index 0000000..0f4e5f3
--- /dev/null
+++ b/kb-app-demo-desktop/src/constants.rs
@@ -0,0 +1,7 @@
+// file: kb-app-demo-desktop/src/constants.rs
+// version: 1
+
+//! Constants for the desktop demo application.
+
+/// Canonical tracing target for the desktop demo application.
+pub(crate) const TRACING_TARGET: &str = "kb-app-demo-desktop";
diff --git a/kb-app-demo-desktop/src/lib.rs b/kb-app-demo-desktop/src/lib.rs
new file mode 100644
index 0000000..9cdafa8
--- /dev/null
+++ b/kb-app-demo-desktop/src/lib.rs
@@ -0,0 +1,93 @@
+// file: kb-app-demo-desktop/src/lib.rs
+// version: 1
+
+//! Tauri desktop demo application for `khadhroony-bot3`.
+
+#![forbid(unsafe_code)]
+#![deny(unreachable_pub)]
+#![warn(missing_docs)]
+
+mod constants;
+mod splash;
+
+use tauri::Manager; // rust-rules: trait-import
+
+/// Runs the desktop demo application.
+pub async fn run() -> kb_core::Result<()> {
+ let builder = tauri::Builder::default().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",
+ )));
+ },
+ };
+ tauri::async_runtime::spawn(async move {
+ let started_at = tokio::time::Instant::now();
+ 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 de Khadhroony Bot3..."),
+ std::option::Option::Some("info"),
+ std::option::Option::None,
+ );
+ crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
+ 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:?}");
+ }
+ });
+ return std::result::Result::Ok(());
+ });
+ let run_result = builder.run(tauri::generate_context!());
+ return match run_result {
+ std::result::Result::Ok(()) => std::result::Result::Ok(()),
+ std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::tauri(format!(
+ "cannot run desktop demo application: {error:?}"
+ ))),
+ };
+}
+
+/// Minimum startup splash duration before fade-out begins.
+pub(crate) use self::splash::SPLASH_MINIMUM_MS;
+/// Splash fade duration.
+pub(crate) use self::splash::SPLASH_FADE_MS;
+/// Delay after fade-out before destroying the splash window.
+pub(crate) use self::splash::SPLASH_CLOSE_WAIT_MS;
+/// Emits one order to the splash frontend.
+pub(crate) use self::splash::emit_splash_order;
+/// Waits for the minimum splash duration.
+pub(crate) use self::splash::wait_until_minimum;
+/// Canonical tracing target for the desktop demo application.
+pub(crate) use self::constants::TRACING_TARGET;
diff --git a/kb-app-demo-desktop/src/main.rs b/kb-app-demo-desktop/src/main.rs
new file mode 100644
index 0000000..49be447
--- /dev/null
+++ b/kb-app-demo-desktop/src/main.rs
@@ -0,0 +1,42 @@
+// file: kb-app-demo-desktop/src/main.rs
+// version: 1
+
+//! Binary entry point for the desktop demo 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
+
+/// Starts the single desktop demo application instance.
+#[tokio::main]
+async fn main() -> std::process::ExitCode {
+ let mut lock_path = std::env::temp_dir();
+ lock_path.push("com_sasedev_kb_app_demo_desktop.lock");
+ let lock_file = match std::fs::File::create(&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 kb-app-demo-desktop 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 run_result = kb_app_demo_desktop::run().await;
+ 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
+ },
+ };
+}
diff --git a/kb-app-demo-desktop/src/splash.rs b/kb-app-demo-desktop/src/splash.rs
new file mode 100644
index 0000000..1ae2209
--- /dev/null
+++ b/kb-app-demo-desktop/src/splash.rs
@@ -0,0 +1,84 @@
+// file: kb-app-demo-desktop/src/splash.rs
+// version: 1
+
+//! Splash-window payloads and startup sequencing helpers.
+
+use tauri::Emitter; // 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 = 900;
+/// Splash fade duration.
+pub(crate) const SPLASH_FADE_MS: u32 = 500;
+/// Delay after fade-out before destroying the splash window.
+pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 550;
+
+/// Command sent from Rust to the splash frontend.
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
+#[ts(export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/splash/SplashOrder.ts")]
+pub(crate) struct SplashOrder {
+ /// Command name.
+ pub(crate) order: std::string::String,
+ /// Optional message.
+ pub(crate) msg: std::option::Option,
+ /// Optional status.
+ pub(crate) status: std::option::Option,
+ /// Optional animation duration in milliseconds.
+ pub(crate) duration_ms: std::option::Option,
+}
+
+/// Emits one splash command.
+pub(crate) fn emit_splash_order(
+ splash_window: &tauri::WebviewWindow,
+ order: &str,
+ msg: std::option::Option<&str>,
+ status: std::option::Option<&str>,
+ duration_ms: std::option::Option,
+) {
+ let payload = SplashOrder {
+ order: order.to_string(),
+ msg: msg.map(std::string::ToString::to_string),
+ status: status.map(std::string::ToString::to_string),
+ duration_ms,
+ };
+ let emit_result = splash_window.emit("splash", payload);
+ if let std::result::Result::Err(error) = emit_result {
+ tracing::error!(target: crate::TRACING_TARGET, "cannot emit splash order '{order}': {error:?}");
+ }
+}
+
+/// 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);
+ let elapsed = start.elapsed();
+ if elapsed >= minimum {
+ return;
+ }
+ tokio::time::sleep(minimum - elapsed).await;
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn splash_order_serializes_duration_ms() {
+ let order = super::SplashOrder {
+ order: "fadein".to_string(),
+ msg: std::option::Option::None,
+ status: std::option::Option::None,
+ duration_ms: std::option::Option::Some(500),
+ };
+ let value_result = serde_json::to_value(order);
+ let value = match value_result {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => panic!("cannot serialize splash order: {error}"),
+ };
+ assert_eq!(value["duration_ms"], serde_json::json!(500));
+ }
+
+ #[test]
+ fn splash_timing_is_ordered() {
+ assert!(super::SPLASH_MINIMUM_MS > 0);
+ assert!(super::SPLASH_FADE_MS > 0);
+ assert!(super::SPLASH_CLOSE_WAIT_MS >= u64::from(super::SPLASH_FADE_MS));
+ }
+}
diff --git a/kb-app-demo-desktop/tauri.conf.json b/kb-app-demo-desktop/tauri.conf.json
new file mode 100644
index 0000000..625c955
--- /dev/null
+++ b/kb-app-demo-desktop/tauri.conf.json
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "Khadhroony Bot3 Demo Desktop",
+ "version": "0.1.0",
+ "identifier": "com.sasedev.kb-app-demo-desktop",
+ "build": {
+ "beforeDevCommand": "npm run dev",
+ "devUrl": "http://localhost:1420",
+ "beforeBuildCommand": "npm run build",
+ "frontendDist": "./dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "label": "splash",
+ "url": "splash.html",
+ "title": "Chargement — Khadhroony Bot3",
+ "width": 960,
+ "height": 637,
+ "resizable": false,
+ "decorations": false,
+ "transparent": true,
+ "center": true,
+ "alwaysOnTop": true
+ },
+ {
+ "label": "main",
+ "url": "main.html",
+ "title": "Khadhroony Bot3",
+ "width": 1200,
+ "height": 768,
+ "minWidth": 800,
+ "minHeight": 600,
+ "center": true,
+ "visible": false
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": [
+ "icons/favicon.png",
+ "icons/favicon.ico"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/kb-app-demo-desktop/tsconfig.json b/kb-app-demo-desktop/tsconfig.json
new file mode 100644
index 0000000..016ec89
--- /dev/null
+++ b/kb-app-demo-desktop/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": [
+ "ES2020",
+ "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"
+ ]
+}
\ No newline at end of file
diff --git a/kb-app-demo-desktop/vite.config.ts b/kb-app-demo-desktop/vite.config.ts
new file mode 100644
index 0000000..cbb4a50
--- /dev/null
+++ b/kb-app-demo-desktop/vite.config.ts
@@ -0,0 +1,84 @@
+// file: kb-app-demo-desktop/vite.config.ts
+// version: 2
+
+import { defineConfig, normalizePath } from "vite";
+import { NodePackageImporter } from "sass-embedded";
+import { resolve } from "path";
+
+const host = process.env.TAURI_DEV_HOST;
+
+export default defineConfig(() => ({
+
+ envPrefix: ['VITE_', 'TAURI_ENV_*'],
+ // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
+ //
+ // 1. prevent Vite from obscuring rust errors
+ clearScreen: false,
+ root: 'frontend', // Set this to your frontend directory
+ publicDir: 'public',
+ build: {
+ outDir: './dist', // Output directory for the build
+ emptyOutDir: true,
+ rollupOptions: {
+ input: {
+ "splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
+ "main": normalizePath(resolve(__dirname, 'frontend/main.html'))
+ },
+ output: {
+ entryFileNames: 'js/[name]-[hash].js',
+ chunkFileNames: 'js/chunks/[name]-[hash].js',
+ assetFileNames: (assetInfo) => {
+ const originalName = assetInfo.name ?? '';
+ const ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
+ if (ext === 'js') {
+ return 'js/[name]-[hash][extname]';
+ }
+ // CSS
+ if (ext === 'css') {
+ return 'css/[name]-[hash][extname]';
+ }
+ if (['eot', 'otf', 'ttf', 'woff', 'woff2'].includes(ext)) {
+ return 'fonts/[name]-[hash][extname]';
+ }
+ if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'].includes(ext)) {
+ return 'imgs/[name][extname]';
+ }
+ if (['mp4', 'webm'].includes(ext)) {
+ return 'videos/[name][extname]';
+ }
+ return 'otherassets/[name][extname]';
+ },
+ },
+ },
+ minify: true,
+ sourcemap: false,
+ cssCodeSplit: true
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ quietDeps: true,
+ silenceDeprecations: ["import", "color-functions", "global-builtin",],
+ verbose: false,
+ api: 'modern',
+ importers: [new NodePackageImporter()],
+ }
+ }
+ },
+ // 2. tauri expects a fixed port, fail if that port is not available
+ server: {
+ port: 1420,
+ strictPort: false,
+ host: host || false,
+ hmr: host
+ ? {
+ protocol: "ws",
+ host,
+ port: 1421,
+ }
+ : undefined,
+ watch: {
+ ignored: ["**/src/**"],
+ },
+ },
+}));
diff --git a/kb-app-demo/Cargo.toml b/kb-app-demo/Cargo.toml
deleted file mode 100644
index 8c0e2c4..0000000
--- a/kb-app-demo/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-# file: kb-app-demo/Cargo.toml
-# version: 2
-
-[package]
-name = "kb-app-demo"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-publish.workspace = true
-
-[dependencies]
-kb-config = { path = "../kb-config" }
-kb-core = { path = "../kb-core" }
-kb-lib = { path = "../kb-lib" }
-kb-logging = { path = "../kb-logging" }
-kb-pipeline = { path = "../kb-pipeline" }
-kb-program-ids = { path = "../kb-program-ids" }
-kb-onchain-transport = { path = "../kb-onchain-transport" }
-kb-store = { path = "../kb-store" }
-kb-wallet = { path = "../kb-wallet" }
-
-[lints]
-workspace = true
diff --git a/kb-app-demo/src/main.rs b/kb-app-demo/src/main.rs
deleted file mode 100644
index e16b3e8..0000000
--- a/kb-app-demo/src/main.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-// file: kb-app-demo/src/main.rs
-// version: 2
-
-#![forbid(unsafe_code)]
-#![deny(unreachable_pub)]
-#![warn(missing_docs)]
-
-//! Minimal migration smoke-test application.
-
-fn main() {
- println!("khadhroony-bot3 workspace scaffold");
- println!("logging target: {}", kb_logging::tracing_target());
- println!(
- "pipeline: {} v{}",
- kb_pipeline::CORE_EXTRACTION_PROCESSOR_NAME,
- kb_pipeline::CORE_EXTRACTION_PROCESSOR_VERSION
- );
-}
diff --git a/kb-config/src/settings.rs b/kb-config/src/settings.rs
index 5152523..3f69533 100644
--- a/kb-config/src/settings.rs
+++ b/kb-config/src/settings.rs
@@ -1319,7 +1319,7 @@ mod tests {
&& target.path == format!("logs/{directory}/app.log")
&& target.level == "debug"
&& target.format == "human"
- && target.targets == std::vec!["kb_app_demo".to_string()];
+ && target.targets == std::vec!["kb-app-demo-desktop".to_string()];
}));
for crate_name in &operational_crates {
for (suffix, level, format) in [