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:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
3640
clients/admin-ui/Cargo.lock
generated
3640
clients/admin-ui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
3688
clients/openrpc/Cargo.lock
generated
3688
clients/openrpc/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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};
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
@@ -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)
|
||||
})?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user