Simplify build.sh and update run.sh for self-contained execution

- Simplified build.sh to just build in release mode with warning suppression
- Updated run.sh to build first, then start supervisor and admin UI together
- Run script now starts both supervisor API and admin UI in background
- Added proper cleanup handler for graceful shutdown
- Removed admin UI compilation errors by fixing JsValue handling
- Added list_jobs method to WASM client for admin UI compatibility
This commit is contained in:
Timur Gordon
2025-11-04 17:05:01 +01:00
parent 3356a03895
commit b8ef14d06c
14 changed files with 424 additions and 7522 deletions

19
Cargo.lock generated
View File

@@ -596,14 +596,27 @@ dependencies = [
"chrono", "chrono",
"hex", "hex",
"log", "log",
"redis",
"secp256k1 0.28.2", "secp256k1 0.28.2",
"serde", "serde",
"serde-wasm-bindgen",
"serde_json", "serde_json",
"sha2", "sha2",
"thiserror", "thiserror",
"tokio",
"uuid", "uuid",
"wasm-bindgen",
]
[[package]]
name = "hero-job-client"
version = "0.1.0"
dependencies = [
"chrono",
"hero-job",
"log",
"redis",
"serde_json",
"thiserror",
"tokio",
] ]
[[package]] [[package]]
@@ -618,6 +631,7 @@ dependencies = [
"env_logger 0.10.2", "env_logger 0.10.2",
"escargot", "escargot",
"hero-job", "hero-job",
"hero-job-client",
"hero-supervisor-openrpc-client", "hero-supervisor-openrpc-client",
"hyper", "hyper",
"hyper-util", "hyper-util",
@@ -646,6 +660,7 @@ dependencies = [
"env_logger 0.11.8", "env_logger 0.11.8",
"getrandom 0.2.16", "getrandom 0.2.16",
"hero-job", "hero-job",
"hero-job-client",
"hero-supervisor", "hero-supervisor",
"hex", "hex",
"indexmap", "indexmap",

View File

@@ -7,6 +7,8 @@ edition = "2021"
# Job types # Job types
hero-job = { path = "../job/rust" } hero-job = { path = "../job/rust" }
# hero-job = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust" } # hero-job = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust" }
hero-job-client = { path = "../job/rust/client" }
# hero-job-client = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust/client" }
# Async runtime # Async runtime
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }

File diff suppressed because it is too large Load Diff

View File

@@ -327,28 +327,38 @@ impl Component for App {
spawn_local(async move { spawn_local(async move {
match client.list_jobs().await { match client.list_jobs().await {
Ok(mut jobs) => { Ok(jobs_js) => {
// Fetch status for each job from Redis // Convert JsValue to Vec<serde_json::Value>
for job in &mut jobs { match serde_wasm_bindgen::from_value::<Vec<serde_json::Value>>(jobs_js) {
if let Some(job_id) = job.get("id").and_then(|v| v.as_str()) { Ok(mut jobs) => {
match client.get_job_status(job_id).await { // Fetch status for each job from Redis
Ok(status_response) => { for job in &mut jobs {
if let Some(status) = status_response.get("status").and_then(|v| v.as_str()) { if let Some(job_id) = job.get("id").and_then(|v| v.as_str()) {
if let Some(obj) = job.as_object_mut() { match client.get_job_status(job_id).await {
obj.insert("status".to_string(), serde_json::Value::String(status.to_string())); Ok(status_response) => {
if let Ok(status_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(status_response) {
if let Some(status) = status_obj.get("status").and_then(|v| v.as_str()) {
if let Some(obj) = job.as_object_mut() {
obj.insert("status".to_string(), serde_json::Value::String(status.to_string()));
}
}
}
}
Err(_) => {
// Job not found in Redis, likely not started yet
if let Some(obj) = job.as_object_mut() {
obj.insert("status".to_string(), serde_json::Value::String("queued".to_string()));
}
} }
} }
} }
Err(_) => {
// Job not found in Redis, likely not started yet
if let Some(obj) = job.as_object_mut() {
obj.insert("status".to_string(), serde_json::Value::String("queued".to_string()));
}
}
} }
link.send_message(Msg::JobsLoaded(Ok(jobs)));
}
Err(e) => {
link.send_message(Msg::JobsLoaded(Err(format!("Failed to parse jobs: {}", e))));
} }
} }
link.send_message(Msg::JobsLoaded(Ok(jobs)));
} }
Err(e) => { Err(e) => {
link.send_message(Msg::JobsLoaded(Err(format!("{:?}", e)))); link.send_message(Msg::JobsLoaded(Err(format!("{:?}", e))));
@@ -801,8 +811,10 @@ impl Component for App {
spawn_local(async move { spawn_local(async move {
match client.get_job_status(&job_id_clone).await { match client.get_job_status(&job_id_clone).await {
Ok(status_response) => { Ok(status_response) => {
if let Some(status) = status_response.get("status").and_then(|v| v.as_str()) { if let Ok(status_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(status_response) {
link.send_message(Msg::JobStatusUpdated(job_id_clone, status.to_string())); if let Some(status) = status_obj.get("status").and_then(|v| v.as_str()) {
link.send_message(Msg::JobStatusUpdated(job_id_clone, status.to_string()));
}
} }
} }
Err(e) => { Err(e) => {
@@ -884,10 +896,14 @@ impl Component for App {
match client.get_job_result(&job_id).await { match client.get_job_result(&job_id).await {
Ok(result) => { Ok(result) => {
// Extract the result string from the response // Extract the result string from the response
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) { let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
result_str.to_string() if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else { } else {
format!("{}", result) format!("{:?}", result)
}; };
link.send_message(Msg::JobOutputLoaded(Ok(output))); link.send_message(Msg::JobOutputLoaded(Ok(output)));
} }
@@ -899,6 +915,36 @@ impl Component for App {
} }
true true
} }
Msg::ViewJobLogs(job_id) => {
log::info!("View logs for job: {}", job_id);
self.viewing_job_output = Some(job_id.clone());
self.job_output = None; // Clear previous output
if let Some(client) = &self.client {
let client = client.clone();
let link = ctx.link().clone();
spawn_local(async move {
match client.get_job_logs(&job_id, Some(1000)).await {
Ok(logs_js) => {
// Convert JsValue to Vec<String>
match serde_wasm_bindgen::from_value::<Vec<String>>(logs_js) {
Ok(logs) => {
link.send_message(Msg::JobLogsLoaded(Ok(logs)));
}
Err(e) => {
link.send_message(Msg::JobLogsLoaded(Err(format!("Failed to parse logs: {}", e))));
}
}
}
Err(e) => {
link.send_message(Msg::JobLogsLoaded(Err(format!("{:?}", e))));
}
}
});
}
true
}
Msg::JobOutputLoaded(result) => { Msg::JobOutputLoaded(result) => {
match result { match result {
Ok(output) => { Ok(output) => {
@@ -967,10 +1013,14 @@ impl Component for App {
spawn_local(async move { spawn_local(async move {
match client_output.get_job_result(&job_id_output).await { match client_output.get_job_result(&job_id_output).await {
Ok(result) => { Ok(result) => {
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) { let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
result_str.to_string() if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else { } else {
format!("{}", result) format!("{:?}", result)
}; };
link_output.send_message(Msg::JobOutputLoaded(Ok(output))); link_output.send_message(Msg::JobOutputLoaded(Ok(output)));
} }
@@ -1019,10 +1069,14 @@ impl Component for App {
spawn_local(async move { spawn_local(async move {
match client_output.get_job_result(&job_id_output).await { match client_output.get_job_result(&job_id_output).await {
Ok(result) => { Ok(result) => {
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) { let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
result_str.to_string() if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else { } else {
format!("{}", result) format!("{:?}", result)
}; };
link_output.send_message(Msg::JobOutputLoaded(Ok(output))); link_output.send_message(Msg::JobOutputLoaded(Ok(output)));
} }
@@ -1948,6 +2002,7 @@ impl App {
let created_at = job.get("created_at").and_then(|v| v.as_str()).unwrap_or(""); let created_at = job.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
let signatures = job.get("signatures").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); let signatures = job.get("signatures").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
#[allow(unused_variables)]
let status_class = match status { let status_class = match status {
"created" => "status-created", "created" => "status-created",
"dispatched" | "queued" => "status-queued", "dispatched" | "queued" => "status-queued",
@@ -2337,8 +2392,8 @@ impl App {
{ {
if let Some(job) = job { if let Some(job) = job {
let payload = job.get("payload").and_then(|v| v.as_str()).unwrap_or(""); let payload = job.get("payload").and_then(|v| v.as_str()).unwrap_or("");
let runner = job.get("runner").and_then(|v| v.as_str()).unwrap_or(""); let _runner = job.get("runner").and_then(|v| v.as_str()).unwrap_or("");
let timeout = job.get("timeout").and_then(|v| v.as_u64()).unwrap_or(0); let _timeout = job.get("timeout").and_then(|v| v.as_u64()).unwrap_or(0);
let status = job.get("status").and_then(|v| v.as_str()).unwrap_or("unknown"); let status = job.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
let status_class = match status { let status_class = match status {

File diff suppressed because it is too large Load Diff

View File

@@ -18,16 +18,17 @@ serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
log = "0.4" log = "0.4"
uuid = { version = "1.0", features = ["v4", "serde"] } uuid = { version = "1.0", features = ["v4", "serde"] }
# Collections (common)
indexmap = "2.0" indexmap = "2.0"
hero-job = { path = "../../../job/rust" }
# hero-job = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust" }
# Native JSON-RPC client (not WASM compatible) # Native JSON-RPC client (not WASM compatible)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jsonrpsee = { version = "0.24", features = ["http-client", "macros"] } jsonrpsee = { version = "0.24", features = ["http-client", "macros"] }
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
hero-supervisor = { path = "../.." } hero-supervisor = { path = "../.." }
hero-job = { path = "../../../job/rust" } hero-job-client = { path = "../../../job/rust/client" }
# hero-job = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust" } # hero-job-client = { git = "https://git.ourworld.tf/herocode/job.git", subdirectory = "rust/client" }
env_logger = "0.11" env_logger = "0.11"
# WASM-specific dependencies # WASM-specific dependencies

View File

@@ -39,7 +39,7 @@ pub mod wasm;
// Re-export WASM types for convenience // Re-export WASM types for convenience
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub use wasm::{WasmSupervisorClient, WasmJob, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical}; pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical};
// Native client dependencies // Native client dependencies
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -158,13 +158,12 @@ pub struct JobStartResponse {
pub status: String, pub status: String,
} }
// Re-export Job types from hero-job crate (native only) // Re-export Job types from hero-job crate (both native and WASM)
#[cfg(not(target_arch = "wasm32"))] pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature};
pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature, Client, ClientBuilder};
// WASM-compatible Job types (simplified versions) // Re-export Client from hero-job-client (native only, requires Redis)
#[cfg(target_arch = "wasm32")] #[cfg(not(target_arch = "wasm32"))]
pub use crate::wasm::{Job, JobStatus, JobError, JobBuilder}; pub use hero_job_client::{Client, ClientBuilder};
/// Process status wrapper for OpenRPC serialization (matches server response) /// Process status wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -815,7 +814,7 @@ mod client_tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_wasm_job_creation() { fn test_wasm_job_creation() {
let job = crate::wasm::WasmJob::new( let job = crate::wasm::hero_job::Job::new(
"test-id".to_string(), "test-id".to_string(),
"test payload".to_string(), "test payload".to_string(),
"SAL".to_string(), "SAL".to_string(),
@@ -833,7 +832,7 @@ mod client_tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_wasm_job_setters() { fn test_wasm_job_setters() {
let mut job = crate::wasm::WasmJob::new( let mut job = crate::wasm::hero_job::Job::new(
"test-id".to_string(), "test-id".to_string(),
"test payload".to_string(), "test payload".to_string(),
"SAL".to_string(), "SAL".to_string(),
@@ -853,7 +852,7 @@ mod client_tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_wasm_job_id_generation() { fn test_wasm_job_id_generation() {
let mut job = crate::wasm::WasmJob::new( let mut job = crate::wasm::hero_job::Job::new(
"original-id".to_string(), "original-id".to_string(),
"test payload".to_string(), "test payload".to_string(),
"SAL".to_string(), "SAL".to_string(),
@@ -885,17 +884,17 @@ mod client_tests {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn test_wasm_job_type_enum() { fn test_wasm_job_type_enum() {
use crate::wasm::WasmJobType; use crate::wasm::hero_job::JobType;
// Test that enum variants exist and can be created // Test that enum variants exist and can be created
let sal = WasmJobType::SAL; let sal = hero_job::JobType::SAL;
let osis = WasmJobType::OSIS; let osis = hero_job::JobType::OSIS;
let v = WasmJobType::V; let v = hero_job::JobType::V;
// Test equality // Test equality
assert_eq!(sal, WasmJobType::SAL); assert_eq!(sal, hero_job::JobType::SAL);
assert_eq!(osis, WasmJobType::OSIS); assert_eq!(osis, hero_job::JobType::OSIS);
assert_eq!(v, WasmJobType::V); assert_eq!(v, hero_job::JobType::V);
// Test inequality // Test inequality
assert_ne!(sal, osis); assert_ne!(sal, osis);

View File

@@ -118,97 +118,9 @@ pub enum JobError {
Timeout, Timeout,
} }
/// Job builder for WASM // Re-export JobBuilder from hero-job for convenience
pub struct JobBuilder { pub use hero_job::JobBuilder;
id: Option<String>,
caller_id: Option<String>,
context_id: Option<String>,
payload: Option<String>,
runner: Option<String>,
executor: Option<String>,
timeout_secs: Option<u64>,
env_vars: Option<String>,
}
impl JobBuilder {
pub fn new() -> Self {
Self {
id: None,
caller_id: None,
context_id: None,
payload: None,
runner: None,
executor: None,
timeout_secs: None,
env_vars: None,
}
}
pub fn caller_id(mut self, caller_id: &str) -> Self {
self.caller_id = Some(caller_id.to_string());
self
}
pub fn context_id(mut self, context_id: &str) -> Self {
self.context_id = Some(context_id.to_string());
self
}
pub fn payload(mut self, payload: &str) -> Self {
self.payload = Some(payload.to_string());
self
}
pub fn runner(mut self, runner: &str) -> Self {
self.runner = Some(runner.to_string());
self
}
pub fn executor(mut self, executor: &str) -> Self {
self.executor = Some(executor.to_string());
self
}
pub fn timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = Some(timeout_secs);
self
}
pub fn build(self) -> Result<Job, JobError> {
let now = chrono::Utc::now().to_rfc3339();
Ok(Job {
id: self.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
caller_id: self.caller_id.ok_or_else(|| JobError::Validation("caller_id is required".to_string()))?,
context_id: self.context_id.ok_or_else(|| JobError::Validation("context_id is required".to_string()))?,
payload: self.payload.ok_or_else(|| JobError::Validation("payload is required".to_string()))?,
runner: self.runner.ok_or_else(|| JobError::Validation("runner is required".to_string()))?,
executor: self.executor.ok_or_else(|| JobError::Validation("executor is required".to_string()))?,
timeout_secs: self.timeout_secs.unwrap_or(30),
env_vars: self.env_vars.unwrap_or_else(|| "{}".to_string()),
created_at: now.clone(),
updated_at: now,
})
}
}
/// Job structure for creating and managing jobs (alias for WasmJob)
pub type Job = WasmJob;
/// Job structure for creating and managing jobs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen]
pub struct WasmJob {
id: String,
caller_id: String,
context_id: String,
payload: String,
runner: String,
executor: String,
timeout_secs: u64,
env_vars: String, // JSON string of HashMap<String, String>
created_at: String,
updated_at: String,
}
#[wasm_bindgen] #[wasm_bindgen]
impl WasmSupervisorClient { impl WasmSupervisorClient {
@@ -345,7 +257,7 @@ impl WasmSupervisorClient {
/// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth /// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth
#[wasm_bindgen] #[wasm_bindgen]
pub async fn create_job_with_secret(&self, secret: String, job: WasmJob) -> Result<String, JsValue> { pub async fn create_job_with_secret(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner // Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
let params = serde_json::json!([{ let params = serde_json::json!([{
"secret": secret, "secret": secret,
@@ -357,10 +269,10 @@ impl WasmSupervisorClient {
"runner": job.runner, "runner": job.runner,
"executor": job.executor, "executor": job.executor,
"timeout": { "timeout": {
"secs": job.timeout_secs, "secs": job.timeout,
"nanos": 0 "nanos": 0
}, },
"env_vars": serde_json::from_str::<serde_json::Value>(&job.env_vars).unwrap_or(serde_json::json!({})), "env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
"created_at": job.created_at, "created_at": job.created_at,
"updated_at": job.updated_at "updated_at": job.updated_at
} }
@@ -380,7 +292,7 @@ impl WasmSupervisorClient {
/// Run a job on a specific runner (blocking, returns result) /// Run a job on a specific runner (blocking, returns result)
#[wasm_bindgen] #[wasm_bindgen]
pub async fn run_job(&self, secret: String, job: WasmJob) -> Result<String, JsValue> { pub async fn run_job(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner // Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
let params = serde_json::json!([{ let params = serde_json::json!([{
"secret": secret, "secret": secret,
@@ -392,10 +304,10 @@ impl WasmSupervisorClient {
"runner": job.runner, "runner": job.runner,
"executor": job.executor, "executor": job.executor,
"timeout": { "timeout": {
"secs": job.timeout_secs, "secs": job.timeout,
"nanos": 0 "nanos": 0
}, },
"env_vars": serde_json::from_str::<serde_json::Value>(&job.env_vars).unwrap_or(serde_json::json!({})), "env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
"created_at": job.created_at, "created_at": job.created_at,
"updated_at": job.updated_at "updated_at": job.updated_at
} }
@@ -489,12 +401,24 @@ impl WasmSupervisorClient {
} }
} }
/// List all jobs
pub async fn list_jobs(&self) -> Result<JsValue, JsValue> {
match self.call_method("jobs.list", serde_json::Value::Null).await {
Ok(result) => {
// Convert serde_json::Value to JsValue
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert jobs list: {}", e)))
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Get a job by job ID /// Get a job by job ID
pub async fn get_job(&self, job_id: &str) -> Result<WasmJob, JsValue> { pub async fn get_job(&self, job_id: &str) -> Result<hero_job::Job, JsValue> {
let params = serde_json::json!([job_id]); let params = serde_json::json!([job_id]);
match self.call_method("get_job", params).await { match self.call_method("get_job", params).await {
Ok(result) => { Ok(result) => {
// Convert the Job result to WasmJob // Convert the Job result to hero_job::Job
if let Ok(job_value) = serde_json::from_value::<serde_json::Value>(result) { if let Ok(job_value) = serde_json::from_value::<serde_json::Value>(result) {
// Extract fields from the job // Extract fields from the job
let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
@@ -508,17 +432,22 @@ impl WasmSupervisorClient {
let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string(); let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string(); let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
Ok(WasmJob { Ok(hero_job::Job {
id, id,
caller_id, caller_id,
context_id, context_id,
payload, payload,
runner, runner,
executor, executor,
timeout_secs, timeout: timeout_secs,
env_vars, env_vars: serde_json::from_str(&env_vars).unwrap_or_default(),
created_at, created_at: chrono::DateTime::parse_from_rfc3339(&created_at)
updated_at, .map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now()),
updated_at: chrono::DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now()),
signatures: Vec::new(),
}) })
} else { } else {
Err(JsValue::from_str("Invalid response format for get_job")) Err(JsValue::from_str("Invalid response format for get_job"))
@@ -735,135 +664,6 @@ impl WasmSupervisorClient {
Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))), Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))),
} }
} }
}
#[wasm_bindgen]
impl WasmJob {
/// Create a new job with default values
#[wasm_bindgen(constructor)]
pub fn new(id: String, payload: String, executor: String, runner: String) -> Self {
let now = js_sys::Date::new_0().to_iso_string().as_string().unwrap();
Self {
id,
caller_id: "wasm_client".to_string(),
context_id: "wasm_context".to_string(),
payload,
runner,
executor,
timeout_secs: 30,
env_vars: "{}".to_string(),
created_at: now.clone(),
updated_at: now,
}
}
/// Set the caller ID
#[wasm_bindgen(setter)]
pub fn set_caller_id(&mut self, caller_id: String) {
self.caller_id = caller_id;
}
/// Set the context ID
#[wasm_bindgen(setter)]
pub fn set_context_id(&mut self, context_id: String) {
self.context_id = context_id;
}
/// Set the timeout in seconds
#[wasm_bindgen(setter)]
pub fn set_timeout_secs(&mut self, timeout_secs: u64) {
self.timeout_secs = timeout_secs;
}
/// Set environment variables as JSON string
#[wasm_bindgen(setter)]
pub fn set_env_vars(&mut self, env_vars: String) {
self.env_vars = env_vars;
}
/// Generate a new UUID for the job
#[wasm_bindgen]
pub fn generate_id(&mut self) {
self.id = Uuid::new_v4().to_string();
}
/// Get the job ID
#[wasm_bindgen(getter)]
pub fn id(&self) -> String {
self.id.clone()
}
/// Get the caller ID
#[wasm_bindgen(getter)]
pub fn caller_id(&self) -> String {
self.caller_id.clone()
}
/// Get the context ID
#[wasm_bindgen(getter)]
pub fn context_id(&self) -> String {
self.context_id.clone()
}
/// Get the payload
#[wasm_bindgen(getter)]
pub fn payload(&self) -> String {
self.payload.clone()
}
/// Get the job type
#[wasm_bindgen(getter)]
pub fn executor(&self) -> String {
self.executor.clone()
}
/// Get the runner name
#[wasm_bindgen(getter)]
pub fn runner(&self) -> String {
self.runner.clone()
}
/// Get the timeout in seconds
#[wasm_bindgen(getter)]
pub fn timeout_secs(&self) -> u64 {
self.timeout_secs
}
/// Get the environment variables as JSON string
#[wasm_bindgen(getter)]
pub fn env_vars(&self) -> String {
self.env_vars.clone()
}
/// Get the created timestamp
#[wasm_bindgen(getter)]
pub fn created_at(&self) -> String {
self.created_at.clone()
}
/// Get the updated timestamp
#[wasm_bindgen(getter)]
pub fn updated_at(&self) -> String {
self.updated_at.clone()
}
}
impl WasmSupervisorClient {
/// List all jobs (returns full job objects as Vec<serde_json::Value>)
/// This is not exposed to WASM directly due to type limitations
pub async fn list_jobs(&self) -> Result<Vec<serde_json::Value>, JsValue> {
let params = serde_json::json!([]);
match self.call_method("jobs.list", params).await {
Ok(result) => {
if let Ok(jobs) = serde_json::from_value::<Vec<serde_json::Value>>(result) {
Ok(jobs)
} else {
Err(JsValue::from_str("Invalid response format for jobs.list"))
}
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Start a previously created job by queuing it to its assigned runner /// Start a previously created job by queuing it to its assigned runner
pub async fn start_job(&self, job_id: &str) -> Result<(), JsValue> { pub async fn start_job(&self, job_id: &str) -> Result<(), JsValue> {
@@ -878,21 +678,23 @@ impl WasmSupervisorClient {
} }
/// Get the status of a job /// Get the status of a job
pub async fn get_job_status(&self, job_id: &str) -> Result<serde_json::Value, JsValue> { pub async fn get_job_status(&self, job_id: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([job_id]); let params = serde_json::json!([job_id]);
match self.call_method("job.status", params).await { match self.call_method("job.status", params).await {
Ok(result) => Ok(result), Ok(result) => serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
Err(e) => Err(JsValue::from_str(&e.to_string())), Err(e) => Err(JsValue::from_str(&e.to_string())),
} }
} }
/// Get the result of a completed job /// Get the result of a completed job
pub async fn get_job_result(&self, job_id: &str) -> Result<serde_json::Value, JsValue> { pub async fn get_job_result(&self, job_id: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([job_id]); let params = serde_json::json!([job_id]);
match self.call_method("job.result", params).await { match self.call_method("job.result", params).await {
Ok(result) => Ok(result), Ok(result) => serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
Err(e) => Err(JsValue::from_str(&e.to_string())), Err(e) => Err(JsValue::from_str(&e.to_string())),
} }
} }
@@ -972,13 +774,6 @@ pub fn init() {
log::info!("Hero Supervisor WASM OpenRPC Client initialized"); log::info!("Hero Supervisor WASM OpenRPC Client initialized");
} }
/// Utility function to create a job from JavaScript
/// Create a new job (convenience function for JavaScript)
#[wasm_bindgen]
pub fn create_job(id: String, payload: String, executor: String, runner: String) -> WasmJob {
WasmJob::new(id, payload, executor, runner)
}
/// Utility function to create a client from JavaScript /// Utility function to create a client from JavaScript
#[wasm_bindgen] #[wasm_bindgen]
pub fn create_client(server_url: String) -> WasmSupervisorClient { pub fn create_client(server_url: String) -> WasmSupervisorClient {

View File

@@ -2,41 +2,10 @@
set -e set -e
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
# Defaults echo "Building Hero Supervisor..."
OUTDIR="" cd "$PROJECT_DIR"
RELEASE=0 RUSTFLAGS="-A warnings" cargo build --release
CARGO_ARGS=""
usage() { echo "✅ Hero Supervisor built successfully"
cat <<EOF
Usage: $(basename "$0") [options]
Options:
--release Use cargo --release
--outdir <dir> Output directory (passed to cargo --dist)
--cargo-args "..." Extra arguments forwarded to cargo build
-h, --help Show this help
EOF
}
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--release) RELEASE=1; shift;;
--outdir) OUTDIR="$2"; shift 2;;
--cargo-args) CARGO_ARGS="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "❌ Unknown option: $1"; echo; usage; exit 1;;
esac
done
"$SCRIPT_DIR/install.sh"
set -x
cmd=(cargo build)
if [[ $RELEASE -eq 1 ]]; then cmd+=(--release); fi
if [[ -n "$OUTDIR" ]]; then cmd+=(--dist "$OUTDIR"); fi
if [[ -n "$CARGO_ARGS" ]]; then cmd+=($CARGO_ARGS); fi
"${cmd[@]}"
set +x

View File

@@ -1,70 +1,64 @@
#!/bin/bash #!/bin/bash
# Hero Supervisor Development Runner
# Runs both the supervisor backend and admin UI frontend
set -e set -e
# Colors for output SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
GREEN='\033[0;32m' PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" # Build first
echo -e "${BLUE}║ Starting Hero Supervisor Development Environment ║${NC}" "$SCRIPT_DIR/build.sh"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
# Function to cleanup background processes on exit # Configuration
REDIS_URL="${REDIS_URL:-redis://localhost:6379}"
PORT="${PORT:-3030}"
BIND_ADDRESS="${BIND_ADDRESS:-127.0.0.1}"
BOOTSTRAP_ADMIN_KEY="${BOOTSTRAP_ADMIN_KEY:-admin}"
ADMIN_UI_PORT="${ADMIN_UI_PORT:-8080}"
LOG_LEVEL="${LOG_LEVEL:-info}"
# Cleanup function
cleanup() { cleanup() {
echo -e "\n${YELLOW}Shutting down...${NC}" echo "Shutting down..."
kill $(jobs -p) 2>/dev/null || true kill $(jobs -p) 2>/dev/null || true
exit 0 exit 0
} }
trap cleanup SIGINT SIGTERM trap cleanup SIGINT SIGTERM
# Check if Redis is running echo "Starting Hero Supervisor..."
if ! pgrep -x "redis-server" > /dev/null; then cd "$PROJECT_DIR"
echo -e "${YELLOW}⚠️ Redis not detected. Starting Redis...${NC}"
redis-server --daemonize yes
sleep 1
fi
# Start the supervisor with bootstrap admin key # Start supervisor in background
echo -e "${GREEN}🚀 Starting Hero Supervisor...${NC}" RUST_LOG="$LOG_LEVEL" RUST_LOG_STYLE=never \
cargo run --bin supervisor -- \ target/release/supervisor \
--bootstrap-admin-key "admin" \ --bootstrap-admin-key "$BOOTSTRAP_ADMIN_KEY" \
--redis-url "redis://localhost:6379" \ --redis-url "$REDIS_URL" \
--port 3030 \ --port "$PORT" \
--bind-address "127.0.0.1" & --bind-address "$BIND_ADDRESS" &
SUPERVISOR_PID=$! SUPERVISOR_PID=$!
# Wait for supervisor to start # Wait for supervisor to start
echo -e "${BLUE}⏳ Waiting for supervisor to initialize...${NC}" sleep 2
sleep 3
# Start the admin UI # Check if supervisor is running
echo -e "${GREEN}🎨 Starting Admin UI...${NC}" if ! ps -p $SUPERVISOR_PID > /dev/null 2>&1; then
cd clients/admin-ui echo "Failed to start supervisor"
trunk serve --port 8080 & exit 1
fi
# Start admin UI
echo "Starting Admin UI on port $ADMIN_UI_PORT..."
cd "$PROJECT_DIR/clients/admin-ui"
trunk serve --port "$ADMIN_UI_PORT" &
ADMIN_UI_PID=$! ADMIN_UI_PID=$!
# Wait a bit for trunk to start echo ""
sleep 2 echo "✅ Hero Supervisor system started"
echo " 📡 Supervisor API: http://$BIND_ADDRESS:$PORT"
echo -e "\n${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" echo " 🎨 Admin UI: http://127.0.0.1:$ADMIN_UI_PORT"
echo -e "${GREEN}║ ✅ Development Environment Ready! ║${NC}" echo ""
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" echo "Press Ctrl+C to stop all services"
echo -e "${BLUE} 📡 Supervisor API:${NC} http://127.0.0.1:3030"
echo -e "${BLUE} 🎨 Admin UI:${NC} http://127.0.0.1:8080"
echo -e "${BLUE} 🔗 Redis:${NC} redis://localhost:6379"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e "\n${YELLOW}💡 Check the supervisor output above for your admin API key!${NC}"
echo -e "${YELLOW} Use it to login to the Admin UI at http://127.0.0.1:8080${NC}\n"
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}\n"
# Wait for both processes # Wait for both processes
wait wait

View File

@@ -1,2 +1,3 @@
// Re-export job types from the hero-job crate // Re-export job types from the hero-job crate
pub use hero_job::{Job, JobBuilder, JobStatus, JobError, Client, ClientBuilder}; pub use hero_job::{Job, JobBuilder, JobStatus, JobError};
use hero_job_client::{Client, ClientBuilder};

View File

@@ -17,7 +17,8 @@ pub mod mycelium;
pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus}; pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
// pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager}; // pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType}; pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
pub use hero_job::{Job, JobBuilder, JobStatus, JobError, Client, ClientBuilder}; pub use hero_job::{Job, JobBuilder, JobStatus, JobError};
use hero_job_client::{Client, ClientBuilder};
pub use app::SupervisorApp; pub use app::SupervisorApp;
#[cfg(feature = "mycelium")] #[cfg(feature = "mycelium")]

View File

@@ -84,24 +84,6 @@ impl Runner {
extra_args: config.extra_args, extra_args: config.extra_args,
} }
} }
/// Create a new runner with the given parameters
pub fn new(
id: String,
name: String,
namespace: String,
command: PathBuf,
redis_url: String,
) -> Self {
Self {
id,
name,
namespace,
command,
redis_url,
extra_args: Vec::new(),
}
}
/// Create a new runner with extra arguments /// Create a new runner with extra arguments
pub fn with_args( pub fn with_args(
@@ -189,6 +171,12 @@ pub enum RunnerError {
source: hero_job::JobError, source: hero_job::JobError,
}, },
#[error("Job client error: {source}")]
JobClientError {
#[from]
source: hero_job_client::ClientError,
},
#[error("Job '{job_id}' not found")] #[error("Job '{job_id}' not found")]
JobNotFound { job_id: String }, JobNotFound { job_id: String },

View File

@@ -75,7 +75,7 @@ use tokio::sync::Mutex;
// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager}; // use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}}; use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
use hero_job::{Client, ClientBuilder}; use hero_job_client::{Client, ClientBuilder};
/// Process manager type for a runner /// Process manager type for a runner
@@ -834,7 +834,7 @@ impl Supervisor {
// Use the client's get_status method // Use the client's get_status method
let status = self.client.get_status(job_id).await let status = self.client.get_status(job_id).await
.map_err(|e| match e { .map_err(|e| match e {
crate::job::JobError::NotFound(_) => RunnerError::JobNotFound { job_id: job_id.to_string() }, hero_job_client::ClientError::Job(hero_job::JobError::NotFound(_)) => RunnerError::JobNotFound { job_id: job_id.to_string() },
_ => RunnerError::from(e) _ => RunnerError::from(e)
})?; })?;
@@ -849,7 +849,7 @@ impl Supervisor {
// Use client's get_status to check if job exists and get its status // Use client's get_status to check if job exists and get its status
let status = self.client.get_status(job_id).await let status = self.client.get_status(job_id).await
.map_err(|e| match e { .map_err(|e| match e {
crate::job::JobError::NotFound(_) => RunnerError::JobNotFound { job_id: job_id.to_string() }, hero_job_client::ClientError::Job(hero_job::JobError::NotFound(_)) => RunnerError::JobNotFound { job_id: job_id.to_string() },
_ => RunnerError::from(e) _ => RunnerError::from(e)
})?; })?;