From 44b1dd4249fa0a2f797341c1b51d39bdb335ebe9 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:21:31 +0200 Subject: [PATCH] remove unused dep and move job out --- Cargo.lock | 153 +--- Cargo.toml | 9 +- clients/admin-ui/Cargo.lock | 14 + clients/admin-ui/index.html | 1 + clients/admin-ui/src/app.rs | 455 ++++++++-- clients/admin-ui/src/jobs.rs | 28 +- clients/admin-ui/src/lib.rs | 9 +- clients/admin-ui/src/runners.rs | 274 +++--- clients/admin-ui/src/sidebar.rs | 750 +++++++++------- clients/admin-ui/src/sidebar_old.rs | 639 ++++++++++++++ clients/admin-ui/src/toast.rs | 165 ++++ clients/admin-ui/styles.css | 1262 +++++---------------------- clients/openrpc/Cargo.lock | 139 +-- clients/openrpc/Cargo.toml | 2 + clients/openrpc/src/lib.rs | 309 ++----- clients/openrpc/src/wasm.rs | 4 +- src/app.rs | 35 + src/client.rs | 328 ------- src/job.rs | 220 +---- src/lib.rs | 17 +- src/mycelium.rs | 297 +++++++ src/openrpc.rs | 18 +- src/runner.rs | 71 +- src/supervisor.rs | 98 ++- 24 files changed, 2558 insertions(+), 2739 deletions(-) create mode 100644 clients/admin-ui/src/sidebar_old.rs create mode 100644 clients/admin-ui/src/toast.rs delete mode 100644 src/client.rs create mode 100644 src/mycelium.rs diff --git a/Cargo.lock b/Cargo.lock index 3f70611..f702fb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,15 +331,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - [[package]] name = "digest" version = "0.10.7" @@ -437,7 +428,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -460,17 +450,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -596,21 +575,50 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hero-job" +version = "0.1.0" +dependencies = [ + "chrono", + "log", + "redis", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "hero-job" +version = "0.1.0" +source = "git+https://git.ourworld.tf/herocode/job#1f7cd4ded8db57fb5ec3f7d42782fe45a70af164" +dependencies = [ + "chrono", + "log", + "redis", + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "hero-supervisor" version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "base64", "chrono", "clap", "env_logger 0.10.2", "escargot", + "hero-job 0.1.0 (git+https://git.ourworld.tf/herocode/job)", "hero-supervisor-openrpc-client", "jsonrpsee", "log", + "rand", "redis", - "sal-service-manager", "serde", "serde_json", "thiserror", @@ -630,6 +638,7 @@ dependencies = [ "console_log", "env_logger 0.11.8", "getrandom 0.2.16", + "hero-job 0.1.0", "hero-supervisor", "js-sys", "jsonrpsee", @@ -1157,12 +1166,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" version = "0.2.19" @@ -1260,19 +1263,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "plist" -version = "1.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" -dependencies = [ - "base64", - "indexmap", - "quick-xml", - "serde", - "time", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -1297,12 +1287,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1330,15 +1314,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.40" @@ -1561,23 +1536,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "sal-service-manager" -version = "0.1.0" -dependencies = [ - "async-trait", - "chrono", - "futures", - "log", - "once_cell", - "plist", - "serde", - "serde_json", - "thiserror", - "tokio", - "zinit-client", -] - [[package]] name = "same-file" version = "1.0.6" @@ -1815,37 +1773,6 @@ dependencies = [ "syn", ] -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -2667,21 +2594,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zinit-client" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd" -dependencies = [ - "async-stream", - "async-trait", - "chrono", - "futures", - "rand", - "serde", - "serde_json", - "thiserror", - "tokio", - "tracing", -] diff --git a/Cargo.toml b/Cargo.toml index cf7b94e..f4394f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +# Shared job crate +hero-job = { git = "https://git.ourworld.tf/herocode/job" } # Async runtime tokio = { version = "1.0", features = ["full"] } @@ -23,7 +25,6 @@ chrono = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" env_logger = "0.10" -sal-service-manager = { path = "../sal/service_manager" } # CLI argument parsing clap = { version = "4.0", features = ["derive"] } @@ -37,6 +38,12 @@ anyhow = "1.0" tower-http = { version = "0.5", features = ["cors"] } tower = "0.4" +# Base64 encoding for Mycelium payloads +base64 = "0.22" + +# Random number generation for message IDs +rand = "0.8" + [dev-dependencies] tokio-test = "0.4" hero-supervisor-openrpc-client = { path = "clients/openrpc" } diff --git a/clients/admin-ui/Cargo.lock b/clients/admin-ui/Cargo.lock index 24d1114..8bdf0fa 100644 --- a/clients/admin-ui/Cargo.lock +++ b/clients/admin-ui/Cargo.lock @@ -1025,6 +1025,19 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hero-job" +version = "0.1.0" +dependencies = [ + "chrono", + "log", + "redis", + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "hero-supervisor" version = "0.1.0" @@ -1034,6 +1047,7 @@ dependencies = [ "chrono", "clap", "env_logger 0.10.2", + "hero-job", "jsonrpsee", "log", "redis", diff --git a/clients/admin-ui/index.html b/clients/admin-ui/index.html index 08a1f56..7af35bd 100644 --- a/clients/admin-ui/index.html +++ b/clients/admin-ui/index.html @@ -4,6 +4,7 @@ Hero Supervisor + diff --git a/clients/admin-ui/src/app.rs b/clients/admin-ui/src/app.rs index 8bad965..3d061ed 100644 --- a/clients/admin-ui/src/app.rs +++ b/clients/admin-ui/src/app.rs @@ -1,11 +1,13 @@ use yew::prelude::*; use gloo::console; use gloo::timers::callback::Interval; +use gloo::storage::{LocalStorage, Storage}; use wasm_bindgen_futures::spawn_local; use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob}; -use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType}; +use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType, SessionData, SESSION_STORAGE_KEY}; use crate::runners::{Runners, RegisterForm}; use crate::jobs::Jobs; +use crate::toast::{Toast, ToastContainer}; /// Generate a unique job ID client-side using UUID v4 fn generate_job_id() -> String { @@ -17,7 +19,6 @@ pub struct JobForm { pub payload: String, pub runner: String, pub executor: String, - pub secret: String, } #[derive(Clone, Debug, PartialEq)] @@ -45,7 +46,10 @@ pub struct AppState { pub job_form: JobForm, pub supervisor_info: Option, pub admin_secret: String, + pub session_secret: String, + pub session_secret_type: SessionSecretType, pub ping_states: std::collections::HashMap, // runner -> ping_state + pub toasts: Vec, // Toast notifications } @@ -54,25 +58,36 @@ pub struct AppState { #[function_component(App)] pub fn app() -> Html { - let state = use_state(|| AppState { - server_url: "http://localhost:3030".to_string(), - runners: vec![], - jobs: vec![], - ongoing_jobs: vec![], - loading: false, - register_form: RegisterForm { - name: String::new(), - secret: String::new(), - }, - job_form: JobForm { - payload: String::new(), - runner: String::new(), - executor: String::new(), - secret: String::new(), - }, - supervisor_info: None, - admin_secret: String::new(), - ping_states: std::collections::HashMap::new(), + let state = use_state(|| { + // Try to load session from localStorage + let (session_secret, session_secret_type) = if let Ok(session_data) = LocalStorage::get::(SESSION_STORAGE_KEY) { + (session_data.secret, session_data.secret_type) + } else { + (String::new(), SessionSecretType::None) + }; + + AppState { + server_url: "http://localhost:3030".to_string(), + runners: vec![], + jobs: vec![], + ongoing_jobs: vec![], + loading: false, + register_form: RegisterForm { + name: String::new(), + secret: String::new(), + }, + job_form: JobForm { + payload: String::new(), + runner: String::new(), + executor: String::new(), + }, + supervisor_info: None, + admin_secret: String::new(), + session_secret, + session_secret_type, + ping_states: std::collections::HashMap::new(), + toasts: vec![], + } }); // Set up polling for ongoing jobs every 2 seconds @@ -131,6 +146,90 @@ pub fn app() -> Html { }); } + // Check server connection status periodically + { + let state = state.clone(); + use_effect_with((), move |_| { + let state = state.clone(); + let client_url = state.server_url.clone(); + + let check_connection = { + let state = state.clone(); + let client_url = client_url.clone(); + Callback::from(move |_| { + let state = state.clone(); + let client_url = client_url.clone(); + let client = WasmSupervisorClient::new(client_url.clone()); + spawn_local(async move { + let mut current_state = (*state).clone(); + + // Try to ping the server to check connection + match client.list_runners().await { + Ok(_) => { + // Server is reachable, now try to load secrets if we have a session secret + let mut admin_secrets = vec![]; + let mut user_secrets = vec![]; + let mut register_secrets = vec![]; + + // Try to load secrets based on current session secret + if !current_state.session_secret.is_empty() { + match current_state.session_secret_type { + SessionSecretType::Admin => { + if let Ok(secrets) = client.list_admin_secrets(¤t_state.session_secret).await { + admin_secrets = secrets; + } + if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await { + user_secrets = secrets; + } + if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await { + register_secrets = secrets; + } + } + SessionSecretType::User => { + if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await { + user_secrets = secrets; + } + } + SessionSecretType::Register => { + if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await { + register_secrets = secrets; + } + } + SessionSecretType::None => {} + } + } + + current_state.supervisor_info = Some(SupervisorInfo { + server_url: client_url.clone(), + runners_count: 0, + admin_secrets, + user_secrets, + register_secrets, + }); + } + Err(_) => { + // Server is not reachable + current_state.supervisor_info = None; + } + } + + state.set(current_state); + }); + }) + }; + + // Check connection immediately + check_connection.emit(()); + + // Set up interval to check connection every 5 seconds + let interval = Interval::new(5000, move || { + check_connection.emit(()); + }); + + move || drop(interval) + }); + } + // Load initial data when component mounts let load_initial_data = { let state = state.clone(); @@ -262,9 +361,10 @@ pub fn app() -> Html { } Err(e) => { console::error!("Failed to load runners:", format!("{:?}", e)); - let mut updated_state = (*state).clone(); - updated_state.loading = false; - state.set(updated_state); + let mut error_state = (*state).clone(); + error_state.loading = false; + error_state.toasts.push(Toast::error(format!("Failed to load runners: {:?}", e))); + state.set(error_state); } } }); @@ -294,7 +394,10 @@ pub fn app() -> Html { job_form: state.job_form.clone(), supervisor_info: state.supervisor_info.clone(), admin_secret: state.admin_secret.clone(), + session_secret: state.session_secret.clone(), + session_secret_type: state.session_secret_type.clone(), ping_states: state.ping_states.clone(), + toasts: state.toasts.clone(), }; state.set(new_state); }) @@ -354,7 +457,6 @@ pub fn app() -> Html { "payload" => new_form.payload = value, "runner" => new_form.runner = value, "executor" => new_form.executor = value, - "secret" => new_form.secret = value, _ => {} } let mut new_state = (*state).clone(); @@ -366,7 +468,7 @@ pub fn app() -> Html { // Run job callback - now uses create_job for immediate display and polling let on_run_job = { let state = state.clone(); - Callback::from(move |_| { + Callback::from(move |_: ()| { let current_state = (*state).clone(); let client = WasmSupervisorClient::new(current_state.server_url.clone()); let job_form = current_state.job_form.clone(); @@ -396,7 +498,7 @@ pub fn app() -> Html { console::log!("Job added to list immediately with ID:", &job_id); // Create the job using fire-and-forget create_job method - match client.create_job(job_form.secret.clone(), job).await { + match client.create_job(current_state.session_secret.clone(), job).await { Ok(returned_job_id) => { console::log!("Job created successfully with ID:", &returned_job_id); } @@ -415,10 +517,71 @@ pub fn app() -> Html { // Supervisor info loaded callback let on_supervisor_info_loaded = { let state = state.clone(); - Callback::from(move |supervisor_info: SupervisorInfo| { - let mut new_state = (*state).clone(); - new_state.supervisor_info = Some(supervisor_info); - state.set(new_state); + Callback::from(move |info: SupervisorInfo| { + let mut current_state = (*state).clone(); + current_state.supervisor_info = Some(info); + state.set(current_state); + }) + }; + + let on_add_secret = { + let state = state.clone(); + Callback::from(move |(secret_type, _secret): (SessionSecretType, String)| { + let mut current_state = (*state).clone(); + if let Some(ref mut info) = current_state.supervisor_info { + // Prompt for a new secret + if let Some(window) = web_sys::window() { + if let Ok(Some(new_secret)) = window.prompt_with_message("Enter new secret:") { + if !new_secret.is_empty() { + match secret_type { + SessionSecretType::Admin => { + if !info.admin_secrets.contains(&new_secret) { + info.admin_secrets.push(new_secret); + } + } + SessionSecretType::User => { + if !info.user_secrets.contains(&new_secret) { + info.user_secrets.push(new_secret); + } + } + SessionSecretType::Register => { + if !info.register_secrets.contains(&new_secret) { + info.register_secrets.push(new_secret); + } + } + SessionSecretType::None => { + // Do nothing for None type + } + } + } + } + } + } + state.set(current_state); + }) + }; + + let on_remove_secret = { + let state = state.clone(); + Callback::from(move |(secret_type, secret): (SessionSecretType, String)| { + let mut current_state = (*state).clone(); + if let Some(ref mut info) = current_state.supervisor_info { + match secret_type { + SessionSecretType::Admin => { + info.admin_secrets.retain(|s| s != &secret); + } + SessionSecretType::User => { + info.user_secrets.retain(|s| s != &secret); + } + SessionSecretType::Register => { + info.register_secrets.retain(|s| s != &secret); + } + SessionSecretType::None => { + // Do nothing for None type + } + } + } + state.set(current_state); }) }; @@ -439,10 +602,14 @@ pub fn app() -> Html { // Remove runner from the list let mut updated_state = (*state_clone).clone(); updated_state.runners.retain(|(name, _)| name != &runner_id); + updated_state.toasts.push(Toast::success("Runner removed successfully".to_string())); state_clone.set(updated_state); } Err(e) => { console::error!("Failed to remove runner:", format!("{:?}", e)); + let mut error_state = (*state_clone).clone(); + error_state.toasts.push(Toast::error(format!("Failed to remove runner: {:?}", e))); + state_clone.set(error_state); } } }); @@ -470,6 +637,9 @@ pub fn app() -> Html { } Err(e) => { console::error!("Failed to stop job:", format!("{:?}", e)); + let mut error_state = (*state_clone).clone(); + error_state.toasts.push(Toast::error(format!("Failed to stop job: {:?}", e))); + state_clone.set(error_state); } } }); @@ -498,16 +668,112 @@ pub fn app() -> Html { } Err(e) => { console::error!("Failed to delete job:", format!("{:?}", e)); + let mut error_state = (*state_clone).clone(); + error_state.toasts.push(Toast::error(format!("Failed to delete job: {:?}", e))); + state_clone.set(error_state); } } }); }) }; - // Ping runner callback - uses run_job for immediate result with proper state management - let on_ping_runner = { + // Toast dismiss callback + let on_toast_dismiss = { let state = state.clone(); - Callback::from(move |(runner_id, secret): (String, String)| { + Callback::from(move |toast_id: String| { + let mut current_state = (*state).clone(); + current_state.toasts.retain(|t| t.id != toast_id); + state.set(current_state); + }) + }; + + // Add toast callback + let add_toast = { + let state = state.clone(); + Callback::from(move |toast: Toast| { + let mut current_state = (*state).clone(); + current_state.toasts.push(toast); + state.set(current_state); + }) + }; + + // Session secret change callback + let on_session_secret_change = { + let state = state.clone(); + Callback::from(move |(secret, secret_type): (String, SessionSecretType)| { + let mut current_state = (*state).clone(); + current_state.session_secret = secret.clone(); + current_state.session_secret_type = secret_type.clone(); + + // If we have a session secret, trigger API call to load secrets + if !secret.is_empty() { + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + spawn_local(async move { + let mut updated_state = (*state_clone).clone(); + + // Try to load secrets from API based on secret type + match client.list_runners().await { + Ok(_) => { + let mut admin_secrets = vec![]; + let mut user_secrets = vec![]; + let mut register_secrets = vec![]; + + // Load secrets based on secret type + match secret_type { + SessionSecretType::Admin => { + if let Ok(secrets) = client.list_admin_secrets(&secret).await { + admin_secrets = secrets; + } + if let Ok(secrets) = client.list_user_secrets(&secret).await { + user_secrets = secrets; + } + if let Ok(secrets) = client.list_register_secrets(&secret).await { + register_secrets = secrets; + } + } + SessionSecretType::User => { + if let Ok(secrets) = client.list_user_secrets(&secret).await { + user_secrets = secrets; + } + } + SessionSecretType::Register => { + if let Ok(secrets) = client.list_register_secrets(&secret).await { + register_secrets = secrets; + } + } + SessionSecretType::None => {} + } + + updated_state.supervisor_info = Some(SupervisorInfo { + server_url: updated_state.server_url.clone(), + runners_count: 0, + admin_secrets, + user_secrets, + register_secrets, + }); + } + Err(e) => { + updated_state.supervisor_info = None; + // Add error toast + let error_msg = format!("Failed to connect to supervisor: {:?}", e); + updated_state.toasts.push(Toast::error(error_msg)); + } + } + + state_clone.set(updated_state); + }); + } + + state.set(current_state); + }) + }; + + // Run job callback + let on_run_job = { + let state = state.clone(); + Callback::from(move |(runner_id, payload): (String, String)| { let current_state = (*state).clone(); let client = WasmSupervisorClient::new(current_state.server_url.clone()); let state_clone = state.clone(); @@ -520,26 +786,22 @@ pub fn app() -> Html { } spawn_local(async move { - console::log!("Pinging runner:", &runner_id); + console::log!("Running job on runner:", &runner_id, "with payload:", &payload); - // Generate unique job ID client-side - let job_id = generate_job_id(); - - // Create ping job with client-generated ID - let ping_job = WasmJob::new( - job_id.clone(), - "ping".to_string(), - "ping".to_string(), - runner_id.clone(), + // Use session secret to create job with payload + let job = WasmJob::new( + generate_job_id(), + payload.clone(), + "default".to_string(), + runner_id.clone() ); - // Use run_job for immediate result instead of create_job - match client.run_job(secret, ping_job).await { - Ok(result) => { - console::log!("Ping successful, result:", &result); - // Set ping state to success with result + match client.run_job(current_state.session_secret, job).await { + Ok(job_id) => { + console::log!("Job created successfully:", &job_id); + // Set ping state to success with job ID let mut success_state = (*state_clone).clone(); - success_state.ping_states.insert(runner_id.clone(), PingState::Success(result)); + success_state.ping_states.insert(runner_id.clone(), PingState::Success(format!("Job created: {}", job_id))); state_clone.set(success_state); // Reset to idle after 3 seconds @@ -553,7 +815,7 @@ pub fn app() -> Html { }); } Err(e) => { - console::error!("Failed to ping runner:", format!("{:?}", e)); + console::error!("Failed to create job:", format!("{:?}", e)); // Set ping state to error let mut error_state = (*state_clone).clone(); let error_msg = format!("Error: {:?}", e); @@ -585,45 +847,60 @@ pub fn app() -> Html { }); html! { -
- - -
- - - - - // Floating refresh button - +
+
+
+ + +
+
+ + + + + // Floating refresh button + +
+
+
+ + // Toast notifications +
} } diff --git a/clients/admin-ui/src/jobs.rs b/clients/admin-ui/src/jobs.rs index 14d1501..e1d1af9 100644 --- a/clients/admin-ui/src/jobs.rs +++ b/clients/admin-ui/src/jobs.rs @@ -11,7 +11,7 @@ pub struct JobsProps { pub job_form: JobForm, pub runners: Vec<(String, String)>, // (name, status) - list of registered runners pub on_job_form_change: Callback<(String, String)>, - pub on_run_job: Callback<()>, + pub on_run_job: Callback<(String, String)>, // (runner, payload) pub on_stop_job: Callback, pub on_delete_job: Callback, } @@ -25,7 +25,6 @@ impl PartialEq for JobsProps { self.job_form.payload == other.job_form.payload && self.job_form.runner == other.job_form.runner && self.job_form.executor == other.job_form.executor && - self.job_form.secret == other.job_form.secret && self.runners.len() == other.runners.len() // Note: Callbacks don't implement PartialEq, so we skip them } @@ -57,18 +56,12 @@ pub fn jobs(props: &JobsProps) -> Html { }) }; - let on_secret_change = { - let on_change = props.on_job_form_change.clone(); - Callback::from(move |e: Event| { - let input: web_sys::HtmlInputElement = e.target_unchecked_into(); - on_change.emit(("secret".to_string(), input.value())); - }) - }; let on_run_click = { let on_run = props.on_run_job.clone(); + let job_form = props.job_form.clone(); Callback::from(move |_: MouseEvent| { - on_run.emit(()); + on_run.emit((job_form.runner.clone(), job_form.payload.clone())); }) }; @@ -84,6 +77,7 @@ pub fn jobs(props: &JobsProps) -> Html { {"Runner"} {"Executor"} {"Status"} + {"Actions"} @@ -126,14 +120,10 @@ pub fn jobs(props: &JobsProps) -> Html { onchange={on_executor_change} /> + + {"Not Started"} + - -
- -
+
+

{"Runners"}

- // Existing runner cards - {for props.runners.iter().map(|(name, status)| { - let status_class = match status.as_str() { - "Running" => "status-running", - "Stopped" => "status-stopped", - "Starting" => "status-starting", - "Stopping" => "status-starting", - "Registering" => "status-registering", - _ => "status-stopped", - }; - - let name_clone = name.clone(); - let name_clone2 = name.clone(); - let on_remove = props.on_remove_runner.clone(); - let on_ping = props.on_ping_runner.clone(); - - html! { -
-
-
-
- - {"●"} - -
{name}
-
- - {"redis://localhost:6379/runner:"}{name} - -
-
- -
-
-
-
- {"📊 Live job count chart (5s updates)"} -
-
-
- { - match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) { - PingState::Idle => html! { -
- - -
- }, - PingState::Waiting => html! { -
- {"⏳"} - {"Waiting for response..."} -
- }, - PingState::Success(result) => html! { -
- {"✅"} - {format!("Success: {}", result)} -
- }, - PingState::Error(error) => html! { -
- {"❌"} - {error} -
- }, - } - } + // All cards in same row - registration card first, then runner cards +
+ // Registration card as first item +
+
+
+ +
{"Add New Runner"}
- } - })} +
+
+
+ + +
+
+ + +
+ +
+
+
+ + {for props.runners.iter().map(|(name, status)| { + let badge_class = match status.as_str() { + "Running" => "status-running", + "Stopped" => "status-stopped", + "Starting" => "status-starting", + "Stopping" => "status-starting", + "Registering" => "status-registering", + _ => "bg-secondary", + }; + + let name_clone = name.clone(); + let name_clone2 = name.clone(); + let on_remove = props.on_remove_runner.clone(); + let on_run = props.on_run_job.clone(); + + html! { +
+
+
+
+ {"●"} +
+
+
{name}
+ {format!("Queue: {}", name)} +
+
+ + {status} + +
+
+ +
+ + +
+ +
+ { + match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) { + PingState::Idle => html! { + {"Ready"} + }, + PingState::Waiting => html! { + + + {"Working..."} + + }, + PingState::Success(ref msg) => html! { + + + {msg} + + }, + PingState::Error(ref msg) => html! { + + + {msg} + + }, + } + } +
+
+
+ } + })} +
} } diff --git a/clients/admin-ui/src/sidebar.rs b/clients/admin-ui/src/sidebar.rs index 5c2e162..5c3947a 100644 --- a/clients/admin-ui/src/sidebar.rs +++ b/clients/admin-ui/src/sidebar.rs @@ -2,24 +2,44 @@ use yew::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use gloo::console; -use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob}; +use gloo::storage::{LocalStorage, Storage}; +use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; +use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq)] pub struct SupervisorInfo { pub server_url: String, - pub admin_secrets_count: usize, - pub user_secrets_count: usize, - pub register_secrets_count: usize, + pub admin_secrets: Vec, + pub user_secrets: Vec, + pub register_secrets: Vec, pub runners_count: usize, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub enum SessionSecretType { None, User, Admin, + Register, } +#[derive(Serialize, Deserialize, Clone)] +pub struct SessionData { + pub secret: String, + pub secret_type: SessionSecretType, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct StoredSecrets { + pub admin_secrets: Vec, + pub user_secrets: Vec, + pub register_secrets: Vec, +} + +pub const STORED_SECRETS_KEY: &str = "supervisor_stored_secrets"; + +pub const SESSION_STORAGE_KEY: &str = "supervisor_session"; + #[derive(Properties, PartialEq)] pub struct SidebarProps { pub server_url: String, @@ -28,17 +48,27 @@ pub struct SidebarProps { pub session_secret_type: SessionSecretType, pub on_session_secret_change: Callback<(String, SessionSecretType)>, pub on_supervisor_info_loaded: Callback, + pub on_add_secret: Callback<(SessionSecretType, String)>, + pub on_remove_secret: Callback<(SessionSecretType, String)>, } #[function_component(Sidebar)] pub fn sidebar(props: &SidebarProps) -> Html { let session_secret_input = use_state(|| String::new()); - let payload_input = use_state(|| String::new()); - let admin_secrets = use_state(|| Vec::::new()); - let user_secrets = use_state(|| Vec::::new()); - let register_secrets = use_state(|| Vec::::new()); + let selected_secret_type = use_state(|| SessionSecretType::Admin); let is_loading = use_state(|| false); + // Load session from localStorage on component mount + { + let on_session_secret_change = props.on_session_secret_change.clone(); + use_effect_with((), move |_| { + if let Ok(session_data) = LocalStorage::get::(SESSION_STORAGE_KEY) { + on_session_secret_change.emit((session_data.secret, session_data.secret_type)); + } + || () + }); + } + let on_session_secret_change = { let session_secret_input = session_secret_input.clone(); Callback::from(move |e: web_sys::Event| { @@ -49,12 +79,11 @@ pub fn sidebar(props: &SidebarProps) -> Html { let on_session_secret_submit = { let session_secret_input = session_secret_input.clone(); + let selected_secret_type = selected_secret_type.clone(); let is_loading = is_loading.clone(); - let admin_secrets = admin_secrets.clone(); - let user_secrets = user_secrets.clone(); - let register_secrets = register_secrets.clone(); - let server_url = props.server_url.clone(); let on_session_secret_change = props.on_session_secret_change.clone(); + let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone(); + let server_url = props.server_url.clone(); Callback::from(move |_: web_sys::MouseEvent| { let secret = (*session_secret_input).clone(); @@ -63,346 +92,431 @@ pub fn sidebar(props: &SidebarProps) -> Html { } is_loading.set(true); - let client = WasmSupervisorClient::new(server_url.clone()); - - let session_secret_input = session_secret_input.clone(); let is_loading = is_loading.clone(); - let admin_secrets = admin_secrets.clone(); - let user_secrets = user_secrets.clone(); - let register_secrets = register_secrets.clone(); let on_session_secret_change = on_session_secret_change.clone(); + let on_supervisor_info_loaded = on_supervisor_info_loaded.clone(); + let server_url = server_url.clone(); + let session_secret_input = session_secret_input.clone(); + let selected_secret_type = selected_secret_type.clone(); spawn_local(async move { - // Try to get admin secrets first to determine if this is an admin secret - match client.list_admin_secrets(&secret).await { - Ok(admin_secret_list) => { - // This is an admin secret - admin_secrets.set(admin_secret_list); + let client = WasmSupervisorClient::new(server_url.clone()); + + match client.discover().await { + Ok(_) => { + console::log!("Connected to supervisor successfully"); - // Also load user and register secrets - if let Ok(user_secret_list) = client.list_user_secrets(&secret).await { - user_secrets.set(user_secret_list); - } - if let Ok(register_secret_list) = client.list_register_secrets(&secret).await { - register_secrets.set(register_secret_list); - } + let secret_type = (*selected_secret_type).clone(); - on_session_secret_change.emit((secret, SessionSecretType::Admin)); - console::log!("Admin session established"); - } - Err(_) => { - // Try as user secret - just test if we can make any call with it - match client.list_runners().await { - Ok(_) => { - // This appears to be a valid user secret - on_session_secret_change.emit((secret, SessionSecretType::User)); - console::log!("User session established"); + // Don't store secrets in localStorage - use API only + + // Save to localStorage + let session_data = SessionData { + secret: secret.clone(), + secret_type: secret_type.clone(), + }; + let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data); + + // Create supervisor info with empty secrets initially + let mut supervisor_info = SupervisorInfo { + server_url: server_url.clone(), + admin_secrets: vec![], + user_secrets: vec![], + register_secrets: vec![], + runners_count: 0, + }; + + // Only fetch secrets if this is an admin secret + if secret_type == SessionSecretType::Admin { + console::log!("Attempting to fetch secrets with admin secret"); + + // Try to fetch admin secrets + match client.list_admin_secrets(&secret).await { + Ok(admin_secrets) => { + console::log!("✅ Fetched admin secrets:", format!("{:?}", admin_secrets)); + supervisor_info.admin_secrets = admin_secrets; + }, + Err(e) => { + console::error!("❌ Failed to fetch admin secrets:", format!("{:?}", e)); + // If admin secret fetch fails, this might not be a valid admin secret + supervisor_info.admin_secrets = vec![secret.clone()]; + } } - Err(e) => { - console::log!("Invalid secret:", format!("{:?}", e)); - on_session_secret_change.emit((String::new(), SessionSecretType::None)); + + // Try to fetch user secrets + match client.list_user_secrets(&secret).await { + Ok(user_secrets) => { + console::log!("✅ Fetched user secrets:", format!("{:?}", user_secrets)); + supervisor_info.user_secrets = user_secrets; + }, + Err(e) => { + console::error!("❌ Failed to fetch user secrets:", format!("{:?}", e)); + } + } + + // Try to fetch register secrets + match client.list_register_secrets(&secret).await { + Ok(register_secrets) => { + console::log!("✅ Fetched register secrets:", format!("{:?}", register_secrets)); + supervisor_info.register_secrets = register_secrets; + }, + Err(e) => { + console::error!("❌ Failed to fetch register secrets:", format!("{:?}", e)); + } + } + } else { + console::log!("Non-admin secret - showing only current secret"); + // For non-admin secrets, only show the current secret + match secret_type { + SessionSecretType::User => { + supervisor_info.user_secrets = vec![secret.clone()]; + }, + SessionSecretType::Register => { + supervisor_info.register_secrets = vec![secret.clone()]; + }, + _ => {} } } - } - } - - is_loading.set(false); - session_secret_input.set(String::new()); - }); - }) - }; - - let on_session_clear = { - let on_session_secret_change = props.on_session_secret_change.clone(); - let admin_secrets = admin_secrets.clone(); - let user_secrets = user_secrets.clone(); - let register_secrets = register_secrets.clone(); - - Callback::from(move |_: web_sys::MouseEvent| { - on_session_secret_change.emit((String::new(), SessionSecretType::None)); - admin_secrets.set(Vec::new()); - user_secrets.set(Vec::new()); - register_secrets.set(Vec::new()); - console::log!("Session cleared"); - }) - }; - - let on_payload_change = { - let payload_input = payload_input.clone(); - Callback::from(move |e: web_sys::Event| { - let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap(); - payload_input.set(input.value()); - }) - }; - - let on_run_click = { - let payload_input = payload_input.clone(); - let server_url = props.server_url.clone(); - let session_secret = props.session_secret.clone(); - let is_loading = is_loading.clone(); - - Callback::from(move |_: web_sys::MouseEvent| { - let payload = (*payload_input).clone(); - if payload.is_empty() || session_secret.is_empty() { - return; - } - - is_loading.set(true); - let client = WasmSupervisorClient::new(server_url.clone()); - - let payload_input = payload_input.clone(); - let is_loading = is_loading.clone(); - let session_secret = session_secret.clone(); - - spawn_local(async move { - // Create WasmJob object using constructor - let job = WasmJob::new( - uuid::Uuid::new_v4().to_string(), - payload.clone(), - "osis".to_string(), - "default".to_string(), - ); - - match client.create_job(session_secret.clone(), job).await { - Ok(job_id) => { - console::log!("Job created successfully:", job_id); - payload_input.set(String::new()); + + on_session_secret_change.emit((secret.clone(), secret_type.clone())); + on_supervisor_info_loaded.emit(supervisor_info); + session_secret_input.set(String::new()); } Err(e) => { - console::log!("Failed to create job:", format!("{:?}", e)); + console::error!("Failed to connect to supervisor:", format!("{:?}", e)); } } + is_loading.set(false); }); }) }; + let on_logout = { + let on_session_secret_change = props.on_session_secret_change.clone(); + Callback::from(move |_: web_sys::MouseEvent| { + // Clear localStorage + let _ = LocalStorage::delete(SESSION_STORAGE_KEY); + on_session_secret_change.emit((String::new(), SessionSecretType::None)); + }) + }; + + let on_add_admin_secret = { + let on_add_secret = props.on_add_secret.clone(); + Callback::from(move |_| { + on_add_secret.emit((SessionSecretType::Admin, String::new())); + }) + }; + + let on_add_user_secret = { + let on_add_secret = props.on_add_secret.clone(); + Callback::from(move |_| { + on_add_secret.emit((SessionSecretType::User, String::new())); + }) + }; + + let on_add_register_secret = { + let on_add_secret = props.on_add_secret.clone(); + Callback::from(move |_| { + on_add_secret.emit((SessionSecretType::User, String::new())); + }) + }; + + let on_session_input = { + let session_secret_input = session_secret_input.clone(); + Callback::from(move |e: InputEvent| { + let input: web_sys::HtmlInputElement = e.target_unchecked_into(); + session_secret_input.set(input.value()); + }) + }; + + let on_session_keypress = { + let on_session_secret_submit = on_session_secret_submit.clone(); + Callback::from(move |e: KeyboardEvent| { + if e.key() == "Enter" { + e.prevent_default(); + // Create a dummy MouseEvent to trigger the submit handler + let dummy_event = web_sys::MouseEvent::new("click").unwrap(); + on_session_secret_submit.emit(dummy_event); + } + }) + }; + + let on_session_toggle = { + let on_session_secret_submit = on_session_secret_submit.clone(); + let on_session_secret_change = props.on_session_secret_change.clone(); + let session_secret = props.session_secret.clone(); + Callback::from(move |e: web_sys::MouseEvent| { + if session_secret.is_empty() { + // Try to login with current input + on_session_secret_submit.emit(e); + } else { + // Logout - clear localStorage and session + let _ = LocalStorage::delete(SESSION_STORAGE_KEY); + on_session_secret_change.emit((String::new(), SessionSecretType::None)); + } + }) + }; + + // Function to refresh secrets from API when switching to an admin secret + let refresh_secrets_from_api = { + let server_url = props.server_url.clone(); + let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone(); + Callback::from(move |(secret, secret_type): (String, SessionSecretType)| { + if secret_type == SessionSecretType::Admin { + let client = WasmSupervisorClient::new(server_url.clone()); + let on_supervisor_info_loaded = on_supervisor_info_loaded.clone(); + let server_url = server_url.clone(); + + spawn_local(async move { + let mut supervisor_info = SupervisorInfo { + server_url: server_url.clone(), + admin_secrets: vec![], + user_secrets: vec![], + register_secrets: vec![], + runners_count: 0, + }; + + // Fetch real secrets from the API + match client.list_admin_secrets(&secret).await { + Ok(admin_secrets) => { + console::log!("Refreshed admin secrets from API:", format!("{:?}", admin_secrets)); + supervisor_info.admin_secrets = admin_secrets; + }, + Err(e) => console::error!("Failed to refresh admin secrets:", format!("{:?}", e)) + } + + match client.list_user_secrets(&secret).await { + Ok(user_secrets) => { + console::log!("Refreshed user secrets from API:", format!("{:?}", user_secrets)); + supervisor_info.user_secrets = user_secrets; + }, + Err(e) => console::error!("Failed to refresh user secrets:", format!("{:?}", e)) + } + + match client.list_register_secrets(&secret).await { + Ok(register_secrets) => { + console::log!("Refreshed register secrets from API:", format!("{:?}", register_secrets)); + supervisor_info.register_secrets = register_secrets; + }, + Err(e) => console::error!("Failed to refresh register secrets:", format!("{:?}", e)) + } + + on_supervisor_info_loaded.emit(supervisor_info); + }); + } + }) + }; + html! { -