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

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 {