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

View File

@@ -7,6 +7,8 @@ edition = "2021"
# Job types
hero-job = { path = "../job/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
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 {
match client.list_jobs().await {
Ok(mut jobs) => {
// Fetch status for each job from Redis
for job in &mut jobs {
if let Some(job_id) = job.get("id").and_then(|v| v.as_str()) {
match client.get_job_status(job_id).await {
Ok(status_response) => {
if let Some(status) = status_response.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()));
Ok(jobs_js) => {
// Convert JsValue to Vec<serde_json::Value>
match serde_wasm_bindgen::from_value::<Vec<serde_json::Value>>(jobs_js) {
Ok(mut jobs) => {
// Fetch status for each job from Redis
for job in &mut jobs {
if let Some(job_id) = job.get("id").and_then(|v| v.as_str()) {
match client.get_job_status(job_id).await {
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) => {
link.send_message(Msg::JobsLoaded(Err(format!("{:?}", e))));
@@ -801,8 +811,10 @@ impl Component for App {
spawn_local(async move {
match client.get_job_status(&job_id_clone).await {
Ok(status_response) => {
if let Some(status) = status_response.get("status").and_then(|v| v.as_str()) {
link.send_message(Msg::JobStatusUpdated(job_id_clone, status.to_string()));
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()) {
link.send_message(Msg::JobStatusUpdated(job_id_clone, status.to_string()));
}
}
}
Err(e) => {
@@ -884,10 +896,14 @@ impl Component for App {
match client.get_job_result(&job_id).await {
Ok(result) => {
// Extract the result string from the response
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else {
format!("{}", result)
format!("{:?}", result)
};
link.send_message(Msg::JobOutputLoaded(Ok(output)));
}
@@ -899,6 +915,36 @@ impl Component for App {
}
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) => {
match result {
Ok(output) => {
@@ -967,10 +1013,14 @@ impl Component for App {
spawn_local(async move {
match client_output.get_job_result(&job_id_output).await {
Ok(result) => {
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else {
format!("{}", result)
format!("{:?}", result)
};
link_output.send_message(Msg::JobOutputLoaded(Ok(output)));
}
@@ -1019,10 +1069,14 @@ impl Component for App {
spawn_local(async move {
match client_output.get_job_result(&job_id_output).await {
Ok(result) => {
let output = if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
let output = if let Ok(result_obj) = serde_wasm_bindgen::from_value::<serde_json::Value>(result.clone()) {
if let Some(result_str) = result_obj.get("result").and_then(|v| v.as_str()) {
result_str.to_string()
} else {
format!("{:?}", result_obj)
}
} else {
format!("{}", result)
format!("{:?}", result)
};
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 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 {
"created" => "status-created",
"dispatched" | "queued" => "status-queued",
@@ -2337,8 +2392,8 @@ impl App {
{
if let Some(job) = job {
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 timeout = job.get("timeout").and_then(|v| v.as_u64()).unwrap_or(0);
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 status = job.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
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"
log = "0.4"
uuid = { version = "1.0", features = ["v4", "serde"] }
# Collections (common)
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)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jsonrpsee = { version = "0.24", features = ["http-client", "macros"] }
tokio = { version = "1.0", features = ["full"] }
hero-supervisor = { path = "../.." }
hero-job = { path = "../../../job/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" }
env_logger = "0.11"
# WASM-specific dependencies

View File

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

View File

@@ -118,97 +118,9 @@ pub enum JobError {
Timeout,
}
/// Job builder for WASM
pub struct 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>,
}
// Re-export JobBuilder from hero-job for convenience
pub use hero_job::JobBuilder;
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]
impl WasmSupervisorClient {
@@ -345,7 +257,7 @@ impl WasmSupervisorClient {
/// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth
#[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
let params = serde_json::json!([{
"secret": secret,
@@ -357,10 +269,10 @@ impl WasmSupervisorClient {
"runner": job.runner,
"executor": job.executor,
"timeout": {
"secs": job.timeout_secs,
"secs": job.timeout,
"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,
"updated_at": job.updated_at
}
@@ -380,7 +292,7 @@ impl WasmSupervisorClient {
/// Run a job on a specific runner (blocking, returns result)
#[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
let params = serde_json::json!([{
"secret": secret,
@@ -392,10 +304,10 @@ impl WasmSupervisorClient {
"runner": job.runner,
"executor": job.executor,
"timeout": {
"secs": job.timeout_secs,
"secs": job.timeout,
"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,
"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
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]);
match self.call_method("get_job", params).await {
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) {
// Extract fields from the job
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 updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
Ok(WasmJob {
Ok(hero_job::Job {
id,
caller_id,
context_id,
payload,
runner,
executor,
timeout_secs,
env_vars,
created_at,
updated_at,
timeout: timeout_secs,
env_vars: serde_json::from_str(&env_vars).unwrap_or_default(),
created_at: chrono::DateTime::parse_from_rfc3339(&created_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 {
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))),
}
}
}
#[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
pub async fn start_job(&self, job_id: &str) -> Result<(), JsValue> {
@@ -878,21 +678,23 @@ impl WasmSupervisorClient {
}
/// 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]);
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())),
}
}
/// 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]);
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())),
}
}
@@ -972,13 +774,6 @@ pub fn init() {
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
#[wasm_bindgen]
pub fn create_client(server_url: String) -> WasmSupervisorClient {

View File

@@ -2,41 +2,10 @@
set -e
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
# Defaults
OUTDIR=""
RELEASE=0
CARGO_ARGS=""
echo "Building Hero Supervisor..."
cd "$PROJECT_DIR"
RUSTFLAGS="-A warnings" cargo build --release
usage() {
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
echo "✅ Hero Supervisor built successfully"

View File

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

View File

@@ -1,2 +1,3 @@
// 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 sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
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;
#[cfg(feature = "mycelium")]

View File

@@ -84,24 +84,6 @@ impl Runner {
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
pub fn with_args(
@@ -189,6 +171,12 @@ pub enum RunnerError {
source: hero_job::JobError,
},
#[error("Job client error: {source}")]
JobClientError {
#[from]
source: hero_job_client::ClientError,
},
#[error("Job '{job_id}' not found")]
JobNotFound { job_id: String },

View File

@@ -75,7 +75,7 @@ use tokio::sync::Mutex;
// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
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
@@ -834,7 +834,7 @@ impl Supervisor {
// Use the client's get_status method
let status = self.client.get_status(job_id).await
.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)
})?;
@@ -849,7 +849,7 @@ impl Supervisor {
// Use client's get_status to check if job exists and get its status
let status = self.client.get_status(job_id).await
.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)
})?;