v0.1.0-pre.050
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/app_state.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Shared Tauri application state and startup initialization.
|
||||
|
||||
@@ -10,7 +10,7 @@ pub(crate) struct AppState {
|
||||
active_profile: kb_config::ProfileConfig,
|
||||
logging_guard: std::sync::Mutex<kb_logging::LoggingGuard>,
|
||||
http_pool: kb_onchain_transport::HttpEndpointPool,
|
||||
ws_pool: kb_onchain_transport::WsEndpointPool,
|
||||
ws_pool: std::sync::Mutex<std::option::Option<std::sync::Arc<kb_onchain_transport::WsEndpointPool>>>,
|
||||
demo_ws_session: tokio::sync::Mutex<std::option::Option<std::sync::Arc<kb_onchain_transport::WsSession>>>,
|
||||
demo_backfill_running: std::sync::atomic::AtomicBool,
|
||||
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
@@ -37,17 +37,13 @@ impl crate::AppState {
|
||||
std::result::Result::Ok(pool) => pool,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let ws_pool = match kb_onchain_transport::WsEndpointPool::from_profile(&active_profile) {
|
||||
std::result::Result::Ok(pool) => pool,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::AppState {
|
||||
config_path: config_path.display().to_string(),
|
||||
app_config,
|
||||
active_profile,
|
||||
logging_guard: std::sync::Mutex::new(logging_guard),
|
||||
http_pool,
|
||||
ws_pool,
|
||||
ws_pool: std::sync::Mutex::new(std::option::Option::None),
|
||||
demo_ws_session: tokio::sync::Mutex::new(std::option::Option::None),
|
||||
demo_backfill_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
@@ -74,9 +70,28 @@ impl crate::AppState {
|
||||
return &self.http_pool;
|
||||
}
|
||||
|
||||
/// Returns the configured WebSocket endpoint pool.
|
||||
pub(crate) fn ws_pool(&self) -> &kb_onchain_transport::WsEndpointPool {
|
||||
return &self.ws_pool;
|
||||
/// Returns the lazily initialized WebSocket endpoint pool.
|
||||
pub(crate) fn demo_ws_pool(
|
||||
&self,
|
||||
) -> kb_core::Result<std::sync::Arc<kb_onchain_transport::WsEndpointPool>> {
|
||||
let lock_result = self.ws_pool.lock();
|
||||
let mut guard = match lock_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"demo WebSocket pool lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::option::Option::Some(pool) = guard.as_ref() {
|
||||
return std::result::Result::Ok(std::sync::Arc::clone(pool));
|
||||
}
|
||||
let pool = match kb_onchain_transport::WsEndpointPool::from_profile(&self.active_profile) {
|
||||
std::result::Result::Ok(value) => std::sync::Arc::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
*guard = std::option::Option::Some(std::sync::Arc::clone(&pool));
|
||||
return std::result::Result::Ok(pool);
|
||||
}
|
||||
|
||||
/// Returns the persistent WebSocket demo session slot.
|
||||
|
||||
363
kb-app-demo-desktop/src/demo_sql_common.rs
Normal file
363
kb-app-demo-desktop/src/demo_sql_common.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
// file: kb-app-demo-desktop/src/demo_sql_common.rs
|
||||
// version: 8
|
||||
|
||||
//! Shared SQL demo helpers and serializable payloads.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// UI-safe table diagnostics shown by SQL demo windows.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlTableSnapshot {
|
||||
/// Table name.
|
||||
pub(crate) table_name: std::string::String,
|
||||
/// Logical Solana domain encoded in the table name.
|
||||
pub(crate) domain: std::string::String,
|
||||
/// Human-readable table role.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Whether the table currently exists.
|
||||
pub(crate) exists: bool,
|
||||
/// Row count when the table exists.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) row_count: std::option::Option<i64>,
|
||||
/// Minimum slot when available.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) min_slot: std::option::Option<i64>,
|
||||
/// Maximum slot when available.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) max_slot: std::option::Option<i64>,
|
||||
/// Latest insertion timestamp rendered by PostgreSQL when available.
|
||||
pub(crate) latest_created_at: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Opens or focuses a SQL demo window.
|
||||
pub(crate) fn open_sql_demo_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
label: &str,
|
||||
html_path: &str,
|
||||
title: &str,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
tracing::info!(target: crate::TRACING_TARGET, window = label, "open SQL demo window");
|
||||
let existing_window = app_handle.get_webview_window(label);
|
||||
if let std::option::Option::Some(window) = existing_window {
|
||||
let show_result = window.show();
|
||||
if let std::result::Result::Err(error) = show_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot show {label} window: {error}")).to_string(),
|
||||
);
|
||||
}
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot focus {label} window: {error}")).to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let builder = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
label,
|
||||
tauri::WebviewUrl::App(html_path.into()),
|
||||
)
|
||||
.title(title)
|
||||
.inner_size(1220.0, 780.0)
|
||||
.min_inner_size(920.0, 560.0)
|
||||
.resizable(true)
|
||||
.visible(true);
|
||||
let build_result = builder.build();
|
||||
return match build_result {
|
||||
std::result::Result::Ok(window) => {
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot focus created {label} window: {error}"))
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot create {label} window: {error}")).to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Builds PostgreSQL options from the active profile without coupling kb-store to kb-config.
|
||||
pub(crate) fn postgres_store_options_from_profile(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
auto_initialize_schema: bool,
|
||||
) -> kb_core::Result<kb_store::PostgresStoreOptions> {
|
||||
return kb_store::PostgresStoreOptions::new(
|
||||
profile.database.postgres.url.clone(),
|
||||
profile.database.postgres.max_connections,
|
||||
profile.database.postgres.connect_timeout_ms,
|
||||
auto_initialize_schema,
|
||||
);
|
||||
}
|
||||
|
||||
/// Connects to PostgreSQL without reapplying schemas initialized at application startup.
|
||||
pub(crate) async fn connect_postgres_store(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
) -> std::result::Result<kb_store::PostgresStore, std::string::String> {
|
||||
if !profile.database.enabled {
|
||||
return std::result::Result::Err(std::string::String::from(
|
||||
"database configuration is disabled in the active profile",
|
||||
));
|
||||
}
|
||||
let options_result = postgres_store_options_from_profile(profile, false);
|
||||
let mut options = match options_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
options.auto_initialize_schema = false;
|
||||
let store_result = kb_store::PostgresStore::connect(options).await;
|
||||
return match store_result {
|
||||
std::result::Result::Ok(store) => std::result::Result::Ok(store),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts store table diagnostics to the UI payload shape.
|
||||
pub(crate) fn table_snapshot_from_pg(
|
||||
value: &kb_store::PostgresTableDiagnostics,
|
||||
) -> crate::DemoSqlTableSnapshot {
|
||||
let row_count = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let min_slot = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.min_slot,
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let max_slot = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.max_slot,
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let latest_created_at = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.latest_created_at.clone(),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
return crate::DemoSqlTableSnapshot {
|
||||
table_name: value.table_name.clone(),
|
||||
domain: value.domain.clone(),
|
||||
role: value.role.clone(),
|
||||
exists: value.exists,
|
||||
row_count,
|
||||
min_slot,
|
||||
max_slot,
|
||||
latest_created_at,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts many table diagnostics to UI payloads.
|
||||
pub(crate) fn table_snapshots_from_pg(
|
||||
values: &[kb_store::PostgresTableDiagnostics],
|
||||
) -> std::vec::Vec<crate::DemoSqlTableSnapshot> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
for value in values {
|
||||
output.push(table_snapshot_from_pg(value));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup.
|
||||
pub(crate) async fn initialize_postgres_schema_for_startup(
|
||||
state: &crate::AppState,
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
) {
|
||||
let profile = state.active_profile();
|
||||
if !profile.database.enabled {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"SQL store disabled; schema initialization skipped.",
|
||||
"info",
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization");
|
||||
return;
|
||||
}
|
||||
if profile.database.backend != "postgres" {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"SQL backend is not PostgreSQL; schema initialization skipped.",
|
||||
"info",
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization");
|
||||
return;
|
||||
}
|
||||
let options_result = postgres_store_options_from_profile(profile, false);
|
||||
let mut options = match options_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
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(), "cannot build PostgreSQL store options");
|
||||
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 masked_dsn = options.masked_dsn();
|
||||
options.auto_initialize_schema = false;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"Checking PostgreSQL raw/core/decode tables...",
|
||||
"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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
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 connection failed during startup schema initialization");
|
||||
return;
|
||||
},
|
||||
};
|
||||
let before_result = store.known_table_diagnostics().await;
|
||||
let before_tables = match before_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
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(), "cannot read table diagnostics before schema initialization");
|
||||
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;
|
||||
}
|
||||
let after_result = store.known_table_diagnostics().await;
|
||||
let after_tables = match after_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
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(), "cannot read table diagnostics after schema initialization");
|
||||
return;
|
||||
},
|
||||
};
|
||||
emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice());
|
||||
}
|
||||
|
||||
fn emit_sql_startup_table_report(
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
before_tables: &[kb_store::PostgresTableDiagnostics],
|
||||
after_tables: &[kb_store::PostgresTableDiagnostics],
|
||||
) {
|
||||
let mut created_count = 0_u32;
|
||||
let mut existing_count = 0_u32;
|
||||
let mut missing_count = 0_u32;
|
||||
for table in after_tables {
|
||||
let before_exists = table_exists_in(before_tables, table.table_name.as_str());
|
||||
if table.exists && before_exists == std::option::Option::Some(false) {
|
||||
created_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Created table {}", table.table_name).as_str(),
|
||||
"success",
|
||||
true,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created");
|
||||
} else if table.exists {
|
||||
existing_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Table {} already exists", table.table_name).as_str(),
|
||||
"info",
|
||||
true,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists");
|
||||
} else {
|
||||
missing_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Table {} is missing after initialization", table.table_name).as_str(),
|
||||
"warning",
|
||||
true,
|
||||
);
|
||||
tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization");
|
||||
}
|
||||
}
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!(
|
||||
"PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing."
|
||||
)
|
||||
.as_str(),
|
||||
schema_report_status(missing_count),
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed");
|
||||
}
|
||||
|
||||
fn table_exists_in(
|
||||
tables: &[kb_store::PostgresTableDiagnostics],
|
||||
table_name: &str,
|
||||
) -> std::option::Option<bool> {
|
||||
for table in tables {
|
||||
if table.table_name.as_str() == table_name {
|
||||
return std::option::Option::Some(table.exists);
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn schema_report_status(missing_count: u32) -> &'static str {
|
||||
if missing_count == 0 {
|
||||
return "success";
|
||||
}
|
||||
return "warning";
|
||||
}
|
||||
|
||||
fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("PostgreSQL schema initialization error: {message}").as_str(),
|
||||
"danger",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_sql_startup_splash(
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
message: &str,
|
||||
status: &str,
|
||||
log_only: bool,
|
||||
) {
|
||||
let order = if log_only { "add_log" } else { "add_msg" };
|
||||
crate::emit_splash_order(
|
||||
splash_window,
|
||||
order,
|
||||
std::option::Option::Some(message),
|
||||
std::option::Option::Some(status),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats debug enums for UI display.
|
||||
pub(crate) fn debug_status<T: std::fmt::Debug>(value: T) -> std::string::String {
|
||||
return format!("{value:?}");
|
||||
}
|
||||
38
kb-app-demo-desktop/src/demo_sql_diag.rs
Normal file
38
kb-app-demo-desktop/src/demo_sql_diag.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// file: kb-app-demo-desktop/src/demo_sql_diag.rs
|
||||
// version: 3
|
||||
|
||||
//! SQL diagnostic demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Complete SQL diagnostic payload shown by `demo_sql_diag`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlDiagPayload {
|
||||
/// Configuration file path.
|
||||
pub(crate) config_path: std::string::String,
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Configured database backend.
|
||||
pub(crate) backend: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// PostgreSQL current schema.
|
||||
pub(crate) current_schema: std::option::Option<std::string::String>,
|
||||
/// PostgreSQL health status.
|
||||
pub(crate) health_status: std::string::String,
|
||||
/// PostgreSQL health message.
|
||||
pub(crate) health_message: std::option::Option<std::string::String>,
|
||||
/// Migration status.
|
||||
pub(crate) migration_status: std::string::String,
|
||||
/// Migration diagnostic message.
|
||||
pub(crate) migration_message: std::option::Option<std::string::String>,
|
||||
/// PostgreSQL server version when available.
|
||||
pub(crate) server_version: std::option::Option<std::string::String>,
|
||||
/// Known raw/core table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
22
kb-app-demo-desktop/src/demo_sql_pg_core.rs
Normal file
22
kb-app-demo-desktop/src/demo_sql_pg_core.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// file: kb-app-demo-desktop/src/demo_sql_pg_core.rs
|
||||
// version: 4
|
||||
|
||||
//! PostgreSQL core store demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// PostgreSQL core store payload shown by `demo_sql_pg_core`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlPgCorePayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Core table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
22
kb-app-demo-desktop/src/demo_sql_pg_raw.rs
Normal file
22
kb-app-demo-desktop/src/demo_sql_pg_raw.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// file: kb-app-demo-desktop/src/demo_sql_pg_raw.rs
|
||||
// version: 5
|
||||
|
||||
//! PostgreSQL canonical acquisition store demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlPgRawPayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Raw table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
409
kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
Normal file
409
kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
|
||||
// version: 7
|
||||
|
||||
//! Read-only SQL replay candidate browser commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One program identifier exposed by the runtime `kb_program_ids` registry.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayKnownProgramOption {
|
||||
/// Stable lower snake case program code.
|
||||
pub(crate) code: std::string::String,
|
||||
/// Base58 Solana program identifier.
|
||||
pub(crate) program_id: std::string::String,
|
||||
}
|
||||
|
||||
/// Static options for the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayOptionsPayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Maximum rows accepted by one query.
|
||||
pub(crate) maximum_limit: u32,
|
||||
/// Program identifiers enumerable from `kb_program_ids`.
|
||||
pub(crate) known_programs: std::vec::Vec<crate::DemoSqlReplayKnownProgramOption>,
|
||||
}
|
||||
|
||||
/// UI request for bounded transaction replay candidates.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayTransactionRequest {
|
||||
/// Optional partial signature search.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional inclusive minimum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) min_slot: std::option::Option<u64>,
|
||||
/// Optional inclusive maximum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) max_slot: std::option::Option<u64>,
|
||||
/// Optional raw processing state.
|
||||
pub(crate) raw_processing_state: std::option::Option<std::string::String>,
|
||||
/// Optional latest ledger status.
|
||||
pub(crate) ledger_status: std::option::Option<std::string::String>,
|
||||
/// Optional exact program id.
|
||||
pub(crate) program_id: std::option::Option<std::string::String>,
|
||||
/// Program scope code: any, outer, inner or logs.
|
||||
pub(crate) program_scope: std::string::String,
|
||||
/// Optional entity kind: mint, owner or account_key.
|
||||
pub(crate) entity_kind: std::option::Option<std::string::String>,
|
||||
/// Optional exact entity value.
|
||||
pub(crate) entity_value: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
/// Orders newest slots first when true.
|
||||
pub(crate) newest_first: bool,
|
||||
}
|
||||
|
||||
/// One transaction row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayTransactionRow {
|
||||
/// Canonical transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Transaction slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) slot: i64,
|
||||
/// Current raw processing state.
|
||||
pub(crate) raw_processing_state: std::string::String,
|
||||
/// Current raw retention state.
|
||||
pub(crate) retention_state: std::string::String,
|
||||
/// Whether a core transaction exists.
|
||||
pub(crate) has_core_transaction: bool,
|
||||
/// Core transaction failure flag when available.
|
||||
pub(crate) transaction_failed: std::option::Option<bool>,
|
||||
/// Latest core extraction ledger status.
|
||||
pub(crate) ledger_status: std::string::String,
|
||||
/// Latest processor version when available.
|
||||
pub(crate) processor_version: std::option::Option<std::string::String>,
|
||||
/// Latest attempt count.
|
||||
pub(crate) attempt_count: i32,
|
||||
/// Number of top-level instructions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_instruction_count: i64,
|
||||
/// Number of inner instructions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_instruction_count: i64,
|
||||
/// Number of distinct top-level programs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_program_count: i64,
|
||||
/// Number of distinct inner programs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_program_count: i64,
|
||||
/// Raw row update timestamp.
|
||||
pub(crate) updated_at: std::string::String,
|
||||
}
|
||||
|
||||
/// UI request for bounded program summaries.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayProgramRequest {
|
||||
/// Optional partial program id search.
|
||||
pub(crate) program_id_contains: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// Aggregated program row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayProgramRow {
|
||||
/// Optional stable code from `kb_program_ids`.
|
||||
pub(crate) program_code: std::option::Option<std::string::String>,
|
||||
/// Program id.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Number of distinct transactions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) transaction_count: i64,
|
||||
/// Number of top-level instruction occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_instruction_count: i64,
|
||||
/// Number of inner instruction occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_instruction_count: i64,
|
||||
/// Number of reliably linked log occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) log_count: i64,
|
||||
/// Lowest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) min_slot: i64,
|
||||
/// Highest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_slot: i64,
|
||||
}
|
||||
|
||||
/// UI request for bounded core entity summaries.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayEntityRequest {
|
||||
/// Entity kind: mint, owner or account_key.
|
||||
pub(crate) entity_kind: std::string::String,
|
||||
/// Optional partial entity value search.
|
||||
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// Aggregated core entity row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayEntityRow {
|
||||
/// Stable entity kind code.
|
||||
pub(crate) entity_kind: std::string::String,
|
||||
/// Mint, owner or account-key address.
|
||||
pub(crate) entity_value: std::string::String,
|
||||
/// Number of distinct transactions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) transaction_count: i64,
|
||||
/// Total number of core-table occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) occurrence_count: i64,
|
||||
/// Lowest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) min_slot: i64,
|
||||
/// Highest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_slot: i64,
|
||||
}
|
||||
|
||||
pub(crate) fn transaction_row_from_pg(
|
||||
row: kb_store::PostgresReplayTransactionCandidate,
|
||||
) -> crate::DemoSqlReplayTransactionRow {
|
||||
return crate::DemoSqlReplayTransactionRow {
|
||||
signature: row.signature,
|
||||
slot: row.slot,
|
||||
raw_processing_state: row.raw_processing_state,
|
||||
retention_state: row.retention_state,
|
||||
has_core_transaction: row.has_core_transaction,
|
||||
transaction_failed: row.transaction_failed,
|
||||
ledger_status: row.ledger_status,
|
||||
processor_version: row.processor_version,
|
||||
attempt_count: row.attempt_count,
|
||||
outer_instruction_count: row.outer_instruction_count,
|
||||
inner_instruction_count: row.inner_instruction_count,
|
||||
outer_program_count: row.outer_program_count,
|
||||
inner_program_count: row.inner_program_count,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn program_row_from_pg(
|
||||
row: kb_store::PostgresReplayProgramSummary,
|
||||
) -> crate::DemoSqlReplayProgramRow {
|
||||
let program_code = kb_program_ids::find_registered_program_id(&row.program_id)
|
||||
.map(|entry| return entry.code().to_owned());
|
||||
return crate::DemoSqlReplayProgramRow {
|
||||
program_code,
|
||||
program_id: row.program_id,
|
||||
transaction_count: row.transaction_count,
|
||||
outer_instruction_count: row.outer_instruction_count,
|
||||
inner_instruction_count: row.inner_instruction_count,
|
||||
log_count: row.log_count,
|
||||
min_slot: row.min_slot,
|
||||
max_slot: row.max_slot,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn entity_row_from_pg(
|
||||
row: kb_store::PostgresReplayEntitySummary,
|
||||
) -> crate::DemoSqlReplayEntityRow {
|
||||
return crate::DemoSqlReplayEntityRow {
|
||||
entity_kind: row.entity_kind,
|
||||
entity_value: row.entity_value,
|
||||
transaction_count: row.transaction_count,
|
||||
occurrence_count: row.occurrence_count,
|
||||
min_slot: row.min_slot,
|
||||
max_slot: row.max_slot,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn validated_csv_file_name(
|
||||
file_name: &str,
|
||||
) -> std::result::Result<&'static str, std::string::String> {
|
||||
return match file_name {
|
||||
"replay_transactions.csv" => std::result::Result::Ok("replay_transactions.csv"),
|
||||
"replay_programs.csv" => std::result::Result::Ok("replay_programs.csv"),
|
||||
"replay_mints.csv" => std::result::Result::Ok("replay_mints.csv"),
|
||||
"replay_owners.csv" => std::result::Result::Ok("replay_owners.csv"),
|
||||
"replay_account_keys.csv" => std::result::Result::Ok("replay_account_keys.csv"),
|
||||
_ => std::result::Result::Err(format!("unsupported replay CSV file name: {file_name}")),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn csv_export_directory_from_current_dir(
|
||||
current_dir: &std::path::Path,
|
||||
) -> std::path::PathBuf {
|
||||
let current_name = current_dir.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
if let (std::option::Option::Some("kb_app_demo_desktop"), std::option::Option::Some(parent)) =
|
||||
(current_name, current_dir.parent())
|
||||
{
|
||||
return parent.join("data").join("exports_csv");
|
||||
}
|
||||
return current_dir.join("data").join("exports_csv");
|
||||
}
|
||||
|
||||
pub(crate) async fn available_csv_export_path(
|
||||
export_dir: &std::path::Path,
|
||||
file_name: &str,
|
||||
) -> std::result::Result<std::path::PathBuf, std::string::String> {
|
||||
let direct_path = export_dir.join(file_name);
|
||||
let direct_exists_result = tokio::fs::try_exists(&direct_path).await;
|
||||
let direct_exists = match direct_exists_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("cannot inspect CSV export path: {error}"));
|
||||
},
|
||||
};
|
||||
if !direct_exists {
|
||||
return std::result::Result::Ok(direct_path);
|
||||
}
|
||||
let stem = file_name.trim_end_matches(".csv");
|
||||
for suffix in 1_u16..=999_u16 {
|
||||
let candidate = export_dir.join(format!("{stem}_{suffix}.csv"));
|
||||
let exists_result = tokio::fs::try_exists(&candidate).await;
|
||||
let exists = match exists_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"cannot inspect CSV export path: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
if !exists {
|
||||
return std::result::Result::Ok(candidate);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err("cannot allocate a unique CSV export path".to_owned());
|
||||
}
|
||||
|
||||
pub(crate) fn program_scope_from_code(
|
||||
code: &str,
|
||||
) -> std::result::Result<kb_store::PostgresReplayProgramScope, std::string::String> {
|
||||
return match code {
|
||||
"any" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Any),
|
||||
"outer" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Outer),
|
||||
"inner" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Inner),
|
||||
"logs" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs),
|
||||
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn optional_entity_kind_from_code(
|
||||
code: std::option::Option<&str>,
|
||||
) -> std::result::Result<
|
||||
std::option::Option<kb_store::PostgresReplayEntityKind>,
|
||||
std::string::String,
|
||||
> {
|
||||
return match code {
|
||||
std::option::Option::Some(value) => {
|
||||
let result = entity_kind_from_code(value);
|
||||
match result {
|
||||
std::result::Result::Ok(kind) => {
|
||||
std::result::Result::Ok(std::option::Option::Some(kind))
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn entity_kind_from_code(
|
||||
code: &str,
|
||||
) -> std::result::Result<kb_store::PostgresReplayEntityKind, std::string::String> {
|
||||
return match code {
|
||||
"mint" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Mint),
|
||||
"owner" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Owner),
|
||||
"account_key" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::AccountKey),
|
||||
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn program_scope_parser_accepts_logs() {
|
||||
let result = crate::program_scope_from_code("logs");
|
||||
assert_eq!(result, std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_kind_parser_rejects_unknown_code() {
|
||||
let result = crate::entity_kind_from_code("unknown");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_file_name_rejects_unknown_name() {
|
||||
let result = crate::validated_csv_file_name("arbitrary.csv");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_export_directory_uses_workspace_root_from_demo_crate() {
|
||||
let current_dir = std::path::Path::new("/tmp/khadhroony-bot2/kb_app_demo_desktop");
|
||||
let result = crate::csv_export_directory_from_current_dir(current_dir);
|
||||
assert_eq!(result, std::path::Path::new("/tmp/khadhroony-bot2/data/exports_csv"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_file_name_accepts_split_entity_exports() {
|
||||
let mint_result = crate::validated_csv_file_name("replay_mints.csv");
|
||||
let owner_result = crate::validated_csv_file_name("replay_owners.csv");
|
||||
let account_result = crate::validated_csv_file_name("replay_account_keys.csv");
|
||||
assert!(mint_result.is_ok());
|
||||
assert!(owner_result.is_ok());
|
||||
assert!(account_result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_row_exposes_registered_code() {
|
||||
let row = kb_store::PostgresReplayProgramSummary {
|
||||
program_id: kb_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
|
||||
transaction_count: 1,
|
||||
outer_instruction_count: 1,
|
||||
inner_instruction_count: 0,
|
||||
log_count: 1,
|
||||
min_slot: 1,
|
||||
max_slot: 1,
|
||||
};
|
||||
let converted = crate::program_row_from_pg(row);
|
||||
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,11 @@ pub(crate) async fn demo_ws_connect_inner(
|
||||
std::result::Result::Ok(request) => request,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let selected_client = match state.ws_pool().select_client_for_role_and_method(&role, &method) {
|
||||
let pool = match state.demo_ws_pool() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let selected_client = match pool.select_client_for_role_and_method(&role, &method) {
|
||||
std::result::Result::Ok(client) => client,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
|
||||
@@ -12,6 +12,11 @@ mod constants;
|
||||
mod demo_backfill;
|
||||
mod demo_http;
|
||||
mod demo_ws;
|
||||
mod demo_sql_common;
|
||||
mod demo_sql_diag;
|
||||
mod demo_sql_pg_core;
|
||||
mod demo_sql_pg_raw;
|
||||
mod demo_sql_replay_candidates;
|
||||
mod frontend_log;
|
||||
mod main_window;
|
||||
mod splash;
|
||||
@@ -90,6 +95,60 @@ pub(crate) use self::demo_ws::demo_ws_status_inner;
|
||||
pub(crate) use self::demo_ws::demo_ws_unsubscribe_inner;
|
||||
/// Disconnects the application WebSocket session during shutdown.
|
||||
pub(crate) use self::demo_ws::disconnect_demo_ws_app_state;
|
||||
/// UI-safe SQL table diagnostic snapshot.
|
||||
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
|
||||
/// Connects to the configured PostgreSQL store.
|
||||
pub(crate) use self::demo_sql_common::connect_postgres_store;
|
||||
/// Formats diagnostic statuses.
|
||||
pub(crate) use self::demo_sql_common::debug_status;
|
||||
/// Opens or focuses one SQL demo window.
|
||||
pub(crate) use self::demo_sql_common::open_sql_demo_window;
|
||||
/// Builds PostgreSQL options from the active profile.
|
||||
pub(crate) use self::demo_sql_common::postgres_store_options_from_profile;
|
||||
/// Converts one PostgreSQL table diagnostic.
|
||||
pub(crate) use self::demo_sql_common::table_snapshot_from_pg;
|
||||
/// Converts PostgreSQL table diagnostics.
|
||||
pub(crate) use self::demo_sql_common::table_snapshots_from_pg;
|
||||
/// SQL diagnostic payload.
|
||||
pub(crate) use self::demo_sql_diag::DemoSqlDiagPayload;
|
||||
/// PostgreSQL core-table payload.
|
||||
pub(crate) use self::demo_sql_pg_core::DemoSqlPgCorePayload;
|
||||
/// PostgreSQL raw-table payload.
|
||||
pub(crate) use self::demo_sql_pg_raw::DemoSqlPgRawPayload;
|
||||
/// SQL replay entity request.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRequest;
|
||||
/// SQL replay entity row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRow;
|
||||
/// Known program option.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption;
|
||||
/// SQL replay options payload.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload;
|
||||
/// SQL replay program request.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRequest;
|
||||
/// SQL replay program row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRow;
|
||||
/// SQL replay transaction request.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest;
|
||||
/// SQL replay transaction row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRow;
|
||||
/// Resolves an available CSV path.
|
||||
pub(crate) use self::demo_sql_replay_candidates::available_csv_export_path;
|
||||
/// Resolves the CSV export directory.
|
||||
pub(crate) use self::demo_sql_replay_candidates::csv_export_directory_from_current_dir;
|
||||
/// Parses an entity-kind code.
|
||||
pub(crate) use self::demo_sql_replay_candidates::entity_kind_from_code;
|
||||
/// Converts a PostgreSQL entity row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::entity_row_from_pg;
|
||||
/// Parses an optional entity-kind code.
|
||||
pub(crate) use self::demo_sql_replay_candidates::optional_entity_kind_from_code;
|
||||
/// Converts a PostgreSQL program row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::program_row_from_pg;
|
||||
/// Parses a program-scope code.
|
||||
pub(crate) use self::demo_sql_replay_candidates::program_scope_from_code;
|
||||
/// Converts a PostgreSQL transaction row.
|
||||
pub(crate) use self::demo_sql_replay_candidates::transaction_row_from_pg;
|
||||
/// Validates a CSV filename.
|
||||
pub(crate) use self::demo_sql_replay_candidates::validated_csv_file_name;
|
||||
/// Frontend logging payload.
|
||||
pub(crate) use self::frontend_log::FrontendLogPayload;
|
||||
/// Emits one normalized frontend log event.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -52,8 +52,33 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_ws_connect,
|
||||
demo_ws_unsubscribe,
|
||||
demo_ws_disconnect,
|
||||
open_demo_sql_diag_window,
|
||||
load_demo_sql_diag,
|
||||
open_demo_sql_pg_raw_window,
|
||||
load_demo_sql_pg_raw,
|
||||
open_demo_sql_pg_core_window,
|
||||
load_demo_sql_pg_core,
|
||||
open_demo_sql_replay_candidates_window,
|
||||
demo_sql_replay_options,
|
||||
load_demo_sql_replay_transactions,
|
||||
load_demo_sql_replay_programs,
|
||||
load_demo_sql_replay_entities,
|
||||
export_demo_sql_replay_csv,
|
||||
]);
|
||||
builder = builder.plugin(tracing_builder.build::<tauri::Wry>());
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if window.label() != "demo_ws" {
|
||||
return;
|
||||
}
|
||||
if !matches!(event, tauri::WindowEvent::Destroyed) {
|
||||
return;
|
||||
}
|
||||
let app_handle = window.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = app_handle.state::<crate::AppState>();
|
||||
crate::disconnect_demo_ws_app_state(&state, false).await;
|
||||
});
|
||||
});
|
||||
builder = builder.setup(|app| {
|
||||
let splash_window = match app.get_webview_window("splash") {
|
||||
std::option::Option::Some(window) => window,
|
||||
@@ -96,13 +121,6 @@ pub fn run() -> kb_core::Result<()> {
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
crate::emit_splash_order(
|
||||
&splash_window,
|
||||
"add_log",
|
||||
std::option::Option::Some("Pool WebSocket initialisé"),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
crate::emit_splash_order(
|
||||
&splash_window,
|
||||
"fadein",
|
||||
@@ -434,7 +452,11 @@ fn open_demo_ws_window(
|
||||
fn demo_ws_list_pool_clients(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<kb_onchain_transport::WsPoolClientSnapshot>, std::string::String> {
|
||||
return std::result::Result::Ok(state.ws_pool().snapshot());
|
||||
let pool = match state.demo_ws_pool() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(pool.snapshot());
|
||||
}
|
||||
|
||||
/// Lists selectable WebSocket roles and methods for the demo UI.
|
||||
@@ -443,7 +465,10 @@ fn demo_ws_options(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoWsOptionsPayload, std::string::String> {
|
||||
return std::result::Result::Ok(crate::DemoWsOptionsPayload {
|
||||
roles: crate::build_ws_role_options(state.ws_pool().snapshot()),
|
||||
roles: crate::build_ws_role_options(match state.demo_ws_pool() {
|
||||
std::result::Result::Ok(pool) => pool.snapshot(),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
}),
|
||||
methods: crate::build_ws_method_options(),
|
||||
});
|
||||
}
|
||||
@@ -488,3 +513,338 @@ async fn demo_ws_disconnect(
|
||||
) -> std::result::Result<crate::DemoWsStatusPayload, std::string::String> {
|
||||
return crate::demo_ws_disconnect_inner(state.inner()).await;
|
||||
}
|
||||
|
||||
/// Opens or focuses the SQL diagnostic demo window.
|
||||
#[tauri::command]
|
||||
fn open_demo_sql_diag_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
return crate::open_sql_demo_window(
|
||||
app_handle,
|
||||
"demo_sql_diag",
|
||||
"demo_sql_diag.html",
|
||||
"Khadhroony Bot3 - SQL diagnostics",
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads read-only SQL diagnostics from the active profile.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_diag(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoSqlDiagPayload, std::string::String> {
|
||||
let profile = state.active_profile().clone();
|
||||
let store_result = crate::connect_postgres_store(&profile).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let diagnostics_result = store.backend_diagnostics().await;
|
||||
let diagnostics = match diagnostics_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let tables_result = store.known_table_diagnostics().await;
|
||||
let tables = match tables_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoSqlDiagPayload {
|
||||
config_path: state.config_path().to_string(),
|
||||
active_profile_name: profile.name.clone(),
|
||||
backend: profile.database.backend.clone(),
|
||||
masked_dsn: store.options().masked_dsn(),
|
||||
current_schema: diagnostics.descriptor.current_schema,
|
||||
health_status: crate::debug_status(diagnostics.health.status),
|
||||
health_message: diagnostics.health.message,
|
||||
migration_status: crate::debug_status(diagnostics.migrations.status),
|
||||
migration_message: diagnostics.migrations.message,
|
||||
server_version: diagnostics.server_version,
|
||||
tables: crate::table_snapshots_from_pg(tables.as_slice()),
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens or focuses the PostgreSQL canonical acquisition demo window.
|
||||
#[tauri::command]
|
||||
fn open_demo_sql_pg_raw_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
return crate::open_sql_demo_window(
|
||||
app_handle,
|
||||
"demo_sql_pg_raw",
|
||||
"demo_sql_pg_raw.html",
|
||||
"Khadhroony Bot3 - PostgreSQL canonical acquisition",
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads read-only raw table diagnostics from PostgreSQL.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_pg_raw(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoSqlPgRawPayload, std::string::String> {
|
||||
let profile = state.active_profile().clone();
|
||||
let store_result = crate::connect_postgres_store(&profile).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let tables_result = store.raw_table_diagnostics().await;
|
||||
let tables = match tables_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoSqlPgRawPayload {
|
||||
active_profile_name: profile.name.clone(),
|
||||
masked_dsn: store.options().masked_dsn(),
|
||||
tables: crate::table_snapshots_from_pg(tables.as_slice()),
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens or focuses the PostgreSQL core store demo window.
|
||||
#[tauri::command]
|
||||
fn open_demo_sql_pg_core_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
return crate::open_sql_demo_window(
|
||||
app_handle,
|
||||
"demo_sql_pg_core",
|
||||
"demo_sql_pg_core.html",
|
||||
"Khadhroony Bot3 - PostgreSQL core store",
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads read-only core table diagnostics from PostgreSQL.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_pg_core(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoSqlPgCorePayload, std::string::String> {
|
||||
let profile = state.active_profile().clone();
|
||||
let store_result = crate::connect_postgres_store(&profile).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let tables_result = store.core_table_diagnostics().await;
|
||||
let tables = match tables_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoSqlPgCorePayload {
|
||||
active_profile_name: profile.name.clone(),
|
||||
masked_dsn: store.options().masked_dsn(),
|
||||
tables: crate::table_snapshots_from_pg(tables.as_slice()),
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens or focuses the SQL replay candidate browser.
|
||||
#[tauri::command]
|
||||
fn open_demo_sql_replay_candidates_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
return crate::open_sql_demo_window(
|
||||
app_handle,
|
||||
"demo_sql_replay_candidates",
|
||||
"demo_sql_replay_candidates.html",
|
||||
"Khadhroony Bot3 - SQL replay candidates",
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn export_demo_sql_replay_csv(
|
||||
file_name: std::string::String,
|
||||
content: std::string::String,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
if content.is_empty() {
|
||||
return std::result::Result::Err("CSV export content must not be empty".to_owned());
|
||||
}
|
||||
if content.len() > 16 * 1024 * 1024 {
|
||||
return std::result::Result::Err("CSV export content exceeds 16 MiB".to_owned());
|
||||
}
|
||||
let normalized_name_result = crate::validated_csv_file_name(&file_name);
|
||||
let normalized_name = match normalized_name_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let current_dir_result = std::env::current_dir();
|
||||
let current_dir = match current_dir_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("cannot resolve current directory: {error}"));
|
||||
},
|
||||
};
|
||||
let export_dir = crate::csv_export_directory_from_current_dir(¤t_dir);
|
||||
let create_result = tokio::fs::create_dir_all(&export_dir).await;
|
||||
if let std::result::Result::Err(error) = create_result {
|
||||
return std::result::Result::Err(format!("cannot create CSV export directory: {error}"));
|
||||
}
|
||||
let export_path_result = crate::available_csv_export_path(&export_dir, normalized_name).await;
|
||||
let export_path = match export_path_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut bytes = std::vec::Vec::with_capacity(content.len() + 3);
|
||||
bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]);
|
||||
bytes.extend_from_slice(content.as_bytes());
|
||||
let write_result = tokio::fs::write(&export_path, bytes).await;
|
||||
if let std::result::Result::Err(error) = write_result {
|
||||
return std::result::Result::Err(format!("cannot write CSV export: {error}"));
|
||||
}
|
||||
let display_path = export_path.to_string_lossy().into_owned();
|
||||
tracing::info!(target: crate::TRACING_TARGET, path = %display_path, "CSV replay candidate export written");
|
||||
return std::result::Result::Ok(display_path);
|
||||
}
|
||||
|
||||
/// Loads static browser options from the active PostgreSQL profile.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_sql_replay_options(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoSqlReplayOptionsPayload, std::string::String> {
|
||||
let profile = state.active_profile().clone();
|
||||
let options_result = crate::postgres_store_options_from_profile(&profile, false);
|
||||
let options = match options_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let mut known_programs =
|
||||
std::vec::Vec::with_capacity(kb_program_ids::registered_program_ids().len());
|
||||
for entry in kb_program_ids::registered_program_ids() {
|
||||
known_programs.push(crate::DemoSqlReplayKnownProgramOption {
|
||||
code: entry.code().to_owned(),
|
||||
program_id: entry.program_id().to_owned(),
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(crate::DemoSqlReplayOptionsPayload {
|
||||
active_profile_name: profile.name,
|
||||
masked_dsn: options.masked_dsn(),
|
||||
maximum_limit: kb_store::MAX_REPLAY_CANDIDATE_ROWS,
|
||||
known_programs,
|
||||
});
|
||||
}
|
||||
|
||||
/// Loads bounded transaction replay candidates from PostgreSQL.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_replay_transactions(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSqlReplayTransactionRequest,
|
||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
|
||||
let program_scope_result = crate::program_scope_from_code(request.program_scope.as_str());
|
||||
let program_scope = match program_scope_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entity_kind_result = crate::optional_entity_kind_from_code(request.entity_kind.as_deref());
|
||||
let entity_kind = match entity_kind_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let filter_result = kb_store::PostgresReplayTransactionFilter::new(
|
||||
request.signature_contains,
|
||||
request.min_slot,
|
||||
request.max_slot,
|
||||
request.raw_processing_state,
|
||||
request.ledger_status,
|
||||
request.program_id,
|
||||
program_scope,
|
||||
entity_kind,
|
||||
request.entity_value,
|
||||
request.limit,
|
||||
request.newest_first,
|
||||
);
|
||||
let filter = match filter_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store_result = crate::connect_postgres_store(state.active_profile()).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rows_result = store.replay_transaction_candidates(&filter).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay transaction candidates");
|
||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
output.push(crate::transaction_row_from_pg(row));
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Loads bounded program summaries from PostgreSQL.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_replay_programs(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSqlReplayProgramRequest,
|
||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
|
||||
let filter_result =
|
||||
kb_store::PostgresReplayProgramFilter::new(request.program_id_contains, request.limit);
|
||||
let filter = match filter_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store_result = crate::connect_postgres_store(state.active_profile()).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rows_result = store.replay_program_summaries(&filter).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay program summaries");
|
||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
output.push(crate::program_row_from_pg(row));
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn load_demo_sql_replay_entities(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSqlReplayEntityRequest,
|
||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
|
||||
let entity_kind_result = crate::entity_kind_from_code(request.entity_kind.as_str());
|
||||
let entity_kind = match entity_kind_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let filter_result = kb_store::PostgresReplayEntityFilter::new(
|
||||
entity_kind,
|
||||
request.entity_value_contains,
|
||||
request.limit,
|
||||
);
|
||||
let filter = match filter_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store_result = crate::connect_postgres_store(state.active_profile()).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rows_result = store.replay_entity_summaries(&filter).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay entity summaries");
|
||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
output.push(crate::entity_row_from_pg(row));
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user