remove unused dep and move job out
This commit is contained in:
		
							
								
								
									
										153
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										153
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -331,15 +331,6 @@ dependencies = [
 | 
				
			|||||||
 "typenum",
 | 
					 "typenum",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "deranged"
 | 
					 | 
				
			||||||
version = "0.4.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "powerfmt",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "digest"
 | 
					name = "digest"
 | 
				
			||||||
version = "0.10.7"
 | 
					version = "0.10.7"
 | 
				
			||||||
@@ -437,7 +428,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "futures-channel",
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 "futures-executor",
 | 
					 | 
				
			||||||
 "futures-io",
 | 
					 "futures-io",
 | 
				
			||||||
 "futures-sink",
 | 
					 "futures-sink",
 | 
				
			||||||
 "futures-task",
 | 
					 "futures-task",
 | 
				
			||||||
@@ -460,17 +450,6 @@ version = "0.3.31"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
					checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "futures-executor"
 | 
					 | 
				
			||||||
version = "0.3.31"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "futures-core",
 | 
					 | 
				
			||||||
 "futures-task",
 | 
					 | 
				
			||||||
 "futures-util",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "futures-io"
 | 
					name = "futures-io"
 | 
				
			||||||
version = "0.3.31"
 | 
					version = "0.3.31"
 | 
				
			||||||
@@ -596,21 +575,50 @@ version = "0.5.2"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
					checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "hero-job"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "redis",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "uuid",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "hero-job"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					source = "git+https://git.ourworld.tf/herocode/job#1f7cd4ded8db57fb5ec3f7d42782fe45a70af164"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "redis",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "uuid",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hero-supervisor"
 | 
					name = "hero-supervisor"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "async-trait",
 | 
					 "async-trait",
 | 
				
			||||||
 | 
					 "base64",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "env_logger 0.10.2",
 | 
					 "env_logger 0.10.2",
 | 
				
			||||||
 "escargot",
 | 
					 "escargot",
 | 
				
			||||||
 | 
					 "hero-job 0.1.0 (git+https://git.ourworld.tf/herocode/job)",
 | 
				
			||||||
 "hero-supervisor-openrpc-client",
 | 
					 "hero-supervisor-openrpc-client",
 | 
				
			||||||
 "jsonrpsee",
 | 
					 "jsonrpsee",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 | 
					 "rand",
 | 
				
			||||||
 "redis",
 | 
					 "redis",
 | 
				
			||||||
 "sal-service-manager",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 "serde_json",
 | 
					 "serde_json",
 | 
				
			||||||
 "thiserror",
 | 
					 "thiserror",
 | 
				
			||||||
@@ -630,6 +638,7 @@ dependencies = [
 | 
				
			|||||||
 "console_log",
 | 
					 "console_log",
 | 
				
			||||||
 "env_logger 0.11.8",
 | 
					 "env_logger 0.11.8",
 | 
				
			||||||
 "getrandom 0.2.16",
 | 
					 "getrandom 0.2.16",
 | 
				
			||||||
 | 
					 "hero-job 0.1.0",
 | 
				
			||||||
 "hero-supervisor",
 | 
					 "hero-supervisor",
 | 
				
			||||||
 "js-sys",
 | 
					 "js-sys",
 | 
				
			||||||
 "jsonrpsee",
 | 
					 "jsonrpsee",
 | 
				
			||||||
@@ -1157,12 +1166,6 @@ dependencies = [
 | 
				
			|||||||
 "windows-sys 0.59.0",
 | 
					 "windows-sys 0.59.0",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "num-conv"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "num-traits"
 | 
					name = "num-traits"
 | 
				
			||||||
version = "0.2.19"
 | 
					version = "0.2.19"
 | 
				
			||||||
@@ -1260,19 +1263,6 @@ version = "0.1.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
					checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "plist"
 | 
					 | 
				
			||||||
version = "1.7.4"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "base64",
 | 
					 | 
				
			||||||
 "indexmap",
 | 
					 | 
				
			||||||
 "quick-xml",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "time",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "portable-atomic"
 | 
					name = "portable-atomic"
 | 
				
			||||||
version = "1.11.1"
 | 
					version = "1.11.1"
 | 
				
			||||||
@@ -1297,12 +1287,6 @@ dependencies = [
 | 
				
			|||||||
 "zerovec",
 | 
					 "zerovec",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "powerfmt"
 | 
					 | 
				
			||||||
version = "0.2.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "ppv-lite86"
 | 
					name = "ppv-lite86"
 | 
				
			||||||
version = "0.2.21"
 | 
					version = "0.2.21"
 | 
				
			||||||
@@ -1330,15 +1314,6 @@ dependencies = [
 | 
				
			|||||||
 "unicode-ident",
 | 
					 "unicode-ident",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "quick-xml"
 | 
					 | 
				
			||||||
version = "0.38.2"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "memchr",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "quote"
 | 
					name = "quote"
 | 
				
			||||||
version = "1.0.40"
 | 
					version = "1.0.40"
 | 
				
			||||||
@@ -1561,23 +1536,6 @@ version = "1.0.20"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 | 
					checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "sal-service-manager"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "async-trait",
 | 
					 | 
				
			||||||
 "chrono",
 | 
					 | 
				
			||||||
 "futures",
 | 
					 | 
				
			||||||
 "log",
 | 
					 | 
				
			||||||
 "once_cell",
 | 
					 | 
				
			||||||
 "plist",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "serde_json",
 | 
					 | 
				
			||||||
 "thiserror",
 | 
					 | 
				
			||||||
 "tokio",
 | 
					 | 
				
			||||||
 "zinit-client",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "same-file"
 | 
					name = "same-file"
 | 
				
			||||||
version = "1.0.6"
 | 
					version = "1.0.6"
 | 
				
			||||||
@@ -1815,37 +1773,6 @@ dependencies = [
 | 
				
			|||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time"
 | 
					 | 
				
			||||||
version = "0.3.41"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "deranged",
 | 
					 | 
				
			||||||
 "itoa",
 | 
					 | 
				
			||||||
 "num-conv",
 | 
					 | 
				
			||||||
 "powerfmt",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "time-core",
 | 
					 | 
				
			||||||
 "time-macros",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time-core"
 | 
					 | 
				
			||||||
version = "0.1.4"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time-macros"
 | 
					 | 
				
			||||||
version = "0.2.22"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "num-conv",
 | 
					 | 
				
			||||||
 "time-core",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tinystr"
 | 
					name = "tinystr"
 | 
				
			||||||
version = "0.8.1"
 | 
					version = "0.8.1"
 | 
				
			||||||
@@ -2667,21 +2594,3 @@ dependencies = [
 | 
				
			|||||||
 "quote",
 | 
					 "quote",
 | 
				
			||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "zinit-client"
 | 
					 | 
				
			||||||
version = "0.4.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "async-stream",
 | 
					 | 
				
			||||||
 "async-trait",
 | 
					 | 
				
			||||||
 "chrono",
 | 
					 | 
				
			||||||
 "futures",
 | 
					 | 
				
			||||||
 "rand",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "serde_json",
 | 
					 | 
				
			||||||
 "thiserror",
 | 
					 | 
				
			||||||
 "tokio",
 | 
					 | 
				
			||||||
 "tracing",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,8 @@ version = "0.1.0"
 | 
				
			|||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					# Shared job crate
 | 
				
			||||||
 | 
					hero-job = { git = "https://git.ourworld.tf/herocode/job" }
 | 
				
			||||||
# Async runtime
 | 
					# Async runtime
 | 
				
			||||||
tokio = { version = "1.0", features = ["full"] }
 | 
					tokio = { version = "1.0", features = ["full"] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,7 +25,6 @@ chrono = "0.4"
 | 
				
			|||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
env_logger = "0.10"
 | 
					env_logger = "0.10"
 | 
				
			||||||
sal-service-manager = { path = "../sal/service_manager" }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# CLI argument parsing
 | 
					# CLI argument parsing
 | 
				
			||||||
clap = { version = "4.0", features = ["derive"] }
 | 
					clap = { version = "4.0", features = ["derive"] }
 | 
				
			||||||
@@ -37,6 +38,12 @@ anyhow = "1.0"
 | 
				
			|||||||
tower-http = { version = "0.5", features = ["cors"] }
 | 
					tower-http = { version = "0.5", features = ["cors"] }
 | 
				
			||||||
tower = "0.4"
 | 
					tower = "0.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Base64 encoding for Mycelium payloads
 | 
				
			||||||
 | 
					base64 = "0.22"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Random number generation for message IDs
 | 
				
			||||||
 | 
					rand = "0.8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-dependencies]
 | 
					[dev-dependencies]
 | 
				
			||||||
tokio-test = "0.4"
 | 
					tokio-test = "0.4"
 | 
				
			||||||
hero-supervisor-openrpc-client = { path = "clients/openrpc" }
 | 
					hero-supervisor-openrpc-client = { path = "clients/openrpc" }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								clients/admin-ui/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								clients/admin-ui/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1025,6 +1025,19 @@ version = "0.5.2"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
					checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "hero-job"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "redis",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "uuid",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hero-supervisor"
 | 
					name = "hero-supervisor"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
@@ -1034,6 +1047,7 @@ dependencies = [
 | 
				
			|||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "env_logger 0.10.2",
 | 
					 "env_logger 0.10.2",
 | 
				
			||||||
 | 
					 "hero-job",
 | 
				
			||||||
 "jsonrpsee",
 | 
					 "jsonrpsee",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 "redis",
 | 
					 "redis",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
    <meta charset="utf-8" />
 | 
					    <meta charset="utf-8" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
				
			||||||
    <title>Hero Supervisor</title>
 | 
					    <title>Hero Supervisor</title>
 | 
				
			||||||
 | 
					    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
 | 
					    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
 | 
				
			||||||
    <link data-trunk rel="css" href="styles.css">
 | 
					    <link data-trunk rel="css" href="styles.css">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
use yew::prelude::*;
 | 
					use yew::prelude::*;
 | 
				
			||||||
use gloo::console;
 | 
					use gloo::console;
 | 
				
			||||||
use gloo::timers::callback::Interval;
 | 
					use gloo::timers::callback::Interval;
 | 
				
			||||||
 | 
					use gloo::storage::{LocalStorage, Storage};
 | 
				
			||||||
use wasm_bindgen_futures::spawn_local;
 | 
					use wasm_bindgen_futures::spawn_local;
 | 
				
			||||||
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
 | 
					use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
 | 
				
			||||||
use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType};
 | 
					use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType, SessionData, SESSION_STORAGE_KEY};
 | 
				
			||||||
use crate::runners::{Runners, RegisterForm};
 | 
					use crate::runners::{Runners, RegisterForm};
 | 
				
			||||||
use crate::jobs::Jobs;
 | 
					use crate::jobs::Jobs;
 | 
				
			||||||
 | 
					use crate::toast::{Toast, ToastContainer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Generate a unique job ID client-side using UUID v4
 | 
					/// Generate a unique job ID client-side using UUID v4
 | 
				
			||||||
fn generate_job_id() -> String {
 | 
					fn generate_job_id() -> String {
 | 
				
			||||||
@@ -17,7 +19,6 @@ pub struct JobForm {
 | 
				
			|||||||
    pub payload: String,
 | 
					    pub payload: String,
 | 
				
			||||||
    pub runner: String,
 | 
					    pub runner: String,
 | 
				
			||||||
    pub executor: String,
 | 
					    pub executor: String,
 | 
				
			||||||
    pub secret: String,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, PartialEq)]
 | 
					#[derive(Clone, Debug, PartialEq)]
 | 
				
			||||||
@@ -45,7 +46,10 @@ pub struct AppState {
 | 
				
			|||||||
    pub job_form: JobForm,
 | 
					    pub job_form: JobForm,
 | 
				
			||||||
    pub supervisor_info: Option<SupervisorInfo>,
 | 
					    pub supervisor_info: Option<SupervisorInfo>,
 | 
				
			||||||
    pub admin_secret: String,
 | 
					    pub admin_secret: String,
 | 
				
			||||||
 | 
					    pub session_secret: String,
 | 
				
			||||||
 | 
					    pub session_secret_type: SessionSecretType,
 | 
				
			||||||
    pub ping_states: std::collections::HashMap<String, PingState>, // runner -> ping_state
 | 
					    pub ping_states: std::collections::HashMap<String, PingState>, // runner -> ping_state
 | 
				
			||||||
 | 
					    pub toasts: Vec<Toast>, // Toast notifications
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,25 +58,36 @@ pub struct AppState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[function_component(App)]
 | 
					#[function_component(App)]
 | 
				
			||||||
pub fn app() -> Html {
 | 
					pub fn app() -> Html {
 | 
				
			||||||
    let state = use_state(|| AppState {
 | 
					    let state = use_state(|| {
 | 
				
			||||||
        server_url: "http://localhost:3030".to_string(),
 | 
					        // Try to load session from localStorage
 | 
				
			||||||
        runners: vec![],
 | 
					        let (session_secret, session_secret_type) = if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
 | 
				
			||||||
        jobs: vec![],
 | 
					            (session_data.secret, session_data.secret_type)
 | 
				
			||||||
        ongoing_jobs: vec![],
 | 
					        } else {
 | 
				
			||||||
        loading: false,
 | 
					            (String::new(), SessionSecretType::None)
 | 
				
			||||||
        register_form: RegisterForm {
 | 
					        };
 | 
				
			||||||
            name: String::new(),
 | 
					        
 | 
				
			||||||
            secret: String::new(),
 | 
					        AppState {
 | 
				
			||||||
        },
 | 
					            server_url: "http://localhost:3030".to_string(),
 | 
				
			||||||
        job_form: JobForm {
 | 
					            runners: vec![],
 | 
				
			||||||
            payload: String::new(),
 | 
					            jobs: vec![],
 | 
				
			||||||
            runner: String::new(),
 | 
					            ongoing_jobs: vec![],
 | 
				
			||||||
            executor: String::new(),
 | 
					            loading: false,
 | 
				
			||||||
            secret: String::new(),
 | 
					            register_form: RegisterForm {
 | 
				
			||||||
        },
 | 
					                name: String::new(),
 | 
				
			||||||
        supervisor_info: None,
 | 
					                secret: String::new(),
 | 
				
			||||||
        admin_secret: String::new(),
 | 
					            },
 | 
				
			||||||
        ping_states: std::collections::HashMap::new(),
 | 
					            job_form: JobForm {
 | 
				
			||||||
 | 
					                payload: String::new(),
 | 
				
			||||||
 | 
					                runner: String::new(),
 | 
				
			||||||
 | 
					                executor: String::new(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            supervisor_info: None,
 | 
				
			||||||
 | 
					            admin_secret: String::new(),
 | 
				
			||||||
 | 
					            session_secret,
 | 
				
			||||||
 | 
					            session_secret_type,
 | 
				
			||||||
 | 
					            ping_states: std::collections::HashMap::new(),
 | 
				
			||||||
 | 
					            toasts: vec![],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set up polling for ongoing jobs every 2 seconds
 | 
					    // Set up polling for ongoing jobs every 2 seconds
 | 
				
			||||||
@@ -131,6 +146,90 @@ pub fn app() -> Html {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check server connection status periodically
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        use_effect_with((), move |_| {
 | 
				
			||||||
 | 
					            let state = state.clone();
 | 
				
			||||||
 | 
					            let client_url = state.server_url.clone();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            let check_connection = {
 | 
				
			||||||
 | 
					                let state = state.clone();
 | 
				
			||||||
 | 
					                let client_url = client_url.clone();
 | 
				
			||||||
 | 
					                Callback::from(move |_| {
 | 
				
			||||||
 | 
					                    let state = state.clone();
 | 
				
			||||||
 | 
					                    let client_url = client_url.clone();
 | 
				
			||||||
 | 
					                    let client = WasmSupervisorClient::new(client_url.clone());
 | 
				
			||||||
 | 
					                    spawn_local(async move {
 | 
				
			||||||
 | 
					                        let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Try to ping the server to check connection
 | 
				
			||||||
 | 
					                        match client.list_runners().await {
 | 
				
			||||||
 | 
					                            Ok(_) => {
 | 
				
			||||||
 | 
					                                // Server is reachable, now try to load secrets if we have a session secret
 | 
				
			||||||
 | 
					                                let mut admin_secrets = vec![];
 | 
				
			||||||
 | 
					                                let mut user_secrets = vec![];
 | 
				
			||||||
 | 
					                                let mut register_secrets = vec![];
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // Try to load secrets based on current session secret
 | 
				
			||||||
 | 
					                                if !current_state.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                                    match current_state.session_secret_type {
 | 
				
			||||||
 | 
					                                        SessionSecretType::Admin => {
 | 
				
			||||||
 | 
					                                            if let Ok(secrets) = client.list_admin_secrets(¤t_state.session_secret).await {
 | 
				
			||||||
 | 
					                                                admin_secrets = secrets;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await {
 | 
				
			||||||
 | 
					                                                user_secrets = secrets;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await {
 | 
				
			||||||
 | 
					                                                register_secrets = secrets;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        SessionSecretType::User => {
 | 
				
			||||||
 | 
					                                            if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await {
 | 
				
			||||||
 | 
					                                                user_secrets = secrets;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        SessionSecretType::Register => {
 | 
				
			||||||
 | 
					                                            if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await {
 | 
				
			||||||
 | 
					                                                register_secrets = secrets;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        SessionSecretType::None => {}
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                current_state.supervisor_info = Some(SupervisorInfo {
 | 
				
			||||||
 | 
					                                    server_url: client_url.clone(),
 | 
				
			||||||
 | 
					                                    runners_count: 0,
 | 
				
			||||||
 | 
					                                    admin_secrets,
 | 
				
			||||||
 | 
					                                    user_secrets,
 | 
				
			||||||
 | 
					                                    register_secrets,
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            Err(_) => {
 | 
				
			||||||
 | 
					                                // Server is not reachable
 | 
				
			||||||
 | 
					                                current_state.supervisor_info = None;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        state.set(current_state);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Check connection immediately
 | 
				
			||||||
 | 
					            check_connection.emit(());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Set up interval to check connection every 5 seconds
 | 
				
			||||||
 | 
					            let interval = Interval::new(5000, move || {
 | 
				
			||||||
 | 
					                check_connection.emit(());
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            move || drop(interval)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Load initial data when component mounts
 | 
					    // Load initial data when component mounts
 | 
				
			||||||
    let load_initial_data = {
 | 
					    let load_initial_data = {
 | 
				
			||||||
        let state = state.clone();
 | 
					        let state = state.clone();
 | 
				
			||||||
@@ -262,9 +361,10 @@ pub fn app() -> Html {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::error!("Failed to load runners:", format!("{:?}", e));
 | 
					                        console::error!("Failed to load runners:", format!("{:?}", e));
 | 
				
			||||||
                        let mut updated_state = (*state).clone();
 | 
					                        let mut error_state = (*state).clone();
 | 
				
			||||||
                        updated_state.loading = false;
 | 
					                        error_state.loading = false;
 | 
				
			||||||
                        state.set(updated_state);
 | 
					                        error_state.toasts.push(Toast::error(format!("Failed to load runners: {:?}", e)));
 | 
				
			||||||
 | 
					                        state.set(error_state);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -294,7 +394,10 @@ pub fn app() -> Html {
 | 
				
			|||||||
                job_form: state.job_form.clone(),
 | 
					                job_form: state.job_form.clone(),
 | 
				
			||||||
                supervisor_info: state.supervisor_info.clone(),
 | 
					                supervisor_info: state.supervisor_info.clone(),
 | 
				
			||||||
                admin_secret: state.admin_secret.clone(),
 | 
					                admin_secret: state.admin_secret.clone(),
 | 
				
			||||||
 | 
					                session_secret: state.session_secret.clone(),
 | 
				
			||||||
 | 
					                session_secret_type: state.session_secret_type.clone(),
 | 
				
			||||||
                ping_states: state.ping_states.clone(),
 | 
					                ping_states: state.ping_states.clone(),
 | 
				
			||||||
 | 
					                toasts: state.toasts.clone(),
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            state.set(new_state);
 | 
					            state.set(new_state);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@@ -354,7 +457,6 @@ pub fn app() -> Html {
 | 
				
			|||||||
                "payload" => new_form.payload = value,
 | 
					                "payload" => new_form.payload = value,
 | 
				
			||||||
                "runner" => new_form.runner = value,
 | 
					                "runner" => new_form.runner = value,
 | 
				
			||||||
                "executor" => new_form.executor = value,
 | 
					                "executor" => new_form.executor = value,
 | 
				
			||||||
                "secret" => new_form.secret = value,
 | 
					 | 
				
			||||||
                _ => {}
 | 
					                _ => {}
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            let mut new_state = (*state).clone();
 | 
					            let mut new_state = (*state).clone();
 | 
				
			||||||
@@ -366,7 +468,7 @@ pub fn app() -> Html {
 | 
				
			|||||||
    // Run job callback - now uses create_job for immediate display and polling
 | 
					    // Run job callback - now uses create_job for immediate display and polling
 | 
				
			||||||
    let on_run_job = {
 | 
					    let on_run_job = {
 | 
				
			||||||
        let state = state.clone();
 | 
					        let state = state.clone();
 | 
				
			||||||
        Callback::from(move |_| {
 | 
					        Callback::from(move |_: ()| {
 | 
				
			||||||
            let current_state = (*state).clone();
 | 
					            let current_state = (*state).clone();
 | 
				
			||||||
            let client = WasmSupervisorClient::new(current_state.server_url.clone());
 | 
					            let client = WasmSupervisorClient::new(current_state.server_url.clone());
 | 
				
			||||||
            let job_form = current_state.job_form.clone();
 | 
					            let job_form = current_state.job_form.clone();
 | 
				
			||||||
@@ -396,7 +498,7 @@ pub fn app() -> Html {
 | 
				
			|||||||
                console::log!("Job added to list immediately with ID:", &job_id);
 | 
					                console::log!("Job added to list immediately with ID:", &job_id);
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // Create the job using fire-and-forget create_job method
 | 
					                // Create the job using fire-and-forget create_job method
 | 
				
			||||||
                match client.create_job(job_form.secret.clone(), job).await {
 | 
					                match client.create_job(current_state.session_secret.clone(), job).await {
 | 
				
			||||||
                    Ok(returned_job_id) => {
 | 
					                    Ok(returned_job_id) => {
 | 
				
			||||||
                        console::log!("Job created successfully with ID:", &returned_job_id);
 | 
					                        console::log!("Job created successfully with ID:", &returned_job_id);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@@ -415,10 +517,71 @@ pub fn app() -> Html {
 | 
				
			|||||||
    // Supervisor info loaded callback
 | 
					    // Supervisor info loaded callback
 | 
				
			||||||
    let on_supervisor_info_loaded = {
 | 
					    let on_supervisor_info_loaded = {
 | 
				
			||||||
        let state = state.clone();
 | 
					        let state = state.clone();
 | 
				
			||||||
        Callback::from(move |supervisor_info: SupervisorInfo| {
 | 
					        Callback::from(move |info: SupervisorInfo| {
 | 
				
			||||||
            let mut new_state = (*state).clone();
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
            new_state.supervisor_info = Some(supervisor_info);
 | 
					            current_state.supervisor_info = Some(info);
 | 
				
			||||||
            state.set(new_state);
 | 
					            state.set(current_state);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_add_secret = {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |(secret_type, _secret): (SessionSecretType, String)| {
 | 
				
			||||||
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					            if let Some(ref mut info) = current_state.supervisor_info {
 | 
				
			||||||
 | 
					                // Prompt for a new secret
 | 
				
			||||||
 | 
					                if let Some(window) = web_sys::window() {
 | 
				
			||||||
 | 
					                    if let Ok(Some(new_secret)) = window.prompt_with_message("Enter new secret:") {
 | 
				
			||||||
 | 
					                        if !new_secret.is_empty() {
 | 
				
			||||||
 | 
					                            match secret_type {
 | 
				
			||||||
 | 
					                                SessionSecretType::Admin => {
 | 
				
			||||||
 | 
					                                    if !info.admin_secrets.contains(&new_secret) {
 | 
				
			||||||
 | 
					                                        info.admin_secrets.push(new_secret);
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::User => {
 | 
				
			||||||
 | 
					                                    if !info.user_secrets.contains(&new_secret) {
 | 
				
			||||||
 | 
					                                        info.user_secrets.push(new_secret);
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::Register => {
 | 
				
			||||||
 | 
					                                    if !info.register_secrets.contains(&new_secret) {
 | 
				
			||||||
 | 
					                                        info.register_secrets.push(new_secret);
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::None => {
 | 
				
			||||||
 | 
					                                    // Do nothing for None type
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            state.set(current_state);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_remove_secret = {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |(secret_type, secret): (SessionSecretType, String)| {
 | 
				
			||||||
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					            if let Some(ref mut info) = current_state.supervisor_info {
 | 
				
			||||||
 | 
					                match secret_type {
 | 
				
			||||||
 | 
					                    SessionSecretType::Admin => {
 | 
				
			||||||
 | 
					                        info.admin_secrets.retain(|s| s != &secret);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    SessionSecretType::User => {
 | 
				
			||||||
 | 
					                        info.user_secrets.retain(|s| s != &secret);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    SessionSecretType::Register => {
 | 
				
			||||||
 | 
					                        info.register_secrets.retain(|s| s != &secret);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    SessionSecretType::None => {
 | 
				
			||||||
 | 
					                        // Do nothing for None type
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            state.set(current_state);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -439,10 +602,14 @@ pub fn app() -> Html {
 | 
				
			|||||||
                        // Remove runner from the list
 | 
					                        // Remove runner from the list
 | 
				
			||||||
                        let mut updated_state = (*state_clone).clone();
 | 
					                        let mut updated_state = (*state_clone).clone();
 | 
				
			||||||
                        updated_state.runners.retain(|(name, _)| name != &runner_id);
 | 
					                        updated_state.runners.retain(|(name, _)| name != &runner_id);
 | 
				
			||||||
 | 
					                        updated_state.toasts.push(Toast::success("Runner removed successfully".to_string()));
 | 
				
			||||||
                        state_clone.set(updated_state);
 | 
					                        state_clone.set(updated_state);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::error!("Failed to remove runner:", format!("{:?}", e));
 | 
					                        console::error!("Failed to remove runner:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                        let mut error_state = (*state_clone).clone();
 | 
				
			||||||
 | 
					                        error_state.toasts.push(Toast::error(format!("Failed to remove runner: {:?}", e)));
 | 
				
			||||||
 | 
					                        state_clone.set(error_state);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -470,6 +637,9 @@ pub fn app() -> Html {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::error!("Failed to stop job:", format!("{:?}", e));
 | 
					                        console::error!("Failed to stop job:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                        let mut error_state = (*state_clone).clone();
 | 
				
			||||||
 | 
					                        error_state.toasts.push(Toast::error(format!("Failed to stop job: {:?}", e)));
 | 
				
			||||||
 | 
					                        state_clone.set(error_state);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -498,16 +668,112 @@ pub fn app() -> Html {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::error!("Failed to delete job:", format!("{:?}", e));
 | 
					                        console::error!("Failed to delete job:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                        let mut error_state = (*state_clone).clone();
 | 
				
			||||||
 | 
					                        error_state.toasts.push(Toast::error(format!("Failed to delete job: {:?}", e)));
 | 
				
			||||||
 | 
					                        state_clone.set(error_state);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Ping runner callback - uses run_job for immediate result with proper state management
 | 
					    // Toast dismiss callback
 | 
				
			||||||
    let on_ping_runner = {
 | 
					    let on_toast_dismiss = {
 | 
				
			||||||
        let state = state.clone();
 | 
					        let state = state.clone();
 | 
				
			||||||
        Callback::from(move |(runner_id, secret): (String, String)| {
 | 
					        Callback::from(move |toast_id: String| {
 | 
				
			||||||
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					            current_state.toasts.retain(|t| t.id != toast_id);
 | 
				
			||||||
 | 
					            state.set(current_state);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add toast callback
 | 
				
			||||||
 | 
					    let add_toast = {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |toast: Toast| {
 | 
				
			||||||
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					            current_state.toasts.push(toast);
 | 
				
			||||||
 | 
					            state.set(current_state);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Session secret change callback
 | 
				
			||||||
 | 
					    let on_session_secret_change = {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |(secret, secret_type): (String, SessionSecretType)| {
 | 
				
			||||||
 | 
					            let mut current_state = (*state).clone();
 | 
				
			||||||
 | 
					            current_state.session_secret = secret.clone();
 | 
				
			||||||
 | 
					            current_state.session_secret_type = secret_type.clone();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // If we have a session secret, trigger API call to load secrets
 | 
				
			||||||
 | 
					            if !secret.is_empty() {
 | 
				
			||||||
 | 
					                let client = WasmSupervisorClient::new(current_state.server_url.clone());
 | 
				
			||||||
 | 
					                let state_clone = state.clone();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                spawn_local(async move {
 | 
				
			||||||
 | 
					                    let mut updated_state = (*state_clone).clone();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Try to load secrets from API based on secret type
 | 
				
			||||||
 | 
					                    match client.list_runners().await {
 | 
				
			||||||
 | 
					                        Ok(_) => {
 | 
				
			||||||
 | 
					                            let mut admin_secrets = vec![];
 | 
				
			||||||
 | 
					                            let mut user_secrets = vec![];
 | 
				
			||||||
 | 
					                            let mut register_secrets = vec![];
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Load secrets based on secret type
 | 
				
			||||||
 | 
					                            match secret_type {
 | 
				
			||||||
 | 
					                                SessionSecretType::Admin => {
 | 
				
			||||||
 | 
					                                    if let Ok(secrets) = client.list_admin_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                        admin_secrets = secrets;
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                    if let Ok(secrets) = client.list_user_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                        user_secrets = secrets;
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                    if let Ok(secrets) = client.list_register_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                        register_secrets = secrets;
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::User => {
 | 
				
			||||||
 | 
					                                    if let Ok(secrets) = client.list_user_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                        user_secrets = secrets;
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::Register => {
 | 
				
			||||||
 | 
					                                    if let Ok(secrets) = client.list_register_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                        register_secrets = secrets;
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                SessionSecretType::None => {}
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            updated_state.supervisor_info = Some(SupervisorInfo {
 | 
				
			||||||
 | 
					                                server_url: updated_state.server_url.clone(),
 | 
				
			||||||
 | 
					                                runners_count: 0,
 | 
				
			||||||
 | 
					                                admin_secrets,
 | 
				
			||||||
 | 
					                                user_secrets,
 | 
				
			||||||
 | 
					                                register_secrets,
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            updated_state.supervisor_info = None;
 | 
				
			||||||
 | 
					                            // Add error toast
 | 
				
			||||||
 | 
					                            let error_msg = format!("Failed to connect to supervisor: {:?}", e);
 | 
				
			||||||
 | 
					                            updated_state.toasts.push(Toast::error(error_msg));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    state_clone.set(updated_state);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            state.set(current_state);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Run job callback
 | 
				
			||||||
 | 
					    let on_run_job = {
 | 
				
			||||||
 | 
					        let state = state.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |(runner_id, payload): (String, String)| {
 | 
				
			||||||
            let current_state = (*state).clone();
 | 
					            let current_state = (*state).clone();
 | 
				
			||||||
            let client = WasmSupervisorClient::new(current_state.server_url.clone());
 | 
					            let client = WasmSupervisorClient::new(current_state.server_url.clone());
 | 
				
			||||||
            let state_clone = state.clone();
 | 
					            let state_clone = state.clone();
 | 
				
			||||||
@@ -520,26 +786,22 @@ pub fn app() -> Html {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            spawn_local(async move {
 | 
					            spawn_local(async move {
 | 
				
			||||||
                console::log!("Pinging runner:", &runner_id);
 | 
					                console::log!("Running job on runner:", &runner_id, "with payload:", &payload);
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // Generate unique job ID client-side
 | 
					                // Use session secret to create job with payload
 | 
				
			||||||
                let job_id = generate_job_id();
 | 
					                let job = WasmJob::new(
 | 
				
			||||||
                
 | 
					                    generate_job_id(),
 | 
				
			||||||
                // Create ping job with client-generated ID
 | 
					                    payload.clone(),
 | 
				
			||||||
                let ping_job = WasmJob::new(
 | 
					                    "default".to_string(),
 | 
				
			||||||
                    job_id.clone(),
 | 
					                    runner_id.clone()
 | 
				
			||||||
                    "ping".to_string(),
 | 
					 | 
				
			||||||
                    "ping".to_string(),
 | 
					 | 
				
			||||||
                    runner_id.clone(),
 | 
					 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                // Use run_job for immediate result instead of create_job
 | 
					                match client.run_job(current_state.session_secret, job).await {
 | 
				
			||||||
                match client.run_job(secret, ping_job).await {
 | 
					                    Ok(job_id) => {
 | 
				
			||||||
                    Ok(result) => {
 | 
					                        console::log!("Job created successfully:", &job_id);
 | 
				
			||||||
                        console::log!("Ping successful, result:", &result);
 | 
					                        // Set ping state to success with job ID
 | 
				
			||||||
                        // Set ping state to success with result
 | 
					 | 
				
			||||||
                        let mut success_state = (*state_clone).clone();
 | 
					                        let mut success_state = (*state_clone).clone();
 | 
				
			||||||
                        success_state.ping_states.insert(runner_id.clone(), PingState::Success(result));
 | 
					                        success_state.ping_states.insert(runner_id.clone(), PingState::Success(format!("Job created: {}", job_id)));
 | 
				
			||||||
                        state_clone.set(success_state);
 | 
					                        state_clone.set(success_state);
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        // Reset to idle after 3 seconds
 | 
					                        // Reset to idle after 3 seconds
 | 
				
			||||||
@@ -553,7 +815,7 @@ pub fn app() -> Html {
 | 
				
			|||||||
                        });
 | 
					                        });
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::error!("Failed to ping runner:", format!("{:?}", e));
 | 
					                        console::error!("Failed to create job:", format!("{:?}", e));
 | 
				
			||||||
                        // Set ping state to error
 | 
					                        // Set ping state to error
 | 
				
			||||||
                        let mut error_state = (*state_clone).clone();
 | 
					                        let mut error_state = (*state_clone).clone();
 | 
				
			||||||
                        let error_msg = format!("Error: {:?}", e);
 | 
					                        let error_msg = format!("Error: {:?}", e);
 | 
				
			||||||
@@ -585,45 +847,60 @@ pub fn app() -> Html {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    html! {
 | 
					    html! {
 | 
				
			||||||
        <div class="app-container">
 | 
					        <div class="bg-dark min-vh-100">
 | 
				
			||||||
            <Sidebar 
 | 
					            <div class="container-fluid h-100">
 | 
				
			||||||
                server_url={state.server_url.clone()}
 | 
					                <div class="row g-0 h-100">
 | 
				
			||||||
                supervisor_info={state.supervisor_info.clone()}
 | 
					                    <Sidebar 
 | 
				
			||||||
                session_secret={state.admin_secret.clone()}
 | 
					                        server_url={state.server_url.clone()}
 | 
				
			||||||
                session_secret_type={SessionSecretType::Admin}
 | 
					                        supervisor_info={state.supervisor_info.clone()}
 | 
				
			||||||
                on_session_secret_change={on_admin_secret_change}
 | 
					                        session_secret={state.session_secret.clone()}
 | 
				
			||||||
                on_supervisor_info_loaded={on_supervisor_info_loaded}
 | 
					                        session_secret_type={state.session_secret_type.clone()}
 | 
				
			||||||
            />
 | 
					                        on_session_secret_change={on_session_secret_change}
 | 
				
			||||||
 | 
					                        on_supervisor_info_loaded={on_supervisor_info_loaded}
 | 
				
			||||||
 | 
					                        on_add_secret={on_add_secret}
 | 
				
			||||||
 | 
					                        on_remove_secret={on_remove_secret}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
            <div class="main-content">
 | 
					                    <main class="col-md-9 col-lg-10 overflow-auto h-100">
 | 
				
			||||||
                <Runners 
 | 
					                    <div class="p-4 main-content">
 | 
				
			||||||
                    server_url={state.server_url.clone()}
 | 
					                        <Runners 
 | 
				
			||||||
                    runners={state.runners.clone()}
 | 
					                            server_url={state.server_url.clone()}
 | 
				
			||||||
                    register_form={state.register_form.clone()}
 | 
					                            runners={state.runners.clone()}
 | 
				
			||||||
                    ping_states={state.ping_states.clone()}
 | 
					                            register_form={state.register_form.clone()}
 | 
				
			||||||
                    on_register_form_change={on_register_form_change}
 | 
					                            ping_states={state.ping_states.clone()}
 | 
				
			||||||
                    on_register_runner={on_register_runner}
 | 
					                            on_register_form_change={on_register_form_change}
 | 
				
			||||||
                    on_load_runners={on_load_runners.clone()}
 | 
					                            on_register_runner={on_register_runner}
 | 
				
			||||||
                    on_remove_runner={on_remove_runner}
 | 
					                            on_load_runners={on_load_runners.clone()}
 | 
				
			||||||
                    on_ping_runner={on_ping_runner}
 | 
					                            on_remove_runner={on_remove_runner}
 | 
				
			||||||
                />
 | 
					                            session_secret={state.session_secret.clone()}
 | 
				
			||||||
 | 
					                            on_run_job={on_run_job.clone()}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                <Jobs 
 | 
					                        <Jobs 
 | 
				
			||||||
                    jobs={state.jobs.clone()}
 | 
					                            jobs={state.jobs.clone()}
 | 
				
			||||||
                    server_url={state.server_url.clone()}
 | 
					                            server_url={state.server_url.clone()}
 | 
				
			||||||
                    job_form={state.job_form.clone()}
 | 
					                            job_form={state.job_form.clone()}
 | 
				
			||||||
                    runners={state.runners.clone()}
 | 
					                            runners={state.runners.clone()}
 | 
				
			||||||
                    on_job_form_change={on_job_form_change}
 | 
					                            on_job_form_change={on_job_form_change}
 | 
				
			||||||
                    on_run_job={on_run_job}
 | 
					                            on_run_job={on_run_job}
 | 
				
			||||||
                    on_stop_job={on_stop_job}
 | 
					                            on_stop_job={on_stop_job}
 | 
				
			||||||
                    on_delete_job={on_delete_job}
 | 
					                            on_delete_job={on_delete_job}
 | 
				
			||||||
                />
 | 
					                        />
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                // Floating refresh button
 | 
					                        // Floating refresh button
 | 
				
			||||||
                <button class="refresh-btn" onclick={on_load_runners.reform(|_| ())}>
 | 
					                        <button class="btn btn-primary position-fixed" style="bottom: 20px; right: 20px; z-index: 1000;" onclick={on_load_runners.reform(|_| ())}>
 | 
				
			||||||
                    {"↻"}
 | 
					                            {"↻"}
 | 
				
			||||||
                </button>
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    </main>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Toast notifications
 | 
				
			||||||
 | 
					            <ToastContainer 
 | 
				
			||||||
 | 
					                toasts={state.toasts.clone()}
 | 
				
			||||||
 | 
					                on_dismiss={on_toast_dismiss}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ pub struct JobsProps {
 | 
				
			|||||||
    pub job_form: JobForm,
 | 
					    pub job_form: JobForm,
 | 
				
			||||||
    pub runners: Vec<(String, String)>, // (name, status) - list of registered runners
 | 
					    pub runners: Vec<(String, String)>, // (name, status) - list of registered runners
 | 
				
			||||||
    pub on_job_form_change: Callback<(String, String)>,
 | 
					    pub on_job_form_change: Callback<(String, String)>,
 | 
				
			||||||
    pub on_run_job: Callback<()>,
 | 
					    pub on_run_job: Callback<(String, String)>, // (runner, payload)
 | 
				
			||||||
    pub on_stop_job: Callback<String>,
 | 
					    pub on_stop_job: Callback<String>,
 | 
				
			||||||
    pub on_delete_job: Callback<String>,
 | 
					    pub on_delete_job: Callback<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -25,7 +25,6 @@ impl PartialEq for JobsProps {
 | 
				
			|||||||
        self.job_form.payload == other.job_form.payload &&
 | 
					        self.job_form.payload == other.job_form.payload &&
 | 
				
			||||||
        self.job_form.runner == other.job_form.runner &&
 | 
					        self.job_form.runner == other.job_form.runner &&
 | 
				
			||||||
        self.job_form.executor == other.job_form.executor &&
 | 
					        self.job_form.executor == other.job_form.executor &&
 | 
				
			||||||
        self.job_form.secret == other.job_form.secret &&
 | 
					 | 
				
			||||||
        self.runners.len() == other.runners.len()
 | 
					        self.runners.len() == other.runners.len()
 | 
				
			||||||
        // Note: Callbacks don't implement PartialEq, so we skip them
 | 
					        // Note: Callbacks don't implement PartialEq, so we skip them
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -57,18 +56,12 @@ pub fn jobs(props: &JobsProps) -> Html {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let on_secret_change = {
 | 
					 | 
				
			||||||
        let on_change = props.on_job_form_change.clone();
 | 
					 | 
				
			||||||
        Callback::from(move |e: Event| {
 | 
					 | 
				
			||||||
            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
 | 
					 | 
				
			||||||
            on_change.emit(("secret".to_string(), input.value()));
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let on_run_click = {
 | 
					    let on_run_click = {
 | 
				
			||||||
        let on_run = props.on_run_job.clone();
 | 
					        let on_run = props.on_run_job.clone();
 | 
				
			||||||
 | 
					        let job_form = props.job_form.clone();
 | 
				
			||||||
        Callback::from(move |_: MouseEvent| {
 | 
					        Callback::from(move |_: MouseEvent| {
 | 
				
			||||||
            on_run.emit(());
 | 
					            on_run.emit((job_form.runner.clone(), job_form.payload.clone()));
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,6 +77,7 @@ pub fn jobs(props: &JobsProps) -> Html {
 | 
				
			|||||||
                            <th>{"Runner"}</th>
 | 
					                            <th>{"Runner"}</th>
 | 
				
			||||||
                            <th>{"Executor"}</th>
 | 
					                            <th>{"Executor"}</th>
 | 
				
			||||||
                            <th>{"Status"}</th>
 | 
					                            <th>{"Status"}</th>
 | 
				
			||||||
 | 
					                            <th>{"Actions"}</th>
 | 
				
			||||||
                        </tr>
 | 
					                        </tr>
 | 
				
			||||||
                    </thead>
 | 
					                    </thead>
 | 
				
			||||||
                    <tbody>
 | 
					                    <tbody>
 | 
				
			||||||
@@ -126,14 +120,10 @@ pub fn jobs(props: &JobsProps) -> Html {
 | 
				
			|||||||
                                    onchange={on_executor_change}
 | 
					                                    onchange={on_executor_change}
 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
                            </td>
 | 
					                            </td>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                                <span class="status-badge status-not-started">{"Not Started"}</span>
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
                            <td class="action-cell">
 | 
					                            <td class="action-cell">
 | 
				
			||||||
                                <input 
 | 
					 | 
				
			||||||
                                    type="password" 
 | 
					 | 
				
			||||||
                                    class="form-control table-input secret-input" 
 | 
					 | 
				
			||||||
                                    placeholder="Secret"
 | 
					 | 
				
			||||||
                                    value={props.job_form.secret.clone()}
 | 
					 | 
				
			||||||
                                    onchange={on_secret_change}
 | 
					 | 
				
			||||||
                                />
 | 
					 | 
				
			||||||
                                <button 
 | 
					                                <button 
 | 
				
			||||||
                                    class="btn btn-primary btn-sm"
 | 
					                                    class="btn btn-primary btn-sm"
 | 
				
			||||||
                                    onclick={on_run_click}
 | 
					                                    onclick={on_run_click}
 | 
				
			||||||
@@ -157,8 +147,10 @@ pub fn jobs(props: &JobsProps) -> Html {
 | 
				
			|||||||
                                    <td><code class="code">{job.payload()}</code></td>
 | 
					                                    <td><code class="code">{job.payload()}</code></td>
 | 
				
			||||||
                                    <td>{job.runner()}</td>
 | 
					                                    <td>{job.runner()}</td>
 | 
				
			||||||
                                    <td>{job.executor()}</td>
 | 
					                                    <td>{job.executor()}</td>
 | 
				
			||||||
 | 
					                                    <td>
 | 
				
			||||||
 | 
					                                        <span class="status-badge status-pending">{"Pending"}</span>
 | 
				
			||||||
 | 
					                                    </td>
 | 
				
			||||||
                                    <td class="action-cell">
 | 
					                                    <td class="action-cell">
 | 
				
			||||||
                                        <span class="status-badge">{"Queued"}</span>
 | 
					 | 
				
			||||||
                                        <button 
 | 
					                                        <button 
 | 
				
			||||||
                                            class="btn-icon btn-stop"
 | 
					                                            class="btn-icon btn-stop"
 | 
				
			||||||
                                            title="Stop Job"
 | 
					                                            title="Stop Job"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
use wasm_bindgen::prelude::*;
 | 
					use wasm_bindgen::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod app;
 | 
					pub mod app;
 | 
				
			||||||
mod sidebar;
 | 
					pub mod sidebar;
 | 
				
			||||||
mod runners;
 | 
					pub mod runners;
 | 
				
			||||||
mod jobs;
 | 
					pub mod jobs;
 | 
				
			||||||
 | 
					pub mod toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[wasm_bindgen(start)]
 | 
					#[wasm_bindgen(start)]
 | 
				
			||||||
pub fn main() {
 | 
					pub fn main() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,11 +18,12 @@ pub struct RunnersProps {
 | 
				
			|||||||
    pub runners: Vec<(String, String)>, // (name, status)
 | 
					    pub runners: Vec<(String, String)>, // (name, status)
 | 
				
			||||||
    pub register_form: RegisterForm,
 | 
					    pub register_form: RegisterForm,
 | 
				
			||||||
    pub ping_states: HashMap<String, PingState>, // runner -> ping_state
 | 
					    pub ping_states: HashMap<String, PingState>, // runner -> ping_state
 | 
				
			||||||
 | 
					    pub session_secret: String,
 | 
				
			||||||
    pub on_register_form_change: Callback<(String, String)>,
 | 
					    pub on_register_form_change: Callback<(String, String)>,
 | 
				
			||||||
    pub on_register_runner: Callback<()>,
 | 
					    pub on_register_runner: Callback<()>,
 | 
				
			||||||
    pub on_load_runners: Callback<()>,
 | 
					    pub on_load_runners: Callback<()>,
 | 
				
			||||||
    pub on_remove_runner: Callback<String>,
 | 
					    pub on_remove_runner: Callback<String>,
 | 
				
			||||||
    pub on_ping_runner: Callback<(String, String)>, // (runner, secret)
 | 
					    pub on_run_job: Callback<(String, String)>, // (runner, payload)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[function_component(Runners)]
 | 
					#[function_component(Runners)]
 | 
				
			||||||
@@ -68,152 +69,137 @@ pub fn runners(props: &RunnersProps) -> Html {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    html! {
 | 
					    html! {
 | 
				
			||||||
        <div class="runners-grid">
 | 
					        <div class="mb-5">
 | 
				
			||||||
            // Registration card (first card)
 | 
					            <h2 class="mb-4">{"Runners"}</h2>
 | 
				
			||||||
            <div class="card register-card">
 | 
					            
 | 
				
			||||||
                <div class="card-title">{"+ Register Runner"}</div>
 | 
					            // All cards in same row - registration card first, then runner cards
 | 
				
			||||||
                <form onsubmit={on_register_runner.reform(|e: web_sys::SubmitEvent| {
 | 
					            <div class="d-flex flex-column gap-3">
 | 
				
			||||||
                    e.prevent_default();
 | 
					                // Registration card as first item
 | 
				
			||||||
                    ()
 | 
					                <div class="card bg-dark border-secondary">
 | 
				
			||||||
                })}>
 | 
					                    <div class="card-header bg-transparent border-secondary">
 | 
				
			||||||
                    <div class="form-group">
 | 
					                        <div class="d-flex align-items-center">
 | 
				
			||||||
                        <input 
 | 
					                            <i class="fas fa-plus-circle me-2 text-success"></i>
 | 
				
			||||||
                            type="text" 
 | 
					                            <h6 class="mb-0 text-white">{"Add New Runner"}</h6>
 | 
				
			||||||
                            class="form-control"
 | 
					                        </div>
 | 
				
			||||||
                            placeholder="Runner name"
 | 
					 | 
				
			||||||
                            value={props.register_form.name.clone()}
 | 
					 | 
				
			||||||
                            onchange={props.on_register_form_change.reform(|e: web_sys::Event| {
 | 
					 | 
				
			||||||
                                let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
					 | 
				
			||||||
                                ("name".to_string(), input.value())
 | 
					 | 
				
			||||||
                            })}
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="card-body">
 | 
				
			||||||
                    <div class="form-group form-row">
 | 
					                        <form onsubmit={on_register_runner.reform(|e: SubmitEvent| e.prevent_default())}>
 | 
				
			||||||
                        <input 
 | 
					                            <div class="mb-3">
 | 
				
			||||||
                            type="password" 
 | 
					                                <label class="form-label text-muted small">{"Runner Name"}</label>
 | 
				
			||||||
                            class="form-control form-control-inline"
 | 
					                                <input
 | 
				
			||||||
                            placeholder="Secret"
 | 
					                                    type="text"
 | 
				
			||||||
                            value={props.register_form.secret.clone()}
 | 
					                                    class="form-control bg-secondary border-0 text-white"
 | 
				
			||||||
                            onchange={props.on_register_form_change.reform(|e: web_sys::Event| {
 | 
					                                    placeholder="e.g., worker-01"
 | 
				
			||||||
                                let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
					                                    value={props.register_form.name.clone()}
 | 
				
			||||||
                                ("secret".to_string(), input.value())
 | 
					                                    oninput={props.on_register_form_change.reform(|e: InputEvent| {
 | 
				
			||||||
                            })}
 | 
					                                        let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
				
			||||||
                        />
 | 
					                                        ("name".to_string(), input.value())
 | 
				
			||||||
                        <button type="submit" class="btn btn-primary">
 | 
					                                    })}
 | 
				
			||||||
                            {"Register"}
 | 
					                                />
 | 
				
			||||||
                        </button>
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div class="mb-3">
 | 
				
			||||||
 | 
					                                <label class="form-label text-muted small">{"Registration Secret"}</label>
 | 
				
			||||||
 | 
					                                <input
 | 
				
			||||||
 | 
					                                    type="password"
 | 
				
			||||||
 | 
					                                    class="form-control bg-secondary border-0 text-white"
 | 
				
			||||||
 | 
					                                    placeholder="Enter secret key"
 | 
				
			||||||
 | 
					                                    value={props.register_form.secret.clone()}
 | 
				
			||||||
 | 
					                                    oninput={props.on_register_form_change.reform(|e: InputEvent| {
 | 
				
			||||||
 | 
					                                        let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
				
			||||||
 | 
					                                        ("secret".to_string(), input.value())
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <button type="submit" class="btn btn-success w-100">
 | 
				
			||||||
 | 
					                                <i class="fas fa-plus me-1"></i>
 | 
				
			||||||
 | 
					                                {"Register Runner"}
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </form>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </form>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
            // Existing runner cards
 | 
					                {for props.runners.iter().map(|(name, status)| {
 | 
				
			||||||
            {for props.runners.iter().map(|(name, status)| {
 | 
					                    let badge_class = match status.as_str() {
 | 
				
			||||||
                let status_class = match status.as_str() {
 | 
					                        "Running" => "status-running",
 | 
				
			||||||
                    "Running" => "status-running",
 | 
					                        "Stopped" => "status-stopped", 
 | 
				
			||||||
                    "Stopped" => "status-stopped",
 | 
					                        "Starting" => "status-starting",
 | 
				
			||||||
                    "Starting" => "status-starting",
 | 
					                        "Stopping" => "status-starting",
 | 
				
			||||||
                    "Stopping" => "status-starting",
 | 
					                        "Registering" => "status-registering",
 | 
				
			||||||
                    "Registering" => "status-registering",
 | 
					                        _ => "bg-secondary",
 | 
				
			||||||
                    _ => "status-stopped",
 | 
					                    };
 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                let name_clone = name.clone();
 | 
					                    let name_clone = name.clone();
 | 
				
			||||||
                let name_clone2 = name.clone();
 | 
					                    let name_clone2 = name.clone();
 | 
				
			||||||
                let on_remove = props.on_remove_runner.clone();
 | 
					                    let on_remove = props.on_remove_runner.clone();
 | 
				
			||||||
                let on_ping = props.on_ping_runner.clone();
 | 
					                    let on_run = props.on_run_job.clone();
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                html! {
 | 
					                    html! {
 | 
				
			||||||
                    <div class="card runner-card">
 | 
					                        <div class="card bg-dark border-secondary mb-3">
 | 
				
			||||||
                        <div class="card-header">
 | 
					                            <div class="card-body d-flex align-items-center justify-content-between p-3">
 | 
				
			||||||
                            <div class="runner-title-section">
 | 
					                                <div class="d-flex align-items-center flex-grow-1">
 | 
				
			||||||
                                <div class="runner-title-with-dot">
 | 
					                                    <div class="me-3">
 | 
				
			||||||
                                    <span class={format!("connection-dot {}", status_class)} title={status.clone()}>
 | 
					                                        <span class={classes!("badge", "rounded-pill", badge_class)}>{"●"}</span>
 | 
				
			||||||
                                        {"●"}
 | 
					                                    </div>
 | 
				
			||||||
                                    </span>
 | 
					                                    <div class="flex-grow-1">
 | 
				
			||||||
                                    <div class="card-title">{name}</div>
 | 
					                                        <h6 class="text-white mb-1">{name}</h6>
 | 
				
			||||||
 | 
					                                        <small class="text-muted">{format!("Queue: {}", name)}</small>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    <div class="me-3">
 | 
				
			||||||
 | 
					                                        <span class={classes!("badge", badge_class)}>
 | 
				
			||||||
 | 
					                                            {status}
 | 
				
			||||||
 | 
					                                        </span>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                <div class="d-flex gap-2">
 | 
				
			||||||
 | 
					                                    <button 
 | 
				
			||||||
 | 
					                                        class="btn btn-sm btn-outline-primary"
 | 
				
			||||||
 | 
					                                        title="Run Job"
 | 
				
			||||||
 | 
					                                        onclick={Callback::from(move |_| on_run.emit((name_clone.clone(), "test".to_string())))}
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                        <i class="fas fa-play"></i>
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                    <button 
 | 
				
			||||||
 | 
					                                        class="btn btn-sm btn-outline-danger"
 | 
				
			||||||
 | 
					                                        title="Remove Runner"
 | 
				
			||||||
 | 
					                                        onclick={Callback::from(move |_| on_remove.emit(name_clone2.clone()))}
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                        <i class="fas fa-trash"></i>
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                <div class="ms-3">
 | 
				
			||||||
 | 
					                                    {
 | 
				
			||||||
 | 
					                                        match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) {
 | 
				
			||||||
 | 
					                                            PingState::Idle => html! {
 | 
				
			||||||
 | 
					                                                <small class="text-muted">{"Ready"}</small>
 | 
				
			||||||
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            PingState::Waiting => html! {
 | 
				
			||||||
 | 
					                                                <small class="text-info">
 | 
				
			||||||
 | 
					                                                    <i class="fas fa-spinner fa-spin me-1"></i>
 | 
				
			||||||
 | 
					                                                    {"Working..."}
 | 
				
			||||||
 | 
					                                                </small>
 | 
				
			||||||
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            PingState::Success(ref msg) => html! {
 | 
				
			||||||
 | 
					                                                <small class="text-success">
 | 
				
			||||||
 | 
					                                                    <i class="fas fa-check me-1"></i>
 | 
				
			||||||
 | 
					                                                    {msg}
 | 
				
			||||||
 | 
					                                                </small>
 | 
				
			||||||
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            PingState::Error(ref msg) => html! {
 | 
				
			||||||
 | 
					                                                <small class="text-danger">
 | 
				
			||||||
 | 
					                                                    <i class="fas fa-times me-1"></i>
 | 
				
			||||||
 | 
					                                                    {msg}
 | 
				
			||||||
 | 
					                                                </small>
 | 
				
			||||||
 | 
					                                            },
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                                <small class="queue-info">
 | 
					 | 
				
			||||||
                                    {"redis://localhost:6379/runner:"}{name}
 | 
					 | 
				
			||||||
                                </small>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                            <div class="runner-actions-top">
 | 
					 | 
				
			||||||
                                <button 
 | 
					 | 
				
			||||||
                                    class="btn btn-sm btn-outline-secondary btn-remove"
 | 
					 | 
				
			||||||
                                    title="Remove Runner"
 | 
					 | 
				
			||||||
                                    onclick={Callback::from(move |_| on_remove.emit(name_clone2.clone()))}
 | 
					 | 
				
			||||||
                                >
 | 
					 | 
				
			||||||
                                    <svg class="trash-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
					 | 
				
			||||||
                                        <polyline points="3,6 5,6 21,6"></polyline>
 | 
					 | 
				
			||||||
                                        <path d="m5,6 1,14 c0,1.1 0.9,2 2,2 h8 c1.1,0 2,-0.9 2,-2 l1,-14"></path>
 | 
					 | 
				
			||||||
                                        <path d="m10,11 v6"></path>
 | 
					 | 
				
			||||||
                                        <path d="m14,11 v6"></path>
 | 
					 | 
				
			||||||
                                        <path d="M7,6V4c0-1.1,0.9-2,2-2h6c0-1.1,0.9-2,2-2v2"></path>
 | 
					 | 
				
			||||||
                                    </svg>
 | 
					 | 
				
			||||||
                                </button>
 | 
					 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="runner-chart">
 | 
					                    }
 | 
				
			||||||
                            <div class="chart-placeholder">
 | 
					                })}
 | 
				
			||||||
                                {"📊 Live job count chart (5s updates)"}
 | 
					            </div>
 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="ping-section">
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) {
 | 
					 | 
				
			||||||
                                    PingState::Idle => html! {
 | 
					 | 
				
			||||||
                                        <div class="input-group input-group-sm">
 | 
					 | 
				
			||||||
                                            <input 
 | 
					 | 
				
			||||||
                                                type="password" 
 | 
					 | 
				
			||||||
                                                class="form-control" 
 | 
					 | 
				
			||||||
                                                placeholder="Secret"
 | 
					 | 
				
			||||||
                                                id={format!("ping-secret-{}", name)}
 | 
					 | 
				
			||||||
                                            />
 | 
					 | 
				
			||||||
                                            <button 
 | 
					 | 
				
			||||||
                                                class="btn btn-outline-primary"
 | 
					 | 
				
			||||||
                                                title="Ping Runner"
 | 
					 | 
				
			||||||
                                                onclick={Callback::from(move |_| {
 | 
					 | 
				
			||||||
                                                    let window = web_sys::window().unwrap();
 | 
					 | 
				
			||||||
                                                    let document = window.document().unwrap();
 | 
					 | 
				
			||||||
                                                    let input_id = format!("ping-secret-{}", name_clone.clone());
 | 
					 | 
				
			||||||
                                                    if let Some(input) = document.get_element_by_id(&input_id) {
 | 
					 | 
				
			||||||
                                                        let input: web_sys::HtmlInputElement = input.dyn_into().unwrap();
 | 
					 | 
				
			||||||
                                                        let secret = input.value();
 | 
					 | 
				
			||||||
                                                        if !secret.is_empty() {
 | 
					 | 
				
			||||||
                                                            on_ping.emit((name_clone.clone(), secret));
 | 
					 | 
				
			||||||
                                                            input.set_value("");
 | 
					 | 
				
			||||||
                                                        }
 | 
					 | 
				
			||||||
                                                    }
 | 
					 | 
				
			||||||
                                                })}
 | 
					 | 
				
			||||||
                                            >
 | 
					 | 
				
			||||||
                                                {"Ping"}
 | 
					 | 
				
			||||||
                                            </button>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    PingState::Waiting => html! {
 | 
					 | 
				
			||||||
                                        <div class="ping-status ping-waiting">
 | 
					 | 
				
			||||||
                                            <span class="ping-spinner">{"⏳"}</span>
 | 
					 | 
				
			||||||
                                            <span>{"Waiting for response..."}</span>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    PingState::Success(result) => html! {
 | 
					 | 
				
			||||||
                                        <div class="ping-status ping-success">
 | 
					 | 
				
			||||||
                                            <span class="ping-icon">{"✅"}</span>
 | 
					 | 
				
			||||||
                                            <span>{format!("Success: {}", result)}</span>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    PingState::Error(error) => html! {
 | 
					 | 
				
			||||||
                                        <div class="ping-status ping-error">
 | 
					 | 
				
			||||||
                                            <span class="ping-icon">{"❌"}</span>
 | 
					 | 
				
			||||||
                                            <span>{error}</span>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })}
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,24 +2,44 @@ use yew::prelude::*;
 | 
				
			|||||||
use wasm_bindgen::JsCast;
 | 
					use wasm_bindgen::JsCast;
 | 
				
			||||||
use wasm_bindgen_futures::spawn_local;
 | 
					use wasm_bindgen_futures::spawn_local;
 | 
				
			||||||
use gloo::console;
 | 
					use gloo::console;
 | 
				
			||||||
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
 | 
					use gloo::storage::{LocalStorage, Storage};
 | 
				
			||||||
 | 
					use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, PartialEq)]
 | 
					#[derive(Clone, PartialEq)]
 | 
				
			||||||
pub struct SupervisorInfo {
 | 
					pub struct SupervisorInfo {
 | 
				
			||||||
    pub server_url: String,
 | 
					    pub server_url: String,
 | 
				
			||||||
    pub admin_secrets_count: usize,
 | 
					    pub admin_secrets: Vec<String>,
 | 
				
			||||||
    pub user_secrets_count: usize,
 | 
					    pub user_secrets: Vec<String>,
 | 
				
			||||||
    pub register_secrets_count: usize,
 | 
					    pub register_secrets: Vec<String>,
 | 
				
			||||||
    pub runners_count: usize,
 | 
					    pub runners_count: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, PartialEq, Debug)]
 | 
					#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
 | 
				
			||||||
pub enum SessionSecretType {
 | 
					pub enum SessionSecretType {
 | 
				
			||||||
    None,
 | 
					    None,
 | 
				
			||||||
    User,
 | 
					    User,
 | 
				
			||||||
    Admin,
 | 
					    Admin,
 | 
				
			||||||
 | 
					    Register,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Clone)]
 | 
				
			||||||
 | 
					pub struct SessionData {
 | 
				
			||||||
 | 
					    pub secret: String,
 | 
				
			||||||
 | 
					    pub secret_type: SessionSecretType,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize, Clone, Default)]
 | 
				
			||||||
 | 
					pub struct StoredSecrets {
 | 
				
			||||||
 | 
					    pub admin_secrets: Vec<String>,
 | 
				
			||||||
 | 
					    pub user_secrets: Vec<String>,
 | 
				
			||||||
 | 
					    pub register_secrets: Vec<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const STORED_SECRETS_KEY: &str = "supervisor_stored_secrets";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const SESSION_STORAGE_KEY: &str = "supervisor_session";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Properties, PartialEq)]
 | 
					#[derive(Properties, PartialEq)]
 | 
				
			||||||
pub struct SidebarProps {
 | 
					pub struct SidebarProps {
 | 
				
			||||||
    pub server_url: String,
 | 
					    pub server_url: String,
 | 
				
			||||||
@@ -28,17 +48,27 @@ pub struct SidebarProps {
 | 
				
			|||||||
    pub session_secret_type: SessionSecretType,
 | 
					    pub session_secret_type: SessionSecretType,
 | 
				
			||||||
    pub on_session_secret_change: Callback<(String, SessionSecretType)>,
 | 
					    pub on_session_secret_change: Callback<(String, SessionSecretType)>,
 | 
				
			||||||
    pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
 | 
					    pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
 | 
				
			||||||
 | 
					    pub on_add_secret: Callback<(SessionSecretType, String)>,
 | 
				
			||||||
 | 
					    pub on_remove_secret: Callback<(SessionSecretType, String)>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[function_component(Sidebar)]
 | 
					#[function_component(Sidebar)]
 | 
				
			||||||
pub fn sidebar(props: &SidebarProps) -> Html {
 | 
					pub fn sidebar(props: &SidebarProps) -> Html {
 | 
				
			||||||
    let session_secret_input = use_state(|| String::new());
 | 
					    let session_secret_input = use_state(|| String::new());
 | 
				
			||||||
    let payload_input = use_state(|| String::new());
 | 
					    let selected_secret_type = use_state(|| SessionSecretType::Admin);
 | 
				
			||||||
    let admin_secrets = use_state(|| Vec::<String>::new());
 | 
					 | 
				
			||||||
    let user_secrets = use_state(|| Vec::<String>::new());
 | 
					 | 
				
			||||||
    let register_secrets = use_state(|| Vec::<String>::new());
 | 
					 | 
				
			||||||
    let is_loading = use_state(|| false);
 | 
					    let is_loading = use_state(|| false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Load session from localStorage on component mount
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        use_effect_with((), move |_| {
 | 
				
			||||||
 | 
					            if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
 | 
				
			||||||
 | 
					                on_session_secret_change.emit((session_data.secret, session_data.secret_type));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            || ()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let on_session_secret_change = {
 | 
					    let on_session_secret_change = {
 | 
				
			||||||
        let session_secret_input = session_secret_input.clone();
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
        Callback::from(move |e: web_sys::Event| {
 | 
					        Callback::from(move |e: web_sys::Event| {
 | 
				
			||||||
@@ -49,12 +79,11 @@ pub fn sidebar(props: &SidebarProps) -> Html {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let on_session_secret_submit = {
 | 
					    let on_session_secret_submit = {
 | 
				
			||||||
        let session_secret_input = session_secret_input.clone();
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					        let selected_secret_type = selected_secret_type.clone();
 | 
				
			||||||
        let is_loading = is_loading.clone();
 | 
					        let is_loading = is_loading.clone();
 | 
				
			||||||
        let admin_secrets = admin_secrets.clone();
 | 
					 | 
				
			||||||
        let user_secrets = user_secrets.clone();
 | 
					 | 
				
			||||||
        let register_secrets = register_secrets.clone();
 | 
					 | 
				
			||||||
        let server_url = props.server_url.clone();
 | 
					 | 
				
			||||||
        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					        let server_url = props.server_url.clone();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        Callback::from(move |_: web_sys::MouseEvent| {
 | 
					        Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
            let secret = (*session_secret_input).clone();
 | 
					            let secret = (*session_secret_input).clone();
 | 
				
			||||||
@@ -63,346 +92,431 @@ pub fn sidebar(props: &SidebarProps) -> Html {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            is_loading.set(true);
 | 
					            is_loading.set(true);
 | 
				
			||||||
            let client = WasmSupervisorClient::new(server_url.clone());
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let session_secret_input = session_secret_input.clone();
 | 
					 | 
				
			||||||
            let is_loading = is_loading.clone();
 | 
					            let is_loading = is_loading.clone();
 | 
				
			||||||
            let admin_secrets = admin_secrets.clone();
 | 
					 | 
				
			||||||
            let user_secrets = user_secrets.clone();
 | 
					 | 
				
			||||||
            let register_secrets = register_secrets.clone();
 | 
					 | 
				
			||||||
            let on_session_secret_change = on_session_secret_change.clone();
 | 
					            let on_session_secret_change = on_session_secret_change.clone();
 | 
				
			||||||
 | 
					            let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					            let server_url = server_url.clone();
 | 
				
			||||||
 | 
					            let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					            let selected_secret_type = selected_secret_type.clone();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            spawn_local(async move {
 | 
					            spawn_local(async move {
 | 
				
			||||||
                // Try to get admin secrets first to determine if this is an admin secret
 | 
					                let client = WasmSupervisorClient::new(server_url.clone());
 | 
				
			||||||
                match client.list_admin_secrets(&secret).await {
 | 
					 | 
				
			||||||
                    Ok(admin_secret_list) => {
 | 
					 | 
				
			||||||
                        // This is an admin secret
 | 
					 | 
				
			||||||
                        admin_secrets.set(admin_secret_list);
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                        // Also load user and register secrets
 | 
					                match client.discover().await {
 | 
				
			||||||
                        if let Ok(user_secret_list) = client.list_user_secrets(&secret).await {
 | 
					                    Ok(_) => {
 | 
				
			||||||
                            user_secrets.set(user_secret_list);
 | 
					                        console::log!("Connected to supervisor successfully");
 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        if let Ok(register_secret_list) = client.list_register_secrets(&secret).await {
 | 
					 | 
				
			||||||
                            register_secrets.set(register_secret_list);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        on_session_secret_change.emit((secret, SessionSecretType::Admin));
 | 
					                        let secret_type = (*selected_secret_type).clone();
 | 
				
			||||||
                        console::log!("Admin session established");
 | 
					                        
 | 
				
			||||||
                    }
 | 
					                        // Don't store secrets in localStorage - use API only
 | 
				
			||||||
                    Err(_) => {
 | 
					                        
 | 
				
			||||||
                        // Try as user secret - just test if we can make any call with it
 | 
					                        // Save to localStorage
 | 
				
			||||||
                        match client.list_runners().await {
 | 
					                        let session_data = SessionData {
 | 
				
			||||||
                            Ok(_) => {
 | 
					                            secret: secret.clone(),
 | 
				
			||||||
                                // This appears to be a valid user secret
 | 
					                            secret_type: secret_type.clone(),
 | 
				
			||||||
                                on_session_secret_change.emit((secret, SessionSecretType::User));
 | 
					                        };
 | 
				
			||||||
                                console::log!("User session established");
 | 
					                        let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Create supervisor info with empty secrets initially
 | 
				
			||||||
 | 
					                        let mut supervisor_info = SupervisorInfo {
 | 
				
			||||||
 | 
					                            server_url: server_url.clone(),
 | 
				
			||||||
 | 
					                            admin_secrets: vec![],
 | 
				
			||||||
 | 
					                            user_secrets: vec![],
 | 
				
			||||||
 | 
					                            register_secrets: vec![],
 | 
				
			||||||
 | 
					                            runners_count: 0,
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Only fetch secrets if this is an admin secret
 | 
				
			||||||
 | 
					                        if secret_type == SessionSecretType::Admin {
 | 
				
			||||||
 | 
					                            console::log!("Attempting to fetch secrets with admin secret");
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Try to fetch admin secrets
 | 
				
			||||||
 | 
					                            match client.list_admin_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                Ok(admin_secrets) => {
 | 
				
			||||||
 | 
					                                    console::log!("✅ Fetched admin secrets:", format!("{:?}", admin_secrets));
 | 
				
			||||||
 | 
					                                    supervisor_info.admin_secrets = admin_secrets;
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                Err(e) => {
 | 
				
			||||||
 | 
					                                    console::error!("❌ Failed to fetch admin secrets:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                                    // If admin secret fetch fails, this might not be a valid admin secret
 | 
				
			||||||
 | 
					                                    supervisor_info.admin_secrets = vec![secret.clone()];
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            Err(e) => {
 | 
					                            
 | 
				
			||||||
                                console::log!("Invalid secret:", format!("{:?}", e));
 | 
					                            // Try to fetch user secrets
 | 
				
			||||||
                                on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
					                            match client.list_user_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                Ok(user_secrets) => {
 | 
				
			||||||
 | 
					                                    console::log!("✅ Fetched user secrets:", format!("{:?}", user_secrets));
 | 
				
			||||||
 | 
					                                    supervisor_info.user_secrets = user_secrets;
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                Err(e) => {
 | 
				
			||||||
 | 
					                                    console::error!("❌ Failed to fetch user secrets:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Try to fetch register secrets
 | 
				
			||||||
 | 
					                            match client.list_register_secrets(&secret).await {
 | 
				
			||||||
 | 
					                                Ok(register_secrets) => {
 | 
				
			||||||
 | 
					                                    console::log!("✅ Fetched register secrets:", format!("{:?}", register_secrets));
 | 
				
			||||||
 | 
					                                    supervisor_info.register_secrets = register_secrets;
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                Err(e) => {
 | 
				
			||||||
 | 
					                                    console::error!("❌ Failed to fetch register secrets:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            console::log!("Non-admin secret - showing only current secret");
 | 
				
			||||||
 | 
					                            // For non-admin secrets, only show the current secret
 | 
				
			||||||
 | 
					                            match secret_type {
 | 
				
			||||||
 | 
					                                SessionSecretType::User => {
 | 
				
			||||||
 | 
					                                    supervisor_info.user_secrets = vec![secret.clone()];
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                SessionSecretType::Register => {
 | 
				
			||||||
 | 
					                                    supervisor_info.register_secrets = vec![secret.clone()];
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                _ => {}
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                is_loading.set(false);
 | 
					                        on_session_secret_change.emit((secret.clone(), secret_type.clone()));
 | 
				
			||||||
                session_secret_input.set(String::new());
 | 
					                        on_supervisor_info_loaded.emit(supervisor_info);
 | 
				
			||||||
            });
 | 
					                        session_secret_input.set(String::new());
 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let on_session_clear = {
 | 
					 | 
				
			||||||
        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
					 | 
				
			||||||
        let admin_secrets = admin_secrets.clone();
 | 
					 | 
				
			||||||
        let user_secrets = user_secrets.clone();
 | 
					 | 
				
			||||||
        let register_secrets = register_secrets.clone();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Callback::from(move |_: web_sys::MouseEvent| {
 | 
					 | 
				
			||||||
            on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
					 | 
				
			||||||
            admin_secrets.set(Vec::new());
 | 
					 | 
				
			||||||
            user_secrets.set(Vec::new());
 | 
					 | 
				
			||||||
            register_secrets.set(Vec::new());
 | 
					 | 
				
			||||||
            console::log!("Session cleared");
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let on_payload_change = {
 | 
					 | 
				
			||||||
        let payload_input = payload_input.clone();
 | 
					 | 
				
			||||||
        Callback::from(move |e: web_sys::Event| {
 | 
					 | 
				
			||||||
            let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
					 | 
				
			||||||
            payload_input.set(input.value());
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let on_run_click = {
 | 
					 | 
				
			||||||
        let payload_input = payload_input.clone();
 | 
					 | 
				
			||||||
        let server_url = props.server_url.clone();
 | 
					 | 
				
			||||||
        let session_secret = props.session_secret.clone();
 | 
					 | 
				
			||||||
        let is_loading = is_loading.clone();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Callback::from(move |_: web_sys::MouseEvent| {
 | 
					 | 
				
			||||||
            let payload = (*payload_input).clone();
 | 
					 | 
				
			||||||
            if payload.is_empty() || session_secret.is_empty() {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            is_loading.set(true);
 | 
					 | 
				
			||||||
            let client = WasmSupervisorClient::new(server_url.clone());
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let payload_input = payload_input.clone();
 | 
					 | 
				
			||||||
            let is_loading = is_loading.clone();
 | 
					 | 
				
			||||||
            let session_secret = session_secret.clone();
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            spawn_local(async move {
 | 
					 | 
				
			||||||
                // Create WasmJob object using constructor
 | 
					 | 
				
			||||||
                let job = WasmJob::new(
 | 
					 | 
				
			||||||
                    uuid::Uuid::new_v4().to_string(),
 | 
					 | 
				
			||||||
                    payload.clone(),
 | 
					 | 
				
			||||||
                    "osis".to_string(),
 | 
					 | 
				
			||||||
                    "default".to_string(),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                match client.create_job(session_secret.clone(), job).await {
 | 
					 | 
				
			||||||
                    Ok(job_id) => {
 | 
					 | 
				
			||||||
                        console::log!("Job created successfully:", job_id);
 | 
					 | 
				
			||||||
                        payload_input.set(String::new());
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    Err(e) => {
 | 
					                    Err(e) => {
 | 
				
			||||||
                        console::log!("Failed to create job:", format!("{:?}", e));
 | 
					                        console::error!("Failed to connect to supervisor:", format!("{:?}", e));
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
                is_loading.set(false);
 | 
					                is_loading.set(false);
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_logout = {
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					            // Clear localStorage
 | 
				
			||||||
 | 
					            let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
 | 
				
			||||||
 | 
					            on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_add_admin_secret = {
 | 
				
			||||||
 | 
					        let on_add_secret = props.on_add_secret.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |_| {
 | 
				
			||||||
 | 
					            on_add_secret.emit((SessionSecretType::Admin, String::new()));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_add_user_secret = {
 | 
				
			||||||
 | 
					        let on_add_secret = props.on_add_secret.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |_| {
 | 
				
			||||||
 | 
					            on_add_secret.emit((SessionSecretType::User, String::new()));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_add_register_secret = {
 | 
				
			||||||
 | 
					        let on_add_secret = props.on_add_secret.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |_| {
 | 
				
			||||||
 | 
					            on_add_secret.emit((SessionSecretType::User, String::new()));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_session_input = {
 | 
				
			||||||
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |e: InputEvent| {
 | 
				
			||||||
 | 
					            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
 | 
				
			||||||
 | 
					            session_secret_input.set(input.value());
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_session_keypress = {
 | 
				
			||||||
 | 
					        let on_session_secret_submit = on_session_secret_submit.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |e: KeyboardEvent| {
 | 
				
			||||||
 | 
					            if e.key() == "Enter" {
 | 
				
			||||||
 | 
					                e.prevent_default();
 | 
				
			||||||
 | 
					                // Create a dummy MouseEvent to trigger the submit handler
 | 
				
			||||||
 | 
					                let dummy_event = web_sys::MouseEvent::new("click").unwrap();
 | 
				
			||||||
 | 
					                on_session_secret_submit.emit(dummy_event);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_session_toggle = {
 | 
				
			||||||
 | 
					        let on_session_secret_submit = on_session_secret_submit.clone();
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        let session_secret = props.session_secret.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |e: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					            if session_secret.is_empty() {
 | 
				
			||||||
 | 
					                // Try to login with current input
 | 
				
			||||||
 | 
					                on_session_secret_submit.emit(e);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Logout - clear localStorage and session
 | 
				
			||||||
 | 
					                let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
 | 
				
			||||||
 | 
					                on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Function to refresh secrets from API when switching to an admin secret
 | 
				
			||||||
 | 
					    let refresh_secrets_from_api = {
 | 
				
			||||||
 | 
					        let server_url = props.server_url.clone();
 | 
				
			||||||
 | 
					        let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |(secret, secret_type): (String, SessionSecretType)| {
 | 
				
			||||||
 | 
					            if secret_type == SessionSecretType::Admin {
 | 
				
			||||||
 | 
					                let client = WasmSupervisorClient::new(server_url.clone());
 | 
				
			||||||
 | 
					                let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					                let server_url = server_url.clone();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                spawn_local(async move {
 | 
				
			||||||
 | 
					                    let mut supervisor_info = SupervisorInfo {
 | 
				
			||||||
 | 
					                        server_url: server_url.clone(),
 | 
				
			||||||
 | 
					                        admin_secrets: vec![],
 | 
				
			||||||
 | 
					                        user_secrets: vec![],
 | 
				
			||||||
 | 
					                        register_secrets: vec![],
 | 
				
			||||||
 | 
					                        runners_count: 0,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Fetch real secrets from the API
 | 
				
			||||||
 | 
					                    match client.list_admin_secrets(&secret).await {
 | 
				
			||||||
 | 
					                        Ok(admin_secrets) => {
 | 
				
			||||||
 | 
					                            console::log!("Refreshed admin secrets from API:", format!("{:?}", admin_secrets));
 | 
				
			||||||
 | 
					                            supervisor_info.admin_secrets = admin_secrets;
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        Err(e) => console::error!("Failed to refresh admin secrets:", format!("{:?}", e))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    match client.list_user_secrets(&secret).await {
 | 
				
			||||||
 | 
					                        Ok(user_secrets) => {
 | 
				
			||||||
 | 
					                            console::log!("Refreshed user secrets from API:", format!("{:?}", user_secrets));
 | 
				
			||||||
 | 
					                            supervisor_info.user_secrets = user_secrets;
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        Err(e) => console::error!("Failed to refresh user secrets:", format!("{:?}", e))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    match client.list_register_secrets(&secret).await {
 | 
				
			||||||
 | 
					                        Ok(register_secrets) => {
 | 
				
			||||||
 | 
					                            console::log!("Refreshed register secrets from API:", format!("{:?}", register_secrets));
 | 
				
			||||||
 | 
					                            supervisor_info.register_secrets = register_secrets;
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        Err(e) => console::error!("Failed to refresh register secrets:", format!("{:?}", e))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    on_supervisor_info_loaded.emit(supervisor_info);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    html! {
 | 
					    html! {
 | 
				
			||||||
        <div class="sidebar">
 | 
					        <div class="col-md-3 col-lg-2 d-md-block sidebar">
 | 
				
			||||||
            <div class="sidebar-header">
 | 
					            <div class="bg-dark rounded m-2 h-100 d-flex flex-column p-3 sidebar-island">
 | 
				
			||||||
                <h2>{"Supervisor"}</h2>
 | 
					                // Header section
 | 
				
			||||||
            </div>
 | 
					                <div class="pb-3 border-bottom border-secondary">
 | 
				
			||||||
            <div class="sidebar-content">
 | 
					                    <h5 class="text-white mb-1">{"Supervisor"}</h5>
 | 
				
			||||||
                <div class="sidebar-sections">
 | 
					                    <small class="text-muted">{"Admin interface for managing jobs and secrets"}</small>
 | 
				
			||||||
                    // Server Info Section
 | 
					 | 
				
			||||||
                    <div class="server-info">
 | 
					 | 
				
			||||||
                        <div class="server-header">
 | 
					 | 
				
			||||||
                            <h3 class="supervisor-title">{"Hero Supervisor"}</h3>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="server-url">
 | 
					 | 
				
			||||||
                            <span class="connection-indicator connected"></span>
 | 
					 | 
				
			||||||
                            <span class="url-text">{props.server_url.clone()}</span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // Session Secret Management Section
 | 
					 | 
				
			||||||
                    <div class="session-section">
 | 
					 | 
				
			||||||
                        <div class="session-header">
 | 
					 | 
				
			||||||
                            <span class="session-title">{"Session"}</span>
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                match props.session_secret_type {
 | 
					 | 
				
			||||||
                                    SessionSecretType::Admin => html! {
 | 
					 | 
				
			||||||
                                        <span class="session-badge admin">{"Admin"}</span>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    SessionSecretType::User => html! {
 | 
					 | 
				
			||||||
                                        <span class="session-badge user">{"User"}</span>
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                    SessionSecretType::None => html! {
 | 
					 | 
				
			||||||
                                        <span class="session-badge none">{"None"}</span>
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        
 | 
					 | 
				
			||||||
                        if props.session_secret_type == SessionSecretType::None {
 | 
					 | 
				
			||||||
                            <div class="session-input-row">
 | 
					 | 
				
			||||||
                                <input 
 | 
					 | 
				
			||||||
                                    type="password" 
 | 
					 | 
				
			||||||
                                    class="session-input"
 | 
					 | 
				
			||||||
                                    placeholder="Enter secret to establish session"
 | 
					 | 
				
			||||||
                                    value={(*session_secret_input).clone()}
 | 
					 | 
				
			||||||
                                    onchange={on_session_secret_change}
 | 
					 | 
				
			||||||
                                    disabled={*is_loading}
 | 
					 | 
				
			||||||
                                />
 | 
					 | 
				
			||||||
                                <button 
 | 
					 | 
				
			||||||
                                    class="session-btn"
 | 
					 | 
				
			||||||
                                    onclick={on_session_secret_submit}
 | 
					 | 
				
			||||||
                                    disabled={*is_loading || session_secret_input.is_empty()}
 | 
					 | 
				
			||||||
                                >
 | 
					 | 
				
			||||||
                                    if *is_loading {
 | 
					 | 
				
			||||||
                                        <i class="fas fa-spinner fa-spin"></i>
 | 
					 | 
				
			||||||
                                    } else {
 | 
					 | 
				
			||||||
                                        <i class="fas fa-sign-in-alt"></i>
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                </button>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            <div class="session-active">
 | 
					 | 
				
			||||||
                                <div class="session-info">
 | 
					 | 
				
			||||||
                                    <span class="session-secret-preview">
 | 
					 | 
				
			||||||
                                        {format!("{}...", &props.session_secret[..std::cmp::min(8, props.session_secret.len())])}
 | 
					 | 
				
			||||||
                                    </span>
 | 
					 | 
				
			||||||
                                    <button 
 | 
					 | 
				
			||||||
                                        class="session-clear-btn"
 | 
					 | 
				
			||||||
                                        onclick={on_session_clear}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        <i class="fas fa-sign-out-alt"></i>
 | 
					 | 
				
			||||||
                                    </button>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // Secrets Management Section (only visible for admin)
 | 
					 | 
				
			||||||
                    if props.session_secret_type == SessionSecretType::Admin {
 | 
					 | 
				
			||||||
                        <div class="secrets-section">
 | 
					 | 
				
			||||||
                            <div class="secrets-header">
 | 
					 | 
				
			||||||
                                <span class="secrets-title">{"Secrets Management"}</span>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                            
 | 
					 | 
				
			||||||
                            <div class="secrets-content">
 | 
					 | 
				
			||||||
                                <div class="secret-group">
 | 
					 | 
				
			||||||
                                    <div class="secret-header">
 | 
					 | 
				
			||||||
                                        <span class="secret-title">{"Admin secrets"}</span>
 | 
					 | 
				
			||||||
                                        <span class="secret-count">{admin_secrets.len()}</span>
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                    <div class="secret-list">
 | 
					 | 
				
			||||||
                                        { for admin_secrets.iter().enumerate().map(|(i, secret)| {
 | 
					 | 
				
			||||||
                                            html! {
 | 
					 | 
				
			||||||
                                                <div class="secret-item" key={i}>
 | 
					 | 
				
			||||||
                                                    <div class="secret-value">{secret.clone()}</div>
 | 
					 | 
				
			||||||
                                                    <button class="btn-icon btn-remove">
 | 
					 | 
				
			||||||
                                                        <i class="fas fa-minus"></i>
 | 
					 | 
				
			||||||
                                                    </button>
 | 
					 | 
				
			||||||
                                                </div>
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                        })}
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                <div class="secret-group">
 | 
					 | 
				
			||||||
                                    <div class="secret-header">
 | 
					 | 
				
			||||||
                                        <span class="secret-title">{"User secrets"}</span>
 | 
					 | 
				
			||||||
                                        <span class="secret-count">{user_secrets.len()}</span>
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                    <div class="secret-list">
 | 
					 | 
				
			||||||
                                        { for user_secrets.iter().enumerate().map(|(i, secret)| {
 | 
					 | 
				
			||||||
                                            html! {
 | 
					 | 
				
			||||||
                                                <div class="secret-item" key={i}>
 | 
					 | 
				
			||||||
                                                    <div class="secret-value">{secret.clone()}</div>
 | 
					 | 
				
			||||||
                                                    <button class="btn-icon btn-remove">
 | 
					 | 
				
			||||||
                                                        <i class="fas fa-minus"></i>
 | 
					 | 
				
			||||||
                                                    </button>
 | 
					 | 
				
			||||||
                                                </div>
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                        })}
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                <div class="secret-group">
 | 
					 | 
				
			||||||
                                    <div class="secret-header">
 | 
					 | 
				
			||||||
                                        <span class="secret-title">{"Register secrets"}</span>
 | 
					 | 
				
			||||||
                                        <span class="secret-count">{register_secrets.len()}</span>
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                    <div class="secret-list">
 | 
					 | 
				
			||||||
                                        { for register_secrets.iter().enumerate().map(|(i, secret)| {
 | 
					 | 
				
			||||||
                                            html! {
 | 
					 | 
				
			||||||
                                                <div class="secret-item" key={i}>
 | 
					 | 
				
			||||||
                                                    <div class="secret-value">{secret.clone()}</div>
 | 
					 | 
				
			||||||
                                                    <button class="btn-icon btn-remove">
 | 
					 | 
				
			||||||
                                                        <i class="fas fa-minus"></i>
 | 
					 | 
				
			||||||
                                                    </button>
 | 
					 | 
				
			||||||
                                                </div>
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                        })}
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // Quick Actions Section
 | 
					 | 
				
			||||||
                    <div class="quick-actions">
 | 
					 | 
				
			||||||
                        <div class="quick-actions-header">
 | 
					 | 
				
			||||||
                            <span class="quick-actions-title">{"Quick Actions"}</span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="quick-actions-content">
 | 
					 | 
				
			||||||
                            if props.session_secret_type != SessionSecretType::None {
 | 
					 | 
				
			||||||
                                <div class="action-row">
 | 
					 | 
				
			||||||
                                    <input 
 | 
					 | 
				
			||||||
                                        type="text" 
 | 
					 | 
				
			||||||
                                        class="action-input"
 | 
					 | 
				
			||||||
                                        placeholder="Enter payload for job"
 | 
					 | 
				
			||||||
                                        value={(*payload_input).clone()}
 | 
					 | 
				
			||||||
                                        onchange={on_payload_change}
 | 
					 | 
				
			||||||
                                    />
 | 
					 | 
				
			||||||
                                    <button 
 | 
					 | 
				
			||||||
                                        class="action-btn run-btn"
 | 
					 | 
				
			||||||
                                        onclick={on_run_click}
 | 
					 | 
				
			||||||
                                        disabled={payload_input.is_empty() || *is_loading}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        if *is_loading {
 | 
					 | 
				
			||||||
                                            <i class="fas fa-spinner fa-spin"></i>
 | 
					 | 
				
			||||||
                                        } else {
 | 
					 | 
				
			||||||
                                            <i class="fas fa-play"></i>
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                        {"Run"}
 | 
					 | 
				
			||||||
                                    </button>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                <div class="action-disabled">
 | 
					 | 
				
			||||||
                                    <span>{"Establish a session to enable quick actions"}</span>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // Supervisor Info Section
 | 
					 | 
				
			||||||
                    if let Some(info) = &props.supervisor_info {
 | 
					 | 
				
			||||||
                        <div class="supervisor-info">
 | 
					 | 
				
			||||||
                            <div class="supervisor-info-header">
 | 
					 | 
				
			||||||
                                <span class="supervisor-info-title">{"Supervisor Info"}</span>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                            <div class="supervisor-info-content">
 | 
					 | 
				
			||||||
                                <div class="info-item">
 | 
					 | 
				
			||||||
                                    <span class="info-label">{"Admin secrets:"}</span>
 | 
					 | 
				
			||||||
                                    <span class="info-value">{info.admin_secrets_count}</span>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                                <div class="info-item">
 | 
					 | 
				
			||||||
                                    <span class="info-label">{"User secrets:"}</span>
 | 
					 | 
				
			||||||
                                    <span class="info-value">{info.user_secrets_count}</span>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                                <div class="info-item">
 | 
					 | 
				
			||||||
                                    <span class="info-label">{"Register secrets:"}</span>
 | 
					 | 
				
			||||||
                                    <span class="info-value">{info.register_secrets_count}</span>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                                <div class="info-item">
 | 
					 | 
				
			||||||
                                    <span class="info-label">{"Runners:"}</span>
 | 
					 | 
				
			||||||
                                    <span class="info-value">{info.runners_count}</span>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Documentation Links at Bottom
 | 
					                // Session login section
 | 
				
			||||||
            <div class="sidebar-footer">
 | 
					                <div class="py-3 border-bottom border-secondary">
 | 
				
			||||||
                <div class="docs-section">
 | 
					                    <div class="mb-2">
 | 
				
			||||||
                    <h5>{"Documentation"}</h5>
 | 
					                        <select 
 | 
				
			||||||
                    <div class="docs-links">
 | 
					                            class="form-select form-select-sm bg-secondary text-white border-0"
 | 
				
			||||||
                        <a href="https://github.com/herocode/supervisor" target="_blank" class="doc-link">
 | 
					                            onchange={{
 | 
				
			||||||
                            {"📖 User Guide"}
 | 
					                                let selected_secret_type = selected_secret_type.clone();
 | 
				
			||||||
                        </a>
 | 
					                                Callback::from(move |e: Event| {
 | 
				
			||||||
                        <a href="https://github.com/herocode/supervisor/blob/main/README.md" target="_blank" class="doc-link">
 | 
					                                    let select: web_sys::HtmlInputElement = e.target_unchecked_into();
 | 
				
			||||||
                            {"🚀 Getting Started"}
 | 
					                                    let secret_type = match select.value().as_str() {
 | 
				
			||||||
                        </a>
 | 
					                                        "Admin" => SessionSecretType::Admin,
 | 
				
			||||||
                        <a href="https://github.com/herocode/supervisor/issues" target="_blank" class="doc-link">
 | 
					                                        "User" => SessionSecretType::User,
 | 
				
			||||||
                            {"🐛 Report Issues"}
 | 
					                                        "Register" => SessionSecretType::Register,
 | 
				
			||||||
                        </a>
 | 
					                                        _ => SessionSecretType::Admin,
 | 
				
			||||||
                        <a href="https://github.com/herocode/supervisor/wiki" target="_blank" class="doc-link">
 | 
					                                    };
 | 
				
			||||||
                            {"📚 API Reference"}
 | 
					                                    selected_secret_type.set(secret_type);
 | 
				
			||||||
                        </a>
 | 
					                                })
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <option value="Admin" selected={*selected_secret_type == SessionSecretType::Admin}>{"Admin Secret"}</option>
 | 
				
			||||||
 | 
					                            <option value="User" selected={*selected_secret_type == SessionSecretType::User}>{"User Secret"}</option>
 | 
				
			||||||
 | 
					                            <option value="Register" selected={*selected_secret_type == SessionSecretType::Register}>{"Register Secret"}</option>
 | 
				
			||||||
 | 
					                        </select>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="input-group input-group-sm">
 | 
				
			||||||
 | 
					                        <input
 | 
				
			||||||
 | 
					                            type="password"
 | 
				
			||||||
 | 
					                            class="form-control bg-secondary text-white border-0"
 | 
				
			||||||
 | 
					                            placeholder={format!("Enter {} secret...", match *selected_secret_type {
 | 
				
			||||||
 | 
					                                SessionSecretType::Admin => "admin",
 | 
				
			||||||
 | 
					                                SessionSecretType::User => "user",
 | 
				
			||||||
 | 
					                                SessionSecretType::Register => "register",
 | 
				
			||||||
 | 
					                                _ => "session"
 | 
				
			||||||
 | 
					                            })}
 | 
				
			||||||
 | 
					                            value={(*session_secret_input).clone()}
 | 
				
			||||||
 | 
					                            oninput={on_session_input}
 | 
				
			||||||
 | 
					                            onkeypress={on_session_keypress}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        <button 
 | 
				
			||||||
 | 
					                            class={classes!("btn", if props.session_secret.is_empty() { "btn-outline-secondary" } else { "btn-outline-success" })}
 | 
				
			||||||
 | 
					                            onclick={on_session_toggle}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            if props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                                {"🔒"}
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                {"🔓"}
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Secret management section (only show when logged in)
 | 
				
			||||||
 | 
					                if !props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                    <div class="flex-grow-1 overflow-auto">
 | 
				
			||||||
 | 
					                        <div class="py-3">
 | 
				
			||||||
 | 
					                            <h6 class="text-white text-uppercase fw-bold mb-3">{"Secret Management"}</h6>
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Admin Secrets
 | 
				
			||||||
 | 
					                            <div class="mb-4">
 | 
				
			||||||
 | 
					                                <div class="d-flex justify-content-between align-items-center mb-2">
 | 
				
			||||||
 | 
					                                    <small class="text-muted text-uppercase fw-bold">{"Admin Secrets"}</small>
 | 
				
			||||||
 | 
					                                    <button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_admin_secret.clone()}>
 | 
				
			||||||
 | 
					                                        {"➕"}
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					                                    {for props.supervisor_info.as_ref().map(|info| &info.admin_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
 | 
				
			||||||
 | 
					                                        let is_current = secret == &props.session_secret && props.session_secret_type == SessionSecretType::Admin;
 | 
				
			||||||
 | 
					                                        let on_select = {
 | 
				
			||||||
 | 
					                                            let on_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					                                            let refresh_secrets = refresh_secrets_from_api.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_change.emit((secret.clone(), SessionSecretType::Admin));
 | 
				
			||||||
 | 
					                                                refresh_secrets.emit((secret.clone(), SessionSecretType::Admin));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        let on_remove = {
 | 
				
			||||||
 | 
					                                            let on_remove = props.on_remove_secret.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_remove.emit((SessionSecretType::Admin, secret.clone()));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        html! {
 | 
				
			||||||
 | 
					                                            <div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
 | 
				
			||||||
 | 
					                                                <code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
 | 
				
			||||||
 | 
					                                                    {format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
 | 
				
			||||||
 | 
					                                                </code>
 | 
				
			||||||
 | 
					                                                <button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
 | 
				
			||||||
 | 
					                                                    {"❌"}
 | 
				
			||||||
 | 
					                                                </button>
 | 
				
			||||||
 | 
					                                            </div>
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // User Secrets
 | 
				
			||||||
 | 
					                            <div class="mb-4">
 | 
				
			||||||
 | 
					                                <div class="d-flex justify-content-between align-items-center mb-2">
 | 
				
			||||||
 | 
					                                    <small class="text-muted text-uppercase fw-bold">{"User Secrets"}</small>
 | 
				
			||||||
 | 
					                                    <button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_user_secret.clone()}>
 | 
				
			||||||
 | 
					                                        {"➕"}
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					                                    {for props.supervisor_info.as_ref().map(|info| &info.user_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
 | 
				
			||||||
 | 
					                                        let is_current = secret == &props.session_secret && props.session_secret_type == SessionSecretType::User;
 | 
				
			||||||
 | 
					                                        let on_select = {
 | 
				
			||||||
 | 
					                                            let on_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_change.emit((secret.clone(), SessionSecretType::User));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        let on_remove = {
 | 
				
			||||||
 | 
					                                            let on_remove = props.on_remove_secret.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_remove.emit((SessionSecretType::User, secret.clone()));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        html! {
 | 
				
			||||||
 | 
					                                            <div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
 | 
				
			||||||
 | 
					                                                <code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
 | 
				
			||||||
 | 
					                                                    {format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
 | 
				
			||||||
 | 
					                                                </code>
 | 
				
			||||||
 | 
					                                                <button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
 | 
				
			||||||
 | 
					                                                    {"❌"}
 | 
				
			||||||
 | 
					                                                </button>
 | 
				
			||||||
 | 
					                                            </div>
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Register Secrets
 | 
				
			||||||
 | 
					                            <div class="mb-4">
 | 
				
			||||||
 | 
					                                <div class="d-flex justify-content-between align-items-center mb-2">
 | 
				
			||||||
 | 
					                                    <small class="text-muted text-uppercase fw-bold">{"Register Secrets"}</small>
 | 
				
			||||||
 | 
					                                    <button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_register_secret.clone()}>
 | 
				
			||||||
 | 
					                                        {"➕"}
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					                                    {for props.supervisor_info.as_ref().map(|info| &info.register_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
 | 
				
			||||||
 | 
					                                        let is_current = secret == &props.session_secret;
 | 
				
			||||||
 | 
					                                        let on_select = {
 | 
				
			||||||
 | 
					                                            let on_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_change.emit((secret.clone(), SessionSecretType::Register));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        let on_remove = {
 | 
				
			||||||
 | 
					                                            let on_remove = props.on_remove_secret.clone();
 | 
				
			||||||
 | 
					                                            let secret = secret.clone();
 | 
				
			||||||
 | 
					                                            Callback::from(move |_| {
 | 
				
			||||||
 | 
					                                                on_remove.emit((SessionSecretType::Register, secret.clone()));
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        };
 | 
				
			||||||
 | 
					                                        html! {
 | 
				
			||||||
 | 
					                                            <div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
 | 
				
			||||||
 | 
					                                                <code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
 | 
				
			||||||
 | 
					                                                    {format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
 | 
				
			||||||
 | 
					                                                </code>
 | 
				
			||||||
 | 
					                                                <button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
 | 
				
			||||||
 | 
					                                                    {"❌"}
 | 
				
			||||||
 | 
					                                                </button>
 | 
				
			||||||
 | 
					                                            </div>
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Navigation and status at bottom
 | 
				
			||||||
 | 
					                <div class="mt-auto">
 | 
				
			||||||
 | 
					                    // Navigation links
 | 
				
			||||||
 | 
					                    <div class="py-2 border-top border-secondary">
 | 
				
			||||||
 | 
					                        <div class="nav nav-pills flex-column">
 | 
				
			||||||
 | 
					                            <a href="#runners" class="nav-link text-muted small">{"Runners"}</a>
 | 
				
			||||||
 | 
					                            <a href="#jobs" class="nav-link text-muted small">{"Jobs"}</a>
 | 
				
			||||||
 | 
					                            <a href="#logs" class="nav-link text-muted small">{"Logs"}</a>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Server status
 | 
				
			||||||
 | 
					                    <div class="py-2 border-top border-secondary">
 | 
				
			||||||
 | 
					                        <div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                            <span class={classes!("badge", "me-2", if props.supervisor_info.is_some() { "bg-success" } else { "bg-danger" })}>
 | 
				
			||||||
 | 
					                                {"●"}
 | 
				
			||||||
 | 
					                            </span>
 | 
				
			||||||
 | 
					                            <small class="text-muted">
 | 
				
			||||||
 | 
					                                {if props.supervisor_info.is_some() { "Connected" } else { "Disconnected" }}
 | 
				
			||||||
 | 
					                            </small>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										639
									
								
								clients/admin-ui/src/sidebar_old.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										639
									
								
								clients/admin-ui/src/sidebar_old.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,639 @@
 | 
				
			|||||||
 | 
					use yew::prelude::*;
 | 
				
			||||||
 | 
					use wasm_bindgen::JsCast;
 | 
				
			||||||
 | 
					use wasm_bindgen_futures::spawn_local;
 | 
				
			||||||
 | 
					use gloo::console;
 | 
				
			||||||
 | 
					use gloo::storage::{LocalStorage, Storage};
 | 
				
			||||||
 | 
					use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, PartialEq)]
 | 
				
			||||||
 | 
					pub struct SupervisorInfo {
 | 
				
			||||||
 | 
					    pub server_url: String,
 | 
				
			||||||
 | 
					    pub admin_secrets_count: usize,
 | 
				
			||||||
 | 
					    pub user_secrets_count: usize,
 | 
				
			||||||
 | 
					    pub register_secrets_count: usize,
 | 
				
			||||||
 | 
					    pub runners_count: usize,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub enum SessionSecretType {
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					    User,
 | 
				
			||||||
 | 
					    Admin,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					struct SessionData {
 | 
				
			||||||
 | 
					    secret: String,
 | 
				
			||||||
 | 
					    secret_type: SessionSecretType,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SESSION_STORAGE_KEY: &str = "supervisor_session";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Properties, PartialEq)]
 | 
				
			||||||
 | 
					pub struct SidebarProps {
 | 
				
			||||||
 | 
					    pub server_url: String,
 | 
				
			||||||
 | 
					    pub supervisor_info: Option<SupervisorInfo>,
 | 
				
			||||||
 | 
					    pub session_secret: String,
 | 
				
			||||||
 | 
					    pub session_secret_type: SessionSecretType,
 | 
				
			||||||
 | 
					    pub on_session_secret_change: Callback<(String, SessionSecretType)>,
 | 
				
			||||||
 | 
					    pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[function_component(Sidebar)]
 | 
				
			||||||
 | 
					pub fn sidebar(props: &SidebarProps) -> Html {
 | 
				
			||||||
 | 
					    let session_secret_input = use_state(|| String::new());
 | 
				
			||||||
 | 
					    let is_loading = use_state(|| false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Load session from localStorage on component mount
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        use_effect_with((), move |_| {
 | 
				
			||||||
 | 
					            if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
 | 
				
			||||||
 | 
					                on_session_secret_change.emit((session_data.secret, session_data.secret_type));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            || ()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_session_secret_change = {
 | 
				
			||||||
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |e: web_sys::Event| {
 | 
				
			||||||
 | 
					            let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
				
			||||||
 | 
					            session_secret_input.set(input.value());
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_session_secret_submit = {
 | 
				
			||||||
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					        let is_loading = is_loading.clone();
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					        let server_url = props.server_url.clone();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					            let secret = (*session_secret_input).clone();
 | 
				
			||||||
 | 
					            if secret.is_empty() {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            is_loading.set(true);
 | 
				
			||||||
 | 
					            let is_loading = is_loading.clone();
 | 
				
			||||||
 | 
					            let on_session_secret_change = on_session_secret_change.clone();
 | 
				
			||||||
 | 
					            let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					            let server_url = server_url.clone();
 | 
				
			||||||
 | 
					            let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            spawn_local(async move {
 | 
				
			||||||
 | 
					                let client = WasmSupervisorClient::new(server_url.clone());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                match client.discover().await {
 | 
				
			||||||
 | 
					                    Ok(_) => {
 | 
				
			||||||
 | 
					                        console::log!("Connected to supervisor successfully");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        let secret_type = if secret.starts_with("admin_") {
 | 
				
			||||||
 | 
					                            SessionSecretType::Admin
 | 
				
			||||||
 | 
					                        } else if secret.starts_with("user_") {
 | 
				
			||||||
 | 
					                            SessionSecretType::User
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            SessionSecretType::User
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Save to localStorage
 | 
				
			||||||
 | 
					                        let session_data = SessionData {
 | 
				
			||||||
 | 
					                            secret: secret.clone(),
 | 
				
			||||||
 | 
					                            secret_type: secret_type.clone(),
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        let supervisor_info = SupervisorInfo {
 | 
				
			||||||
 | 
					                            server_url: server_url.clone(),
 | 
				
			||||||
 | 
					                            admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
 | 
				
			||||||
 | 
					                            user_secrets_count: 1,
 | 
				
			||||||
 | 
					                            register_secrets_count: 0,
 | 
				
			||||||
 | 
					                            runners_count: 0,
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        on_session_secret_change.emit((secret.clone(), secret_type.clone()));
 | 
				
			||||||
 | 
					                        on_supervisor_info_loaded.emit(supervisor_info);
 | 
				
			||||||
 | 
					                        session_secret_input.set(String::new());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        console::error!("Failed to connect to supervisor:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                is_loading.set(false);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let on_logout = {
 | 
				
			||||||
 | 
					        let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					            // Clear localStorage
 | 
				
			||||||
 | 
					            let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
 | 
				
			||||||
 | 
					            on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html! {
 | 
				
			||||||
 | 
					        <div class="sidebar">
 | 
				
			||||||
 | 
					            // Header with logo and title
 | 
				
			||||||
 | 
					            <div class="sidebar-header">
 | 
				
			||||||
 | 
					                <div class="logo-section">
 | 
				
			||||||
 | 
					                    <div class="logo">{"⚡"}</div>
 | 
				
			||||||
 | 
					                    <h1 class="title">{"Supervisor"}</h1>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Main control island
 | 
				
			||||||
 | 
					            <div class="control-island">
 | 
				
			||||||
 | 
					                // Session Login Section
 | 
				
			||||||
 | 
					                <div class="session-section">
 | 
				
			||||||
 | 
					                    <h3 class="section-title">{"Session Login"}</h3>
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                        <div class="login-form">
 | 
				
			||||||
 | 
					                            <div class="input-group">
 | 
				
			||||||
 | 
					                                <input 
 | 
				
			||||||
 | 
					                                    type="password" 
 | 
				
			||||||
 | 
					                                    class="secret-input" 
 | 
				
			||||||
 | 
					                                    placeholder="Enter session secret"
 | 
				
			||||||
 | 
					                                    value={(*session_secret_input).clone()}
 | 
				
			||||||
 | 
					                                    onchange={on_session_secret_change}
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                                <button 
 | 
				
			||||||
 | 
					                                    class="connect-btn"
 | 
				
			||||||
 | 
					                                    onclick={on_session_secret_submit}
 | 
				
			||||||
 | 
					                                    disabled={*is_loading}
 | 
				
			||||||
 | 
					                                >
 | 
				
			||||||
 | 
					                                    if *is_loading {
 | 
				
			||||||
 | 
					                                        <span class="loading-spinner"></span>
 | 
				
			||||||
 | 
					                                        {"Connecting"}
 | 
				
			||||||
 | 
					                                    } else {
 | 
				
			||||||
 | 
					                                        {"🔐 Connect"}
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                </button>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div class="login-hint">
 | 
				
			||||||
 | 
					                                {"Use admin_ or user_ prefixed secrets"}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        <div class="session-active">
 | 
				
			||||||
 | 
					                            <div class="session-status">
 | 
				
			||||||
 | 
					                                <div class="status-indicator"></div>
 | 
				
			||||||
 | 
					                                <div class="status-info">
 | 
				
			||||||
 | 
					                                    <span class="status-text">{"Connected"}</span>
 | 
				
			||||||
 | 
					                                    <span class="session-badge">{
 | 
				
			||||||
 | 
					                                        match props.session_secret_type {
 | 
				
			||||||
 | 
					                                            SessionSecretType::Admin => "Admin",
 | 
				
			||||||
 | 
					                                            SessionSecretType::User => "User",
 | 
				
			||||||
 | 
					                                            SessionSecretType::None => "None",
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }</span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <button 
 | 
				
			||||||
 | 
					                                class="logout-btn"
 | 
				
			||||||
 | 
					                                onclick={on_logout}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                {"🚪 Logout"}
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Secret Management Section (Admin only)
 | 
				
			||||||
 | 
					                if props.session_secret_type == SessionSecretType::Admin && !props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                    <div class="secrets-section">
 | 
				
			||||||
 | 
					                        <h3 class="section-title">{"Secret Management"}</h3>
 | 
				
			||||||
 | 
					                        <div class="secret-display">
 | 
				
			||||||
 | 
					                            <div class="secret-item">
 | 
				
			||||||
 | 
					                                <label class="secret-label">{"Current Session Secret"}</label>
 | 
				
			||||||
 | 
					                                <div class="secret-value">
 | 
				
			||||||
 | 
					                                    <code>{&props.session_secret}</code>
 | 
				
			||||||
 | 
					                                    <button class="copy-btn" title="Copy to clipboard">{"📋"}</button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Server Info Section
 | 
				
			||||||
 | 
					                if let Some(info) = &props.supervisor_info {
 | 
				
			||||||
 | 
					                    <div class="server-info-section">
 | 
				
			||||||
 | 
					                        <h3 class="section-title">{"Server Status"}</h3>
 | 
				
			||||||
 | 
					                        <div class="info-cards">
 | 
				
			||||||
 | 
					                            <div class="info-card">
 | 
				
			||||||
 | 
					                                <div class="info-icon">{"🏃"}</div>
 | 
				
			||||||
 | 
					                                <div class="info-content">
 | 
				
			||||||
 | 
					                                    <div class="info-number">{info.runners_count}</div>
 | 
				
			||||||
 | 
					                                    <div class="info-label">{"Runners"}</div>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div class="info-card">
 | 
				
			||||||
 | 
					                                <div class="info-icon">{"🔗"}</div>
 | 
				
			||||||
 | 
					                                <div class="info-content">
 | 
				
			||||||
 | 
					                                    <div class="info-text">{&info.server_url.replace("http://", "").replace("https://", "")}</div>
 | 
				
			||||||
 | 
					                                    <div class="info-label">{"Server"}</div>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Footer with navigation links
 | 
				
			||||||
 | 
					            <div class="sidebar-footer">
 | 
				
			||||||
 | 
					                <div class="nav-links">
 | 
				
			||||||
 | 
					                    <a href="https://github.com/herocode/supervisor" target="_blank" class="nav-link">
 | 
				
			||||||
 | 
					                        <span class="nav-icon">{"📖"}</span>
 | 
				
			||||||
 | 
					                        <span class="nav-text">{"Documentation"}</span>
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                    <a href="https://github.com/herocode/supervisor/issues" target="_blank" class="nav-link">
 | 
				
			||||||
 | 
					                        <span class="nav-icon">{"🐛"}</span>
 | 
				
			||||||
 | 
					                        <span class="nav-text">{"Report Issue"}</span>
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                    <a href="#" class="nav-link">
 | 
				
			||||||
 | 
					                        <span class="nav-icon">{"⚙️"}</span>
 | 
				
			||||||
 | 
					                        <span class="nav-text">{"Settings"}</span>
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="version-info">
 | 
				
			||||||
 | 
					                    {"Hero Supervisor v0.1.0"}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    is_loading.set(true);
 | 
				
			||||||
 | 
					    let is_loading = is_loading.clone();
 | 
				
			||||||
 | 
					    let on_session_secret_change = on_session_secret_change.clone();
 | 
				
			||||||
 | 
					    let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					    let server_url = server_url.clone();
 | 
				
			||||||
 | 
					    let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    spawn_local(async move {
 | 
				
			||||||
 | 
					        let client = WasmSupervisorClient::new(server_url.clone());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        match client.discover().await {
 | 
				
			||||||
 | 
					            Ok(_) => {
 | 
				
			||||||
 | 
					                console::log!("Connected to supervisor successfully");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let secret_type = if secret.starts_with("admin_") {
 | 
				
			||||||
 | 
					                    SessionSecretType::Admin
 | 
				
			||||||
 | 
					                } else if secret.starts_with("user_") {
 | 
				
			||||||
 | 
					                    SessionSecretType::User
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    SessionSecretType::User
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Save to localStorage
 | 
				
			||||||
 | 
					                let session_data = SessionData {
 | 
				
			||||||
 | 
					                    secret: secret.clone(),
 | 
				
			||||||
 | 
					                    secret_type: secret_type.clone(),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let supervisor_info = SupervisorInfo {
 | 
				
			||||||
 | 
					                    server_url: server_url.clone(),
 | 
				
			||||||
 | 
					                    admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
 | 
				
			||||||
 | 
					                    user_secrets_count: 1,
 | 
				
			||||||
 | 
					                    register_secrets_count: 0,
 | 
				
			||||||
 | 
					                    runners_count: 0,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                on_session_secret_change.emit((secret.clone(), secret_type.clone()));
 | 
				
			||||||
 | 
					                on_supervisor_info_loaded.emit(supervisor_info);
 | 
				
			||||||
 | 
					                session_secret_input.set(String::new());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                console::error!("Failed to connect to supervisor:", format!("{:?}", e));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        is_loading.set(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let on_logout = {
 | 
				
			||||||
 | 
					    let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					    Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					        // Clear localStorage
 | 
				
			||||||
 | 
					        let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
 | 
				
			||||||
 | 
					        on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html! {
 | 
				
			||||||
 | 
					    <div class="sidebar">
 | 
				
			||||||
 | 
					        // Header with logo and title
 | 
				
			||||||
 | 
					        <div class="sidebar-header">
 | 
				
			||||||
 | 
					            <div class="logo-section">
 | 
				
			||||||
 | 
					                <div class="logo">{"⚡"}</div>
 | 
				
			||||||
 | 
					                <h1 class="title">{"Supervisor"}</h1>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Main control island
 | 
				
			||||||
 | 
					        <div class="control-island">
 | 
				
			||||||
 | 
					            // Session Login Section
 | 
				
			||||||
 | 
					            <div class="session-section">
 | 
				
			||||||
 | 
					                <h3 class="section-title">{"Session Login"}</h3>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                    <div class="login-form">
 | 
				
			||||||
 | 
					                        <div class="input-group">
 | 
				
			||||||
 | 
					                            <input 
 | 
				
			||||||
 | 
					                                type="password" 
 | 
				
			||||||
 | 
					                                class="secret-input" 
 | 
				
			||||||
 | 
					                                placeholder="Enter session secret"
 | 
				
			||||||
 | 
					                                value={(*session_secret_input).clone()}
 | 
				
			||||||
 | 
					                                onchange={on_session_secret_change}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <button 
 | 
				
			||||||
 | 
					                                class="connect-btn"
 | 
				
			||||||
 | 
					                                onclick={on_session_secret_submit}
 | 
				
			||||||
 | 
					                                disabled={*is_loading}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                if *is_loading {
 | 
				
			||||||
 | 
					                                    <span class="loading-spinner"></span>
 | 
				
			||||||
 | 
					                                    {"Connecting"}
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    {"🔐 Connect"}
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="login-hint">
 | 
				
			||||||
 | 
					                            {"Use admin_ or user_ prefixed secrets"}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    <div class="session-active">
 | 
				
			||||||
 | 
					                        <div class="session-status">
 | 
				
			||||||
 | 
					                            <div class="status-indicator"></div>
 | 
				
			||||||
 | 
					                            <div class="status-info">
 | 
				
			||||||
 | 
					                                <span class="status-text">{"Connected"}</span>
 | 
				
			||||||
 | 
					                                <span class="session-badge">{
 | 
				
			||||||
 | 
					                                    match props.session_secret_type {
 | 
				
			||||||
 | 
					                                        SessionSecretType::Admin => "Admin",
 | 
				
			||||||
 | 
					                                        SessionSecretType::User => "User",
 | 
				
			||||||
 | 
					                                        SessionSecretType::None => "None",
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }</span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <button 
 | 
				
			||||||
 | 
					                            class="logout-btn"
 | 
				
			||||||
 | 
					                            onclick={on_logout}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            {"🚪 Logout"}
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Secret Management Section (Admin only)
 | 
				
			||||||
 | 
					            if props.session_secret_type == SessionSecretType::Admin && !props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                <div class="secrets-section">
 | 
				
			||||||
 | 
					                    <h3 class="section-title">{"Secret Management"}</h3>
 | 
				
			||||||
 | 
					                    <div class="secret-display">
 | 
				
			||||||
 | 
					                        <div class="secret-item">
 | 
				
			||||||
 | 
					                            <label class="secret-label">{"Current Session Secret"}</label>
 | 
				
			||||||
 | 
					                            <div class="secret-value">
 | 
				
			||||||
 | 
					                                <code>{&props.session_secret}</code>
 | 
				
			||||||
 | 
					                                <button class="copy-btn" title="Copy to clipboard">{"📋"}</button>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Server Info Section
 | 
				
			||||||
 | 
					            if let Some(info) = &props.supervisor_info {
 | 
				
			||||||
 | 
					                <div class="server-info-section">
 | 
				
			||||||
 | 
					                    <h3 class="section-title">{"Server Status"}</h3>
 | 
				
			||||||
 | 
					                    <div class="info-cards">
 | 
				
			||||||
 | 
					                        <div class="info-card">
 | 
				
			||||||
 | 
					                            <div class="info-icon">{"🏃"}</div>
 | 
				
			||||||
 | 
					                            <div class="info-content">
 | 
				
			||||||
 | 
					                                <div class="info-number">{info.runners_count}</div>
 | 
				
			||||||
 | 
					                                <div class="info-label">{"Runners"}</div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="info-card">
 | 
				
			||||||
 | 
					                            <div class="info-icon">{"🔗"}</div>
 | 
				
			||||||
 | 
					                            <div class="info-content">
 | 
				
			||||||
 | 
					                                <div class="info-text">{&info.server_url.replace("http://", "").replace("https://", "")}</div>
 | 
				
			||||||
 | 
					                                <div class="info-label">{"Server"}</div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Footer with navigation links
 | 
				
			||||||
 | 
					        <div class="sidebar-footer">
 | 
				
			||||||
 | 
					            <div class="nav-links">
 | 
				
			||||||
 | 
					                <a href="https://github.com/herocode/supervisor" target="_blank" class="nav-link">
 | 
				
			||||||
 | 
					                    <span class="nav-icon">{"📖"}</span>
 | 
				
			||||||
 | 
					                    <span class="nav-text">{"Documentation"}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                <a href="https://github.com/herocode/supervisor/issues" target="_blank" class="nav-link">
 | 
				
			||||||
 | 
					                    <span class="nav-icon">{"🐛"}</span>
 | 
				
			||||||
 | 
					                    <span class="nav-text">{"Report Issue"}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                <a href="#" class="nav-link">
 | 
				
			||||||
 | 
					                    <span class="nav-icon">{"⚙️"}</span>
 | 
				
			||||||
 | 
					                    <span class="nav-text">{"Settings"}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="version-info">
 | 
				
			||||||
 | 
					                {"Hero Supervisor v0.1.0"}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let on_session_secret_change = {
 | 
				
			||||||
 | 
					    let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					    Callback::from(move |e: web_sys::Event| {
 | 
				
			||||||
 | 
					        let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
 | 
				
			||||||
 | 
					        session_secret_input.set(input.value());
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let on_session_secret_submit = {
 | 
				
			||||||
 | 
					    let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					    let is_loading = is_loading.clone();
 | 
				
			||||||
 | 
					    let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					    let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					    let server_url = props.server_url.clone();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					        let secret = (*session_secret_input).clone();
 | 
				
			||||||
 | 
					        if secret.is_empty() {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        is_loading.set(true);
 | 
				
			||||||
 | 
					        let is_loading = is_loading.clone();
 | 
				
			||||||
 | 
					        let on_session_secret_change = on_session_secret_change.clone();
 | 
				
			||||||
 | 
					        let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
 | 
				
			||||||
 | 
					        let server_url = server_url.clone();
 | 
				
			||||||
 | 
					        let session_secret_input = session_secret_input.clone();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        spawn_local(async move {
 | 
				
			||||||
 | 
					            let client = WasmSupervisorClient::new(server_url.clone());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            match client.discover().await {
 | 
				
			||||||
 | 
					                Ok(_) => {
 | 
				
			||||||
 | 
					                    console::log!("Connected to supervisor successfully");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    let secret_type = if secret.starts_with("admin_") {
 | 
				
			||||||
 | 
					                        SessionSecretType::Admin
 | 
				
			||||||
 | 
					                    } else if secret.starts_with("user_") {
 | 
				
			||||||
 | 
					                        SessionSecretType::User
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        SessionSecretType::User
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Save to localStorage
 | 
				
			||||||
 | 
					                    let session_data = SessionData {
 | 
				
			||||||
 | 
					                        secret: secret.clone(),
 | 
				
			||||||
 | 
					                        secret_type: secret_type.clone(),
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    let supervisor_info = SupervisorInfo {
 | 
				
			||||||
 | 
					                        server_url: server_url.clone(),
 | 
				
			||||||
 | 
					                        admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
 | 
				
			||||||
 | 
					                        user_secrets_count: 1,
 | 
				
			||||||
 | 
					                        register_secrets_count: 0,
 | 
				
			||||||
 | 
					                        runners_count: 0,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    on_session_secret_change.emit((secret.clone(), secret_type.clone()));
 | 
				
			||||||
 | 
					                    on_supervisor_info_loaded.emit(supervisor_info);
 | 
				
			||||||
 | 
					                    session_secret_input.set(String::new());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    console::error!("Failed to connect to supervisor:", format!("{:?}", e));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            is_loading.set(false);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let on_logout = {
 | 
				
			||||||
 | 
					    let on_session_secret_change = props.on_session_secret_change.clone();
 | 
				
			||||||
 | 
					    Callback::from(move |_: web_sys::MouseEvent| {
 | 
				
			||||||
 | 
					        // Clear localStorage
 | 
				
			||||||
 | 
					        let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
 | 
				
			||||||
 | 
					        on_session_secret_change.emit((String::new(), SessionSecretType::None));
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html! {
 | 
				
			||||||
 | 
					    <div class="sidebar">
 | 
				
			||||||
 | 
					        <div class="sidebar-header">
 | 
				
			||||||
 | 
					            <div class="logo-section">
 | 
				
			||||||
 | 
					                <div class="logo">{"⚡"}</div>
 | 
				
			||||||
 | 
					                <h1 class="title">{"Supervisor"}</h1>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="control-island">
 | 
				
			||||||
 | 
					            <div class="session-section">
 | 
				
			||||||
 | 
					                <h3 class="section-title">{"Session Login"}</h3>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if props.session_secret.is_empty() {
 | 
				
			||||||
 | 
					                    <div class="login-form">
 | 
				
			||||||
 | 
					                        <div class="input-group">
 | 
				
			||||||
 | 
					                            <input 
 | 
				
			||||||
 | 
					                                type="password" 
 | 
				
			||||||
 | 
					                                class="secret-input" 
 | 
				
			||||||
 | 
					                                placeholder="Enter session secret"
 | 
				
			||||||
 | 
					                                value={(*session_secret_input).clone()}
 | 
				
			||||||
 | 
					                                onchange={on_session_secret_change}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <button 
 | 
				
			||||||
 | 
					                                class="connect-btn"
 | 
				
			||||||
 | 
					                                onclick={on_session_secret_submit}
 | 
				
			||||||
 | 
					                                disabled={*is_loading}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                if *is_loading {
 | 
				
			||||||
 | 
					                                    <span class="loading-spinner"></span>
 | 
				
			||||||
 | 
					                                    {"Connecting"}
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    {"🔐 Connect"}
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="login-hint">
 | 
				
			||||||
 | 
					                            {"Use admin_ or user_ prefixed secrets"}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    <div class="session-active">
 | 
				
			||||||
 | 
					                        <div class="session-status">
 | 
				
			||||||
 | 
					                            <div class="status-indicator"></div>
 | 
				
			||||||
 | 
					                            <div class="status-info">
 | 
				
			||||||
 | 
					                                <span class="status-text">{"Connected"}</span>
 | 
				
			||||||
 | 
					                                <span class="session-badge">{
 | 
				
			||||||
 | 
					                                    match props.session_secret_type {
 | 
				
			||||||
 | 
					                                        SessionSecretType::Admin => "Admin",
 | 
				
			||||||
 | 
					                                        SessionSecretType::User => "User",
 | 
				
			||||||
 | 
					                                        SessionSecretType::None => "None",
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }</span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <button 
 | 
				
			||||||
 | 
					                            class="logout-btn"
 | 
				
			||||||
 | 
					                            onclick={on_logout}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            {"🚪 Logout"}
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Supervisor Info Section
 | 
				
			||||||
 | 
					            if let Some(info) = &props.supervisor_info {
 | 
				
			||||||
 | 
					                <div class="supervisor-info">
 | 
				
			||||||
 | 
					                    <div class="supervisor-info-header">
 | 
				
			||||||
 | 
					                        <span class="supervisor-info-title">{"Supervisor Info"}</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="supervisor-info-content">
 | 
				
			||||||
 | 
					                        <div class="info-item">
 | 
				
			||||||
 | 
					                            <span class="info-label">{"Admin secrets:"}</span>
 | 
				
			||||||
 | 
					                            <span class="info-value">{info.admin_secrets_count}</span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="info-item">
 | 
				
			||||||
 | 
					                            <span class="info-label">{"User secrets:"}</span>
 | 
				
			||||||
 | 
					                            <span class="info-value">{info.user_secrets_count}</span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="info-item">
 | 
				
			||||||
 | 
					                            <span class="info-label">{"Register secrets:"}</span>
 | 
				
			||||||
 | 
					                            <span class="info-value">{info.register_secrets_count}</span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="info-item">
 | 
				
			||||||
 | 
					                            <span class="info-label">{"Runners:"}</span>
 | 
				
			||||||
 | 
					                            <span class="info-value">{info.runners_count}</span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    {{ ... }}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										165
									
								
								clients/admin-ui/src/toast.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								clients/admin-ui/src/toast.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					//! Toast notification component for displaying errors, warnings, and info messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use yew::prelude::*;
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use gloo::timers::callback::Timeout;
 | 
				
			||||||
 | 
					use uuid::Uuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq)]
 | 
				
			||||||
 | 
					pub enum ToastType {
 | 
				
			||||||
 | 
					    Error,
 | 
				
			||||||
 | 
					    Warning,
 | 
				
			||||||
 | 
					    Info,
 | 
				
			||||||
 | 
					    Success,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ToastType {
 | 
				
			||||||
 | 
					    pub fn css_class(&self) -> &'static str {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            ToastType::Error => "toast-error",
 | 
				
			||||||
 | 
					            ToastType::Warning => "toast-warning", 
 | 
				
			||||||
 | 
					            ToastType::Info => "toast-info",
 | 
				
			||||||
 | 
					            ToastType::Success => "toast-success",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn icon(&self) -> &'static str {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            ToastType::Error => "❌",
 | 
				
			||||||
 | 
					            ToastType::Warning => "⚠️",
 | 
				
			||||||
 | 
					            ToastType::Info => "ℹ️",
 | 
				
			||||||
 | 
					            ToastType::Success => "✅",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn bg_class(&self) -> &'static str {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            ToastType::Error => "bg-danger",
 | 
				
			||||||
 | 
					            ToastType::Warning => "bg-warning",
 | 
				
			||||||
 | 
					            ToastType::Info => "bg-info", 
 | 
				
			||||||
 | 
					            ToastType::Success => "bg-success",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq)]
 | 
				
			||||||
 | 
					pub struct Toast {
 | 
				
			||||||
 | 
					    pub id: String,
 | 
				
			||||||
 | 
					    pub message: String,
 | 
				
			||||||
 | 
					    pub toast_type: ToastType,
 | 
				
			||||||
 | 
					    pub timestamp: f64,
 | 
				
			||||||
 | 
					    pub auto_dismiss: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Toast {
 | 
				
			||||||
 | 
					    pub fn new(message: String, toast_type: ToastType) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            id: Uuid::new_v4().to_string(),
 | 
				
			||||||
 | 
					            message,
 | 
				
			||||||
 | 
					            toast_type,
 | 
				
			||||||
 | 
					            timestamp: js_sys::Date::now(),
 | 
				
			||||||
 | 
					            auto_dismiss: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn error(message: String) -> Self {
 | 
				
			||||||
 | 
					        Self::new(message, ToastType::Error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn warning(message: String) -> Self {
 | 
				
			||||||
 | 
					        Self::new(message, ToastType::Warning)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn info(message: String) -> Self {
 | 
				
			||||||
 | 
					        Self::new(message, ToastType::Info)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn success(message: String) -> Self {
 | 
				
			||||||
 | 
					        Self::new(message, ToastType::Success)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn persistent(mut self) -> Self {
 | 
				
			||||||
 | 
					        self.auto_dismiss = false;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Properties, PartialEq)]
 | 
				
			||||||
 | 
					pub struct ToastContainerProps {
 | 
				
			||||||
 | 
					    pub toasts: Vec<Toast>,
 | 
				
			||||||
 | 
					    pub on_dismiss: Callback<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[function_component(ToastContainer)]
 | 
				
			||||||
 | 
					pub fn toast_container(props: &ToastContainerProps) -> Html {
 | 
				
			||||||
 | 
					    let timeouts = use_mut_ref(HashMap::<String, Timeout>::new);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Auto-dismiss toasts after 5 seconds
 | 
				
			||||||
 | 
					    use_effect_with(props.toasts.clone(), {
 | 
				
			||||||
 | 
					        let on_dismiss = props.on_dismiss.clone();
 | 
				
			||||||
 | 
					        let timeouts = timeouts.clone();
 | 
				
			||||||
 | 
					        move |toasts| {
 | 
				
			||||||
 | 
					            for toast in toasts {
 | 
				
			||||||
 | 
					                if toast.auto_dismiss {
 | 
				
			||||||
 | 
					                    let toast_id = toast.id.clone();
 | 
				
			||||||
 | 
					                    let on_dismiss = on_dismiss.clone();
 | 
				
			||||||
 | 
					                    let timeout = Timeout::new(5000, move || {
 | 
				
			||||||
 | 
					                        on_dismiss.emit(toast_id);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    timeouts.borrow_mut().insert(toast.id.clone(), timeout);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            move || {
 | 
				
			||||||
 | 
					                timeouts.borrow_mut().clear();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html! {
 | 
				
			||||||
 | 
					        <div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1055;">
 | 
				
			||||||
 | 
					            {for props.toasts.iter().map(|toast| {
 | 
				
			||||||
 | 
					                let on_dismiss = {
 | 
				
			||||||
 | 
					                    let on_dismiss = props.on_dismiss.clone();
 | 
				
			||||||
 | 
					                    let toast_id = toast.id.clone();
 | 
				
			||||||
 | 
					                    Callback::from(move |_| {
 | 
				
			||||||
 | 
					                        on_dismiss.emit(toast_id.clone());
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                html! {
 | 
				
			||||||
 | 
					                    <div key={toast.id.clone()} class={classes!("toast", "show", "mb-2")} role="alert">
 | 
				
			||||||
 | 
					                        <div class={classes!("toast-header", toast.toast_type.bg_class(), "text-white")}>
 | 
				
			||||||
 | 
					                            <span class="me-2">{toast.toast_type.icon()}</span>
 | 
				
			||||||
 | 
					                            <strong class="me-auto">{
 | 
				
			||||||
 | 
					                                match toast.toast_type {
 | 
				
			||||||
 | 
					                                    ToastType::Error => "Error",
 | 
				
			||||||
 | 
					                                    ToastType::Warning => "Warning", 
 | 
				
			||||||
 | 
					                                    ToastType::Info => "Info",
 | 
				
			||||||
 | 
					                                    ToastType::Success => "Success",
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }</strong>
 | 
				
			||||||
 | 
					                            <small class="text-white-50">{format_timestamp(toast.timestamp)}</small>
 | 
				
			||||||
 | 
					                            <button type="button" class="btn-close btn-close-white ms-2" onclick={on_dismiss}></button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="toast-body bg-dark text-white">
 | 
				
			||||||
 | 
					                            {toast.message.clone()}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn format_timestamp(timestamp: f64) -> String {
 | 
				
			||||||
 | 
					    let now = js_sys::Date::now();
 | 
				
			||||||
 | 
					    let diff = (now - timestamp) / 1000.0; // seconds
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if diff < 60.0 {
 | 
				
			||||||
 | 
					        "now".to_string()
 | 
				
			||||||
 | 
					    } else if diff < 3600.0 {
 | 
				
			||||||
 | 
					        format!("{}m ago", (diff / 60.0) as u32)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        format!("{}h ago", (diff / 3600.0) as u32)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										139
									
								
								clients/openrpc/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										139
									
								
								clients/openrpc/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -331,15 +331,6 @@ dependencies = [
 | 
				
			|||||||
 "typenum",
 | 
					 "typenum",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "deranged"
 | 
					 | 
				
			||||||
version = "0.4.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "powerfmt",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "digest"
 | 
					name = "digest"
 | 
				
			||||||
version = "0.10.7"
 | 
					version = "0.10.7"
 | 
				
			||||||
@@ -426,7 +417,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "futures-channel",
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 "futures-executor",
 | 
					 | 
				
			||||||
 "futures-io",
 | 
					 "futures-io",
 | 
				
			||||||
 "futures-sink",
 | 
					 "futures-sink",
 | 
				
			||||||
 "futures-task",
 | 
					 "futures-task",
 | 
				
			||||||
@@ -449,17 +439,6 @@ version = "0.3.31"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
					checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "futures-executor"
 | 
					 | 
				
			||||||
version = "0.3.31"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "futures-core",
 | 
					 | 
				
			||||||
 "futures-task",
 | 
					 | 
				
			||||||
 "futures-util",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "futures-io"
 | 
					name = "futures-io"
 | 
				
			||||||
version = "0.3.31"
 | 
					version = "0.3.31"
 | 
				
			||||||
@@ -585,19 +564,34 @@ version = "0.5.2"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
					checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "hero-job"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "redis",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "uuid",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hero-supervisor"
 | 
					name = "hero-supervisor"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "async-trait",
 | 
					 "async-trait",
 | 
				
			||||||
 | 
					 "base64",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "env_logger 0.10.2",
 | 
					 "env_logger 0.10.2",
 | 
				
			||||||
 | 
					 "hero-job",
 | 
				
			||||||
 "jsonrpsee",
 | 
					 "jsonrpsee",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 | 
					 "rand",
 | 
				
			||||||
 "redis",
 | 
					 "redis",
 | 
				
			||||||
 "sal-service-manager",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 "serde_json",
 | 
					 "serde_json",
 | 
				
			||||||
 "thiserror",
 | 
					 "thiserror",
 | 
				
			||||||
@@ -616,6 +610,7 @@ dependencies = [
 | 
				
			|||||||
 "console_log",
 | 
					 "console_log",
 | 
				
			||||||
 "env_logger 0.11.8",
 | 
					 "env_logger 0.11.8",
 | 
				
			||||||
 "getrandom 0.2.16",
 | 
					 "getrandom 0.2.16",
 | 
				
			||||||
 | 
					 "hero-job",
 | 
				
			||||||
 "hero-supervisor",
 | 
					 "hero-supervisor",
 | 
				
			||||||
 "js-sys",
 | 
					 "js-sys",
 | 
				
			||||||
 "jsonrpsee",
 | 
					 "jsonrpsee",
 | 
				
			||||||
@@ -1155,12 +1150,6 @@ dependencies = [
 | 
				
			|||||||
 "windows-sys 0.59.0",
 | 
					 "windows-sys 0.59.0",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "num-conv"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "num-traits"
 | 
					name = "num-traits"
 | 
				
			||||||
version = "0.2.19"
 | 
					version = "0.2.19"
 | 
				
			||||||
@@ -1258,19 +1247,6 @@ version = "0.1.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
					checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "plist"
 | 
					 | 
				
			||||||
version = "1.7.4"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "base64",
 | 
					 | 
				
			||||||
 "indexmap",
 | 
					 | 
				
			||||||
 "quick-xml",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "time",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "portable-atomic"
 | 
					name = "portable-atomic"
 | 
				
			||||||
version = "1.11.1"
 | 
					version = "1.11.1"
 | 
				
			||||||
@@ -1295,12 +1271,6 @@ dependencies = [
 | 
				
			|||||||
 "zerovec",
 | 
					 "zerovec",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "powerfmt"
 | 
					 | 
				
			||||||
version = "0.2.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "ppv-lite86"
 | 
					name = "ppv-lite86"
 | 
				
			||||||
version = "0.2.21"
 | 
					version = "0.2.21"
 | 
				
			||||||
@@ -1328,15 +1298,6 @@ dependencies = [
 | 
				
			|||||||
 "unicode-ident",
 | 
					 "unicode-ident",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "quick-xml"
 | 
					 | 
				
			||||||
version = "0.38.3"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "memchr",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "quote"
 | 
					name = "quote"
 | 
				
			||||||
version = "1.0.40"
 | 
					version = "1.0.40"
 | 
				
			||||||
@@ -1559,23 +1520,6 @@ version = "1.0.20"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 | 
					checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "sal-service-manager"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "async-trait",
 | 
					 | 
				
			||||||
 "chrono",
 | 
					 | 
				
			||||||
 "futures",
 | 
					 | 
				
			||||||
 "log",
 | 
					 | 
				
			||||||
 "once_cell",
 | 
					 | 
				
			||||||
 "plist",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "serde_json",
 | 
					 | 
				
			||||||
 "thiserror",
 | 
					 | 
				
			||||||
 "tokio",
 | 
					 | 
				
			||||||
 "zinit-client",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "same-file"
 | 
					name = "same-file"
 | 
				
			||||||
version = "1.0.6"
 | 
					version = "1.0.6"
 | 
				
			||||||
@@ -1813,37 +1757,6 @@ dependencies = [
 | 
				
			|||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time"
 | 
					 | 
				
			||||||
version = "0.3.41"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "deranged",
 | 
					 | 
				
			||||||
 "itoa",
 | 
					 | 
				
			||||||
 "num-conv",
 | 
					 | 
				
			||||||
 "powerfmt",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "time-core",
 | 
					 | 
				
			||||||
 "time-macros",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time-core"
 | 
					 | 
				
			||||||
version = "0.1.4"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "time-macros"
 | 
					 | 
				
			||||||
version = "0.2.22"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "num-conv",
 | 
					 | 
				
			||||||
 "time-core",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tinystr"
 | 
					name = "tinystr"
 | 
				
			||||||
version = "0.8.1"
 | 
					version = "0.8.1"
 | 
				
			||||||
@@ -2690,21 +2603,3 @@ dependencies = [
 | 
				
			|||||||
 "quote",
 | 
					 "quote",
 | 
				
			||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "zinit-client"
 | 
					 | 
				
			||||||
version = "0.4.0"
 | 
					 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					 | 
				
			||||||
checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd"
 | 
					 | 
				
			||||||
dependencies = [
 | 
					 | 
				
			||||||
 "async-stream",
 | 
					 | 
				
			||||||
 "async-trait",
 | 
					 | 
				
			||||||
 "chrono",
 | 
					 | 
				
			||||||
 "futures",
 | 
					 | 
				
			||||||
 "rand",
 | 
					 | 
				
			||||||
 "serde",
 | 
					 | 
				
			||||||
 "serde_json",
 | 
					 | 
				
			||||||
 "thiserror",
 | 
					 | 
				
			||||||
 "tokio",
 | 
					 | 
				
			||||||
 "tracing",
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,8 @@ crate-type = ["cdylib", "rlib"]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
# Common dependencies for both native and WASM
 | 
					# Common dependencies for both native and WASM
 | 
				
			||||||
 | 
					hero-supervisor = { path = "../../" }
 | 
				
			||||||
 | 
					hero-job = { path = "../../../job" }
 | 
				
			||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
thiserror = "1.0"
 | 
					thiserror = "1.0"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,14 +27,10 @@
 | 
				
			|||||||
//! ```
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
use thiserror::Error;
 | 
					use thiserror::Error;
 | 
				
			||||||
use serde_json;
 | 
					use serde_json;
 | 
				
			||||||
use uuid::Uuid;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Import types from the main supervisor crate
 | 
					// Import types from the main supervisor crate
 | 
				
			||||||
#[cfg(not(target_arch = "wasm32"))]
 | 
					 | 
				
			||||||
use hero_supervisor::RunnerStatus;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(target_arch = "wasm32")]
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
use wasm_bindgen::prelude::*;
 | 
					use wasm_bindgen::prelude::*;
 | 
				
			||||||
@@ -132,13 +128,12 @@ pub enum RunnerType {
 | 
				
			|||||||
    PyRunner,
 | 
					    PyRunner,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Process manager type for a runner
 | 
					/// Process manager type for WASM compatibility
 | 
				
			||||||
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 | 
					#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 | 
				
			||||||
pub enum ProcessManagerType {
 | 
					pub enum ProcessManagerType {
 | 
				
			||||||
    /// Simple process manager for direct process spawning
 | 
					 | 
				
			||||||
    Simple,
 | 
					    Simple,
 | 
				
			||||||
    /// Tmux process manager for session-based management
 | 
					    Tmux(String),
 | 
				
			||||||
    Tmux(String), // session name
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Configuration for an actor runner
 | 
					/// Configuration for an actor runner
 | 
				
			||||||
@@ -157,16 +152,6 @@ pub struct RunnerConfig {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Job status enumeration
 | 
					 | 
				
			||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub enum JobStatus {
 | 
					 | 
				
			||||||
    Dispatched,
 | 
					 | 
				
			||||||
    WaitingForPrerequisites,
 | 
					 | 
				
			||||||
    Started,
 | 
					 | 
				
			||||||
    Error,
 | 
					 | 
				
			||||||
    Stopping,
 | 
					 | 
				
			||||||
    Finished,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Job result response
 | 
					/// Job result response
 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
					#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
				
			||||||
@@ -186,30 +171,8 @@ pub struct JobStatusResponse {
 | 
				
			|||||||
    pub completed_at: Option<String>,
 | 
					    pub completed_at: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Job structure for creating and managing jobs
 | 
					// Re-export Job types from shared crate
 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
					pub use hero_job::{Job, JobStatus, JobError, JobBuilder};
 | 
				
			||||||
pub struct Job {
 | 
					 | 
				
			||||||
    /// Unique job identifier
 | 
					 | 
				
			||||||
    pub id: String,
 | 
					 | 
				
			||||||
    /// ID of the caller/client that created this job
 | 
					 | 
				
			||||||
    pub caller_id: String,
 | 
					 | 
				
			||||||
    /// Context ID for grouping related jobs
 | 
					 | 
				
			||||||
    pub context_id: String,
 | 
					 | 
				
			||||||
    /// Script content or payload to execute
 | 
					 | 
				
			||||||
    pub payload: String,
 | 
					 | 
				
			||||||
    /// Name of the specific runner/actor to execute this job
 | 
					 | 
				
			||||||
    pub runner: String,
 | 
					 | 
				
			||||||
    /// Name of the executor the runner will use to execute this job
 | 
					 | 
				
			||||||
    pub executor: String,
 | 
					 | 
				
			||||||
    /// Job execution timeout (in seconds)
 | 
					 | 
				
			||||||
    pub timeout: u64,
 | 
					 | 
				
			||||||
    /// Environment variables for job execution
 | 
					 | 
				
			||||||
    pub env_vars: HashMap<String, String>,
 | 
					 | 
				
			||||||
    /// Timestamp when the job was created
 | 
					 | 
				
			||||||
    pub created_at: String,
 | 
					 | 
				
			||||||
    /// Timestamp when the job was last updated
 | 
					 | 
				
			||||||
    pub updated_at: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// 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)]
 | 
				
			||||||
@@ -245,9 +208,21 @@ pub type ProcessStatus = ProcessStatusWrapper;
 | 
				
			|||||||
#[cfg(target_arch = "wasm32")]
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
pub type LogInfo = LogInfoWrapper;
 | 
					pub type LogInfo = LogInfoWrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple ProcessStatus type for native builds to avoid service manager dependency
 | 
				
			||||||
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
 | 
					pub type ProcessStatus = ProcessStatusWrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Re-export types from supervisor crate for native builds
 | 
					/// Re-export types from supervisor crate for native builds
 | 
				
			||||||
#[cfg(not(target_arch = "wasm32"))]
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
pub use hero_supervisor::{LogInfo, RunnerStatus as ProcessStatus};
 | 
					pub use hero_supervisor::{ProcessManagerType, RunnerStatus};
 | 
				
			||||||
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
 | 
					pub use hero_supervisor::runner::LogInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Type aliases for WASM compatibility
 | 
				
			||||||
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
 | 
					pub type RunnerStatus = ProcessStatusWrapper;
 | 
				
			||||||
 | 
					#[cfg(target_arch = "wasm32")]
 | 
				
			||||||
 | 
					pub type LogInfo = LogInfoWrapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(not(target_arch = "wasm32"))]
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
impl SupervisorClient {
 | 
					impl SupervisorClient {
 | 
				
			||||||
@@ -425,31 +400,12 @@ impl SupervisorClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Get status of a specific runner
 | 
					    /// Get status of a specific runner
 | 
				
			||||||
    pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<ProcessStatus> {
 | 
					    pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
 | 
				
			||||||
        #[cfg(target_arch = "wasm32")]
 | 
					        let status: RunnerStatus = self
 | 
				
			||||||
        {
 | 
					            .client
 | 
				
			||||||
            let status: ProcessStatusWrapper = self
 | 
					            .request("get_runner_status", rpc_params![actor_id])
 | 
				
			||||||
                .client
 | 
					            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
				
			||||||
                .request("get_runner_status", rpc_params![actor_id])
 | 
					        Ok(status)
 | 
				
			||||||
                .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					 | 
				
			||||||
            Ok(status)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        #[cfg(not(target_arch = "wasm32"))]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let status: ProcessStatusWrapper = self
 | 
					 | 
				
			||||||
                .client
 | 
					 | 
				
			||||||
                .request("get_runner_status", rpc_params![actor_id])
 | 
					 | 
				
			||||||
                .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					 | 
				
			||||||
            // Convert wrapper to internal type for native builds
 | 
					 | 
				
			||||||
            let internal_status = match status {
 | 
					 | 
				
			||||||
                ProcessStatusWrapper::Running => RunnerStatus::Running,
 | 
					 | 
				
			||||||
                ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
 | 
					 | 
				
			||||||
                ProcessStatusWrapper::Starting => RunnerStatus::Starting,
 | 
					 | 
				
			||||||
                ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
 | 
					 | 
				
			||||||
                ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            Ok(internal_status)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Get logs for a specific runner
 | 
					    /// Get logs for a specific runner
 | 
				
			||||||
@@ -459,28 +415,11 @@ impl SupervisorClient {
 | 
				
			|||||||
        lines: Option<usize>,
 | 
					        lines: Option<usize>,
 | 
				
			||||||
        follow: bool,
 | 
					        follow: bool,
 | 
				
			||||||
    ) -> ClientResult<Vec<LogInfo>> {
 | 
					    ) -> ClientResult<Vec<LogInfo>> {
 | 
				
			||||||
        #[cfg(target_arch = "wasm32")]
 | 
					        let logs: Vec<LogInfo> = self
 | 
				
			||||||
        {
 | 
					            .client
 | 
				
			||||||
            let logs: Vec<LogInfoWrapper> = self
 | 
					            .request("get_runner_logs", rpc_params![actor_id, lines, follow])
 | 
				
			||||||
                .client
 | 
					            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
				
			||||||
                .request("get_runner_logs", rpc_params![actor_id, lines, follow])
 | 
					        Ok(logs)
 | 
				
			||||||
                .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					 | 
				
			||||||
            Ok(logs)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        #[cfg(not(target_arch = "wasm32"))]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let logs: Vec<LogInfoWrapper> = self
 | 
					 | 
				
			||||||
                .client
 | 
					 | 
				
			||||||
                .request("get_runner_logs", rpc_params![actor_id, lines, follow])
 | 
					 | 
				
			||||||
                .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					 | 
				
			||||||
            // Convert wrapper to internal type for native builds
 | 
					 | 
				
			||||||
            let internal_logs = logs.into_iter().map(|log| hero_supervisor::LogInfo {
 | 
					 | 
				
			||||||
                timestamp: log.timestamp,
 | 
					 | 
				
			||||||
                level: log.level,
 | 
					 | 
				
			||||||
                message: log.message,
 | 
					 | 
				
			||||||
            }).collect();
 | 
					 | 
				
			||||||
            Ok(internal_logs)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Queue a job to a specific runner
 | 
					    /// Queue a job to a specific runner
 | 
				
			||||||
@@ -497,8 +436,7 @@ impl SupervisorClient {
 | 
				
			|||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Queue a job to a specific runner and wait for the result
 | 
					    /// Queue a job and wait for completion
 | 
				
			||||||
    /// This implements the proper Hero job protocol with BLPOP on reply queue
 | 
					 | 
				
			||||||
    pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
 | 
					    pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
 | 
				
			||||||
        let params = serde_json::json!({
 | 
					        let params = serde_json::json!({
 | 
				
			||||||
            "runner": runner,
 | 
					            "runner": runner,
 | 
				
			||||||
@@ -513,6 +451,20 @@ impl SupervisorClient {
 | 
				
			|||||||
        Ok(result)
 | 
					        Ok(result)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Run a job on a specific runner
 | 
				
			||||||
 | 
					    pub async fn run_job(&self, secret: &str, job: Job) -> ClientResult<JobResult> {
 | 
				
			||||||
 | 
					        let params = serde_json::json!({
 | 
				
			||||||
 | 
					            "secret": secret,
 | 
				
			||||||
 | 
					            "job": job
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let result: JobResult = self
 | 
				
			||||||
 | 
					            .client
 | 
				
			||||||
 | 
					            .request("run_job", rpc_params![params])
 | 
				
			||||||
 | 
					            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
				
			||||||
 | 
					        Ok(result)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    /// Get job result by job ID
 | 
					    /// Get job result by job ID
 | 
				
			||||||
    pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
 | 
					    pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
 | 
				
			||||||
        let result: Option<String> = self
 | 
					        let result: Option<String> = self
 | 
				
			||||||
@@ -523,31 +475,12 @@ impl SupervisorClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Get status of all runners
 | 
					    /// Get status of all runners
 | 
				
			||||||
    pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, ProcessStatus)>> {
 | 
					    pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
 | 
				
			||||||
        let statuses: Vec<(String, ProcessStatusWrapper)> = self
 | 
					        let statuses: Vec<(String, RunnerStatus)> = self
 | 
				
			||||||
            .client
 | 
					            .client
 | 
				
			||||||
            .request("get_all_runner_status", rpc_params![])
 | 
					            .request("get_all_runner_status", rpc_params![])
 | 
				
			||||||
            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
				
			||||||
        
 | 
					        Ok(statuses)
 | 
				
			||||||
        #[cfg(target_arch = "wasm32")]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(statuses)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        #[cfg(not(target_arch = "wasm32"))]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // Convert wrapper to internal type for native builds
 | 
					 | 
				
			||||||
            let internal_statuses = statuses.into_iter().map(|(name, status)| {
 | 
					 | 
				
			||||||
                let internal_status = match status {
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Running => RunnerStatus::Running,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Starting => RunnerStatus::Starting,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                (name, internal_status)
 | 
					 | 
				
			||||||
            }).collect();
 | 
					 | 
				
			||||||
            Ok(internal_statuses)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Start all runners
 | 
					    /// Start all runners
 | 
				
			||||||
@@ -569,31 +502,12 @@ impl SupervisorClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Get status of all runners (alternative method)
 | 
					    /// Get status of all runners (alternative method)
 | 
				
			||||||
    pub async fn get_all_status(&self) -> ClientResult<Vec<(String, ProcessStatus)>> {
 | 
					    pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
 | 
				
			||||||
        let statuses: Vec<(String, ProcessStatusWrapper)> = self
 | 
					        let statuses: Vec<(String, RunnerStatus)> = self
 | 
				
			||||||
            .client
 | 
					            .client
 | 
				
			||||||
            .request("get_all_status", rpc_params![])
 | 
					            .request("get_all_status", rpc_params![])
 | 
				
			||||||
            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
					            .await.map_err(|e| ClientError::JsonRpc(e))?;
 | 
				
			||||||
        
 | 
					        Ok(statuses)
 | 
				
			||||||
        #[cfg(target_arch = "wasm32")]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(statuses)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        #[cfg(not(target_arch = "wasm32"))]
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // Convert wrapper to internal type for native builds
 | 
					 | 
				
			||||||
            let internal_statuses = statuses.into_iter().map(|(name, status)| {
 | 
					 | 
				
			||||||
                let internal_status = match status {
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Running => RunnerStatus::Running,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Starting => RunnerStatus::Starting,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
 | 
					 | 
				
			||||||
                    ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                (name, internal_status)
 | 
					 | 
				
			||||||
            }).collect();
 | 
					 | 
				
			||||||
            Ok(internal_statuses)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Add a secret to the supervisor
 | 
					    /// Add a secret to the supervisor
 | 
				
			||||||
@@ -683,129 +597,6 @@ impl SupervisorClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Builder for creating jobs with a fluent API
 | 
					 | 
				
			||||||
pub struct JobBuilder {
 | 
					 | 
				
			||||||
    caller_id: String,
 | 
					 | 
				
			||||||
    context_id: String,
 | 
					 | 
				
			||||||
    payload: String,
 | 
					 | 
				
			||||||
    runner: String,
 | 
					 | 
				
			||||||
    executor: String,
 | 
					 | 
				
			||||||
    timeout: u64, // timeout in seconds
 | 
					 | 
				
			||||||
    env_vars: HashMap<String, String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl JobBuilder {
 | 
					 | 
				
			||||||
    /// Create a new job builder
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            caller_id: "".to_string(),
 | 
					 | 
				
			||||||
            context_id: "".to_string(),
 | 
					 | 
				
			||||||
            payload: "".to_string(),
 | 
					 | 
				
			||||||
            runner: "".to_string(),
 | 
					 | 
				
			||||||
            executor: "".to_string(),
 | 
					 | 
				
			||||||
            timeout: 300, // 5 minutes default
 | 
					 | 
				
			||||||
            env_vars: HashMap::new(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the caller ID for this job
 | 
					 | 
				
			||||||
    pub fn caller_id(mut self, caller_id: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.caller_id = caller_id.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the context ID for this job
 | 
					 | 
				
			||||||
    pub fn context_id(mut self, context_id: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.context_id = context_id.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the payload (script content) for this job
 | 
					 | 
				
			||||||
    pub fn payload(mut self, payload: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.payload = payload.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the executor for this job
 | 
					 | 
				
			||||||
    pub fn executor(mut self, executor: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.executor = executor.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the runner name for this job
 | 
					 | 
				
			||||||
    pub fn runner(mut self, runner: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.runner = runner.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the timeout for job execution (in seconds)
 | 
					 | 
				
			||||||
    pub fn timeout(mut self, timeout: u64) -> Self {
 | 
					 | 
				
			||||||
        self.timeout = timeout;
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set a single environment variable
 | 
					 | 
				
			||||||
    pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
 | 
					 | 
				
			||||||
        self.env_vars.insert(key.into(), value.into());
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set multiple environment variables from a HashMap
 | 
					 | 
				
			||||||
    pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
 | 
					 | 
				
			||||||
        self.env_vars = env_vars;
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Build the job
 | 
					 | 
				
			||||||
    pub fn build(self) -> ClientResult<Job> {
 | 
					 | 
				
			||||||
        if self.caller_id.is_empty() {
 | 
					 | 
				
			||||||
            return Err(ClientError::Server {
 | 
					 | 
				
			||||||
                message: "caller_id is required".to_string(),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.context_id.is_empty() {
 | 
					 | 
				
			||||||
            return Err(ClientError::Server {
 | 
					 | 
				
			||||||
                message: "context_id is required".to_string(),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.payload.is_empty() {
 | 
					 | 
				
			||||||
            return Err(ClientError::Server {
 | 
					 | 
				
			||||||
                message: "payload is required".to_string(),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.runner.is_empty() {
 | 
					 | 
				
			||||||
            return Err(ClientError::Server {
 | 
					 | 
				
			||||||
                message: "runner is required".to_string(),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.executor.is_empty() {
 | 
					 | 
				
			||||||
            return Err(ClientError::Server {
 | 
					 | 
				
			||||||
                message: "executor is required".to_string(),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let now = chrono::Utc::now().to_rfc3339();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Ok(Job {
 | 
					 | 
				
			||||||
            id: Uuid::new_v4().to_string(),
 | 
					 | 
				
			||||||
            caller_id: self.caller_id,
 | 
					 | 
				
			||||||
            context_id: self.context_id,
 | 
					 | 
				
			||||||
            payload: self.payload,
 | 
					 | 
				
			||||||
            runner: self.runner,
 | 
					 | 
				
			||||||
            executor: self.executor,
 | 
					 | 
				
			||||||
            timeout: self.timeout,
 | 
					 | 
				
			||||||
            env_vars: self.env_vars,
 | 
					 | 
				
			||||||
            created_at: now.clone(),
 | 
					 | 
				
			||||||
            updated_at: now,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for JobBuilder {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self::new()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
mod tests {
 | 
					mod tests {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -206,7 +206,7 @@ impl WasmSupervisorClient {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }]);
 | 
					        }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self.call_method("run_job", params).await {
 | 
					        match self.call_method("jobs.run", params).await {
 | 
				
			||||||
            Ok(result) => {
 | 
					            Ok(result) => {
 | 
				
			||||||
                if let Some(result_str) = result.as_str() {
 | 
					                if let Some(result_str) = result.as_str() {
 | 
				
			||||||
                    Ok(result_str.to_string())
 | 
					                    Ok(result_str.to_string())
 | 
				
			||||||
@@ -234,7 +234,7 @@ impl WasmSupervisorClient {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// List all job IDs from Redis
 | 
					    /// List all job IDs from Redis
 | 
				
			||||||
    pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
 | 
					    pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
 | 
				
			||||||
        match self.call_method("list_jobs", serde_json::Value::Null).await {
 | 
					        match self.call_method("jobs.list", serde_json::Value::Null).await {
 | 
				
			||||||
            Ok(result) => {
 | 
					            Ok(result) => {
 | 
				
			||||||
                if let Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
 | 
					                if let Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
 | 
				
			||||||
                    Ok(jobs)
 | 
					                    Ok(jobs)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								src/app.rs
									
									
									
									
									
								
							@@ -6,6 +6,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::Supervisor;
 | 
					use crate::Supervisor;
 | 
				
			||||||
use crate::openrpc::start_openrpc_servers;
 | 
					use crate::openrpc::start_openrpc_servers;
 | 
				
			||||||
 | 
					use crate::mycelium::MyceliumServer;
 | 
				
			||||||
use log::{info, error, debug};
 | 
					use log::{info, error, debug};
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
use tokio::sync::Mutex;
 | 
					use tokio::sync::Mutex;
 | 
				
			||||||
@@ -42,6 +43,9 @@ impl SupervisorApp {
 | 
				
			|||||||
        // Start OpenRPC server
 | 
					        // Start OpenRPC server
 | 
				
			||||||
        self.start_openrpc_server().await?;
 | 
					        self.start_openrpc_server().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Start Mycelium server
 | 
				
			||||||
 | 
					        self.start_mycelium_server().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Set up graceful shutdown
 | 
					        // Set up graceful shutdown
 | 
				
			||||||
        self.setup_graceful_shutdown().await;
 | 
					        self.setup_graceful_shutdown().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -146,6 +150,37 @@ impl SupervisorApp {
 | 
				
			|||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Start the Mycelium server
 | 
				
			||||||
 | 
					    async fn start_mycelium_server(&self) -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					        info!("Starting Mycelium server...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
 | 
				
			||||||
 | 
					        let mycelium_port = 8990; // Standard Mycelium port
 | 
				
			||||||
 | 
					        let bind_address = "127.0.0.1".to_string();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let mycelium_server = MyceliumServer::new(
 | 
				
			||||||
 | 
					            supervisor_for_mycelium,
 | 
				
			||||||
 | 
					            bind_address,
 | 
				
			||||||
 | 
					            mycelium_port,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Start the Mycelium server in a background task
 | 
				
			||||||
 | 
					        let server_handle = tokio::spawn(async move {
 | 
				
			||||||
 | 
					            if let Err(e) = mycelium_server.start().await {
 | 
				
			||||||
 | 
					                error!("Mycelium server error: {}", e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Give the server a moment to start
 | 
				
			||||||
 | 
					        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
 | 
				
			||||||
 | 
					        info!("Mycelium server started successfully on port {}", mycelium_port);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Store the handle for potential cleanup
 | 
				
			||||||
 | 
					        std::mem::forget(server_handle); // For now, let it run in background
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Get status of all runners
 | 
					    /// Get status of all runners
 | 
				
			||||||
    pub async fn get_status(&self) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
 | 
					    pub async fn get_status(&self) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
 | 
				
			||||||
        debug!("Getting status of all runners");
 | 
					        debug!("Getting status of all runners");
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										328
									
								
								src/client.rs
									
									
									
									
									
								
							
							
						
						
									
										328
									
								
								src/client.rs
									
									
									
									
									
								
							@@ -1,328 +0,0 @@
 | 
				
			|||||||
//! Main supervisor implementation for managing multiple actor runners.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::Utc;
 | 
					 | 
				
			||||||
use redis::AsyncCommands;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{runner::{RunnerError, RunnerResult}, job::JobStatus, JobError};
 | 
					 | 
				
			||||||
use crate::{job::Job};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Client for managing jobs in Redis
 | 
					 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					 | 
				
			||||||
pub struct Client {
 | 
					 | 
				
			||||||
    redis_client: redis::Client,
 | 
					 | 
				
			||||||
    namespace: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct ClientBuilder {
 | 
					 | 
				
			||||||
    /// Redis URL for connection
 | 
					 | 
				
			||||||
    redis_url: String,
 | 
					 | 
				
			||||||
    /// Namespace for queue keys
 | 
					 | 
				
			||||||
    namespace: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ClientBuilder {
 | 
					 | 
				
			||||||
    /// Create a new supervisor builder
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            redis_url: "redis://localhost:6379".to_string(),
 | 
					 | 
				
			||||||
            namespace: "".to_string(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the Redis URL
 | 
					 | 
				
			||||||
    pub fn redis_url<S: Into<String>>(mut self, url: S) -> Self {
 | 
					 | 
				
			||||||
        self.redis_url = url.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the namespace for queue keys
 | 
					 | 
				
			||||||
    pub fn namespace<S: Into<String>>(mut self, namespace: S) -> Self {
 | 
					 | 
				
			||||||
        self.namespace = namespace.into();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Build the supervisor
 | 
					 | 
				
			||||||
    pub async fn build(self) -> RunnerResult<Client> {
 | 
					 | 
				
			||||||
        // Create Redis client
 | 
					 | 
				
			||||||
        let redis_client = redis::Client::open(self.redis_url.as_str())
 | 
					 | 
				
			||||||
            .map_err(|e| RunnerError::ConfigError {
 | 
					 | 
				
			||||||
                reason: format!("Invalid Redis URL: {}", e),
 | 
					 | 
				
			||||||
            })?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Client {
 | 
					 | 
				
			||||||
            redis_client,
 | 
					 | 
				
			||||||
            namespace: self.namespace,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for Client {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        // Note: Default implementation creates an empty supervisor
 | 
					 | 
				
			||||||
        // Use Supervisor::builder() for proper initialization
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            redis_client: redis::Client::open("redis://localhost:6379").unwrap(),
 | 
					 | 
				
			||||||
            namespace: "".to_string(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Client {
 | 
					 | 
				
			||||||
    /// Create a new supervisor builder
 | 
					 | 
				
			||||||
    pub fn builder() -> ClientBuilder {
 | 
					 | 
				
			||||||
        ClientBuilder::new()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// List all job IDs from Redis
 | 
					 | 
				
			||||||
    pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| RunnerError::RedisError { source: e })?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let keys: Vec<String> = conn.keys(format!("{}:*", &self.jobs_key())).await?;
 | 
					 | 
				
			||||||
        let job_ids: Vec<String> = keys
 | 
					 | 
				
			||||||
            .into_iter()
 | 
					 | 
				
			||||||
            .filter_map(|key| {
 | 
					 | 
				
			||||||
                if key.starts_with(&format!("{}:", self.jobs_key())) {
 | 
					 | 
				
			||||||
                    key.strip_prefix(&format!("{}:", self.jobs_key()))
 | 
					 | 
				
			||||||
                        .map(|s| s.to_string())
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    None
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .collect();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Ok(job_ids)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn jobs_key(&self) -> String {
 | 
					 | 
				
			||||||
        if self.namespace.is_empty() {
 | 
					 | 
				
			||||||
            format!("job")
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            format!("{}:job", self.namespace)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn job_key(&self, job_id: &str) -> String {
 | 
					 | 
				
			||||||
        if self.namespace.is_empty() {
 | 
					 | 
				
			||||||
            format!("job:{}", job_id)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            format!("{}:job:{}", self.namespace, job_id)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn job_reply_key(&self, job_id: &str) -> String {
 | 
					 | 
				
			||||||
        if self.namespace.is_empty() {
 | 
					 | 
				
			||||||
            format!("reply:{}", job_id)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            format!("{}:reply:{}", self.namespace, job_id)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set job error in Redis
 | 
					 | 
				
			||||||
    pub async fn set_error(&self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
        error: &str,
 | 
					 | 
				
			||||||
    ) -> Result<(), JobError> {
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        let now = Utc::now();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
        conn.hset_multiple(&job_key, &[
 | 
					 | 
				
			||||||
                ("error", error),
 | 
					 | 
				
			||||||
                ("status", JobStatus::Error.as_str()),
 | 
					 | 
				
			||||||
                ("updated_at", &now.to_rfc3339()),
 | 
					 | 
				
			||||||
            ]).await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError::Redis(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set job status in Redis
 | 
					 | 
				
			||||||
    pub async fn set_job_status(&self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
        status: JobStatus,
 | 
					 | 
				
			||||||
    ) -> Result<(), JobError> {
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        let now = Utc::now();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
        conn.hset_multiple(&job_key, &[
 | 
					 | 
				
			||||||
                ("status", status.as_str()),
 | 
					 | 
				
			||||||
                ("updated_at", &now.to_rfc3339()),
 | 
					 | 
				
			||||||
            ]).await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError::Redis(e))?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// Get job status from Redis
 | 
					 | 
				
			||||||
        pub async fn get_status(
 | 
					 | 
				
			||||||
            &self,
 | 
					 | 
				
			||||||
            job_id: &str,
 | 
					 | 
				
			||||||
        ) -> Result<JobStatus, JobError> {
 | 
					 | 
				
			||||||
            let mut conn = self.redis_client
 | 
					 | 
				
			||||||
                .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let status_str: Option<String> = conn.hget(&self.job_key(job_id), "status").await?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            match status_str {
 | 
					 | 
				
			||||||
                Some(s) => JobStatus::from_str(&s).ok_or_else(|| JobError::InvalidStatus(s)),
 | 
					 | 
				
			||||||
                None => Err(JobError::NotFound(job_id.to_string())),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Delete job from Redis
 | 
					 | 
				
			||||||
    pub async fn delete_from_redis(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
    ) -> Result<(), JobError> {
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
                .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        let _: () = conn.del(&job_key).await?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Store this job in Redis
 | 
					 | 
				
			||||||
    pub async fn store_job_in_redis(&self, job: &Job) -> Result<(), JobError> {
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let job_key = self.job_key(&job.id);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Serialize the job data
 | 
					 | 
				
			||||||
        let job_data = serde_json::to_string(job)?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Store job data in Redis hash
 | 
					 | 
				
			||||||
        let _: () = conn.hset_multiple(&job_key, &[
 | 
					 | 
				
			||||||
            ("data", job_data),
 | 
					 | 
				
			||||||
            ("status", JobStatus::Dispatched.as_str().to_string()),
 | 
					 | 
				
			||||||
            ("created_at", job.created_at.to_rfc3339()),
 | 
					 | 
				
			||||||
            ("updated_at", job.updated_at.to_rfc3339()),
 | 
					 | 
				
			||||||
        ]).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Set TTL for the job (24 hours)
 | 
					 | 
				
			||||||
        let _: () = conn.expire(&job_key, 86400).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Load a job from Redis by ID
 | 
					 | 
				
			||||||
    pub async fn load_job_from_redis(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
    ) -> Result<Job, JobError> {
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Get job data from Redis
 | 
					 | 
				
			||||||
        let job_data: Option<String> = conn.hget(&job_key, "data").await?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        match job_data {
 | 
					 | 
				
			||||||
            Some(data) => {
 | 
					 | 
				
			||||||
                let job: Job = serde_json::from_str(&data)?;
 | 
					 | 
				
			||||||
                Ok(job)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            None => Err(JobError::NotFound(job_id.to_string())),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Delete a job by ID
 | 
					 | 
				
			||||||
    pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
 | 
					 | 
				
			||||||
        use redis::AsyncCommands;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client.get_multiplexed_async_connection().await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        let deleted_count: i32 = conn.del(&job_key).await
 | 
					 | 
				
			||||||
            .map_err(|e| RunnerError::QueueError {
 | 
					 | 
				
			||||||
                actor_id: job_id.to_string(),
 | 
					 | 
				
			||||||
                reason: format!("Failed to delete job: {}", e),
 | 
					 | 
				
			||||||
            })?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if deleted_count == 0 {
 | 
					 | 
				
			||||||
            return Err(RunnerError::QueueError {
 | 
					 | 
				
			||||||
                actor_id: job_id.to_string(),
 | 
					 | 
				
			||||||
                reason: format!("Job '{}' not found or already deleted", job_id),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set job result in Redis
 | 
					 | 
				
			||||||
    pub async fn set_result(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
        result: &str,
 | 
					 | 
				
			||||||
    ) -> Result<(), JobError> {
 | 
					 | 
				
			||||||
        let job_key = self.job_key(&job_id);
 | 
					 | 
				
			||||||
        let now = Utc::now();
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
        let _: () = conn.hset_multiple(&job_key, &[
 | 
					 | 
				
			||||||
            ("result", result),
 | 
					 | 
				
			||||||
            ("status", JobStatus::Finished.as_str()),
 | 
					 | 
				
			||||||
            ("updated_at", &now.to_rfc3339()),
 | 
					 | 
				
			||||||
        ]).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Get job result from Redis
 | 
					 | 
				
			||||||
    pub async fn get_result(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        job_id: &str,
 | 
					 | 
				
			||||||
    ) -> Result<Option<String>, JobError> {
 | 
					 | 
				
			||||||
        let job_key = self.job_key(job_id);
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError:: Redis(e))?;
 | 
					 | 
				
			||||||
        let result: Option<String> = conn.hget(&job_key, "result").await?;
 | 
					 | 
				
			||||||
        Ok(result)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Get a job ID from the work queue (blocking pop)
 | 
					 | 
				
			||||||
    pub async fn get_job_id(&self, queue_key: &str) -> Result<Option<String>, JobError> {
 | 
					 | 
				
			||||||
        let mut conn = self.redis_client
 | 
					 | 
				
			||||||
            .get_multiplexed_async_connection()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError::Redis(e))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Use BRPOP with a short timeout to avoid blocking indefinitely
 | 
					 | 
				
			||||||
        let result: Option<(String, String)> = conn.brpop(queue_key, 1.0).await
 | 
					 | 
				
			||||||
            .map_err(|e| JobError::Redis(e))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Ok(result.map(|(_, job_id)| job_id))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Get a job by ID (alias for load_job_from_redis)
 | 
					 | 
				
			||||||
    pub async fn get_job(&self, job_id: &str) -> Result<Job, JobError> {
 | 
					 | 
				
			||||||
        self.load_job_from_redis(job_id).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										220
									
								
								src/job.rs
									
									
									
									
									
								
							
							
						
						
									
										220
									
								
								src/job.rs
									
									
									
									
									
								
							@@ -1,218 +1,2 @@
 | 
				
			|||||||
use chrono::{DateTime, Utc};
 | 
					// Re-export job types from the separate job crate
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					pub use hero_job::*;
 | 
				
			||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
use uuid::Uuid;
 | 
					 | 
				
			||||||
use thiserror::Error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Job status enumeration
 | 
					 | 
				
			||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub enum JobStatus {
 | 
					 | 
				
			||||||
    Dispatched,
 | 
					 | 
				
			||||||
    WaitingForPrerequisites,
 | 
					 | 
				
			||||||
    Started,
 | 
					 | 
				
			||||||
    Error,
 | 
					 | 
				
			||||||
    Stopping,
 | 
					 | 
				
			||||||
    Finished,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl JobStatus {
 | 
					 | 
				
			||||||
    pub fn as_str(&self) -> &'static str {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            JobStatus::Dispatched => "dispatched",
 | 
					 | 
				
			||||||
            JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
 | 
					 | 
				
			||||||
            JobStatus::Started => "started",
 | 
					 | 
				
			||||||
            JobStatus::Error => "error",
 | 
					 | 
				
			||||||
            JobStatus::Stopping => "stopping",
 | 
					 | 
				
			||||||
            JobStatus::Finished => "finished",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn from_str(s: &str) -> Option<Self> {
 | 
					 | 
				
			||||||
        match s {
 | 
					 | 
				
			||||||
            "dispatched" => Some(JobStatus::Dispatched),
 | 
					 | 
				
			||||||
            "waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
 | 
					 | 
				
			||||||
            "started" => Some(JobStatus::Started),
 | 
					 | 
				
			||||||
            "error" => Some(JobStatus::Error),
 | 
					 | 
				
			||||||
            "stopping" => Some(JobStatus::Stopping),
 | 
					 | 
				
			||||||
            "finished" => Some(JobStatus::Finished),
 | 
					 | 
				
			||||||
            _ => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Representation of a script execution request.
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// This structure contains all the information needed to execute a script
 | 
					 | 
				
			||||||
/// on a actor service, including the script content, dependencies, and metadata.
 | 
					 | 
				
			||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct Job {
 | 
					 | 
				
			||||||
    pub id: String,
 | 
					 | 
				
			||||||
    pub caller_id: String,
 | 
					 | 
				
			||||||
    pub context_id: String,
 | 
					 | 
				
			||||||
    pub payload: String,
 | 
					 | 
				
			||||||
    pub runner: String, // name of the runner to execute this job
 | 
					 | 
				
			||||||
    pub executor: String, // name of the executor the runner will use to execute this job
 | 
					 | 
				
			||||||
    pub timeout: u64, // timeout in seconds
 | 
					 | 
				
			||||||
    pub env_vars: HashMap<String, String>, // environment variables for script execution
 | 
					 | 
				
			||||||
    pub created_at: chrono::DateTime<chrono::Utc>,
 | 
					 | 
				
			||||||
    pub updated_at: chrono::DateTime<chrono::Utc>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Error types for job operations
 | 
					 | 
				
			||||||
#[derive(Error, Debug)]
 | 
					 | 
				
			||||||
pub enum JobError {
 | 
					 | 
				
			||||||
    #[error("Redis error: {0}")]
 | 
					 | 
				
			||||||
    Redis(#[from] redis::RedisError),
 | 
					 | 
				
			||||||
    #[error("Serialization error: {0}")]
 | 
					 | 
				
			||||||
    Serialization(#[from] serde_json::Error),
 | 
					 | 
				
			||||||
    #[error("Job not found: {0}")]
 | 
					 | 
				
			||||||
    NotFound(String),
 | 
					 | 
				
			||||||
    #[error("Invalid job status: {0}")]
 | 
					 | 
				
			||||||
    InvalidStatus(String),
 | 
					 | 
				
			||||||
    #[error("Timeout error: {0}")]
 | 
					 | 
				
			||||||
    Timeout(String),
 | 
					 | 
				
			||||||
    #[error("Invalid job data: {0}")]
 | 
					 | 
				
			||||||
    InvalidData(String),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Job {
 | 
					 | 
				
			||||||
    /// Create a new job with the given parameters
 | 
					 | 
				
			||||||
    pub fn new(
 | 
					 | 
				
			||||||
        caller_id: String,
 | 
					 | 
				
			||||||
        context_id: String,
 | 
					 | 
				
			||||||
        payload: String,
 | 
					 | 
				
			||||||
        runner: String,
 | 
					 | 
				
			||||||
        executor: String,
 | 
					 | 
				
			||||||
    ) -> Self {
 | 
					 | 
				
			||||||
        let now = Utc::now();
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            id: Uuid::new_v4().to_string(),
 | 
					 | 
				
			||||||
            caller_id,
 | 
					 | 
				
			||||||
            context_id,
 | 
					 | 
				
			||||||
            payload,
 | 
					 | 
				
			||||||
            runner,
 | 
					 | 
				
			||||||
            executor,
 | 
					 | 
				
			||||||
            timeout: 300, // 5 minutes default
 | 
					 | 
				
			||||||
            env_vars: HashMap::new(),
 | 
					 | 
				
			||||||
            created_at: now,
 | 
					 | 
				
			||||||
            updated_at: now,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Builder for constructing job execution requests.
 | 
					 | 
				
			||||||
pub struct JobBuilder {
 | 
					 | 
				
			||||||
    caller_id: String,
 | 
					 | 
				
			||||||
    context_id: String,
 | 
					 | 
				
			||||||
    payload: String,
 | 
					 | 
				
			||||||
    runner: String,
 | 
					 | 
				
			||||||
    executor: String,
 | 
					 | 
				
			||||||
    timeout: u64, // timeout in seconds
 | 
					 | 
				
			||||||
    env_vars: HashMap<String, String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl JobBuilder {
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            caller_id: "".to_string(),
 | 
					 | 
				
			||||||
            context_id: "".to_string(),
 | 
					 | 
				
			||||||
            payload: "".to_string(),
 | 
					 | 
				
			||||||
            runner: "".to_string(),
 | 
					 | 
				
			||||||
            executor: "".to_string(),
 | 
					 | 
				
			||||||
            timeout: 300, // 5 minutes default
 | 
					 | 
				
			||||||
            env_vars: HashMap::new(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the caller ID for this job
 | 
					 | 
				
			||||||
    pub fn caller_id(mut self, caller_id: &str) -> Self {
 | 
					 | 
				
			||||||
        self.caller_id = caller_id.to_string();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the context ID for this job
 | 
					 | 
				
			||||||
    pub fn context_id(mut self, context_id: &str) -> Self {
 | 
					 | 
				
			||||||
        self.context_id = context_id.to_string();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the payload (script content) for this job
 | 
					 | 
				
			||||||
    pub fn payload(mut self, payload: &str) -> Self {
 | 
					 | 
				
			||||||
        self.payload = payload.to_string();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the runner name for this job
 | 
					 | 
				
			||||||
    pub fn runner(mut self, runner: &str) -> Self {
 | 
					 | 
				
			||||||
        self.runner = runner.to_string();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the executor for this job
 | 
					 | 
				
			||||||
    pub fn executor(mut self, executor: &str) -> Self {
 | 
					 | 
				
			||||||
        self.executor = executor.to_string();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set the timeout for job execution (in seconds)
 | 
					 | 
				
			||||||
    pub fn timeout(mut self, timeout: u64) -> Self {
 | 
					 | 
				
			||||||
        self.timeout = timeout;
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set a single environment variable
 | 
					 | 
				
			||||||
    pub fn env_var(mut self, key: &str, value: &str) -> Self {
 | 
					 | 
				
			||||||
        self.env_vars.insert(key.to_string(), value.to_string());
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Set multiple environment variables from a HashMap
 | 
					 | 
				
			||||||
    pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
 | 
					 | 
				
			||||||
        self.env_vars = env_vars;
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Clear all environment variables
 | 
					 | 
				
			||||||
    pub fn clear_env_vars(mut self) -> Self {
 | 
					 | 
				
			||||||
        self.env_vars.clear();
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Build the job
 | 
					 | 
				
			||||||
    pub fn build(self) -> Result<Job, JobError> {
 | 
					 | 
				
			||||||
        if self.caller_id.is_empty() {
 | 
					 | 
				
			||||||
            return Err(JobError::InvalidData("caller_id is required".to_string()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.context_id.is_empty() {
 | 
					 | 
				
			||||||
            return Err(JobError::InvalidData("context_id is required".to_string()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.payload.is_empty() {
 | 
					 | 
				
			||||||
            return Err(JobError::InvalidData("payload is required".to_string()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.runner.is_empty() {
 | 
					 | 
				
			||||||
            return Err(JobError::InvalidData("runner is required".to_string()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if self.executor.is_empty() {
 | 
					 | 
				
			||||||
            return Err(JobError::InvalidData("executor is required".to_string()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut job = Job::new(
 | 
					 | 
				
			||||||
            self.caller_id,
 | 
					 | 
				
			||||||
            self.context_id,
 | 
					 | 
				
			||||||
            self.payload,
 | 
					 | 
				
			||||||
            self.runner,
 | 
					 | 
				
			||||||
            self.executor,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        job.timeout = self.timeout;
 | 
					 | 
				
			||||||
        job.env_vars = self.env_vars;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(job)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for JobBuilder {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self::new()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/lib.rs
									
									
									
									
									
								
							@@ -3,20 +3,17 @@
 | 
				
			|||||||
//! See README.md for detailed documentation and usage examples.
 | 
					//! See README.md for detailed documentation and usage examples.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod runner;
 | 
					pub mod runner;
 | 
				
			||||||
pub mod supervisor;
 | 
					 | 
				
			||||||
pub mod job;
 | 
					pub mod job;
 | 
				
			||||||
pub mod client;
 | 
					pub mod supervisor;
 | 
				
			||||||
pub mod app;
 | 
					pub mod app;
 | 
				
			||||||
 | 
					 | 
				
			||||||
// OpenRPC server module
 | 
					 | 
				
			||||||
pub mod openrpc;
 | 
					pub mod openrpc;
 | 
				
			||||||
 | 
					pub mod mycelium;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Re-export main types for convenience
 | 
					// Re-export main types for convenience
 | 
				
			||||||
pub use runner::{
 | 
					pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
 | 
				
			||||||
    LogInfo, 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 job::{Job, JobBuilder, JobStatus, JobError};
 | 
					pub use hero_job::{Job, JobBuilder, JobStatus, JobError};
 | 
				
			||||||
pub use client::{Client, ClientBuilder};
 | 
					pub use hero_job::Client;
 | 
				
			||||||
pub use app::SupervisorApp;
 | 
					pub use app::SupervisorApp;
 | 
				
			||||||
 | 
					pub use mycelium::MyceliumServer;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										297
									
								
								src/mycelium.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/mycelium.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
				
			|||||||
 | 
					//! # Mycelium Server Integration for Hero Supervisor
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! This module implements a Mycelium-compatible JSON-RPC server that bridges
 | 
				
			||||||
 | 
					//! Mycelium transport messages to the supervisor's OpenRPC interface.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use tokio::sync::Mutex;
 | 
				
			||||||
 | 
					use serde_json::{Value, json};
 | 
				
			||||||
 | 
					use log::{info, error, debug};
 | 
				
			||||||
 | 
					use base64::Engine;
 | 
				
			||||||
 | 
					use crate::Supervisor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Mycelium server that handles pushMessage calls and forwards them to supervisor
 | 
				
			||||||
 | 
					pub struct MyceliumServer {
 | 
				
			||||||
 | 
					    supervisor: Arc<Mutex<Supervisor>>,
 | 
				
			||||||
 | 
					    bind_address: String,
 | 
				
			||||||
 | 
					    port: u16,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl MyceliumServer {
 | 
				
			||||||
 | 
					    pub fn new(supervisor: Arc<Mutex<Supervisor>>, bind_address: String, port: u16) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            supervisor,
 | 
				
			||||||
 | 
					            bind_address,
 | 
				
			||||||
 | 
					            port,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Start the Mycelium-compatible JSON-RPC server
 | 
				
			||||||
 | 
					    pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					        use jsonrpsee::server::{ServerBuilder, RpcModule};
 | 
				
			||||||
 | 
					        use tower_http::cors::{CorsLayer, Any};
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        info!("Starting Mycelium server on {}:{}", self.bind_address, self.port);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let cors = CorsLayer::new()
 | 
				
			||||||
 | 
					            .allow_methods(Any)
 | 
				
			||||||
 | 
					            .allow_headers(Any)
 | 
				
			||||||
 | 
					            .allow_origin(Any);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let server = ServerBuilder::default()
 | 
				
			||||||
 | 
					            .build(format!("{}:{}", self.bind_address, self.port))
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let mut module = RpcModule::new(());
 | 
				
			||||||
 | 
					        let supervisor_clone = self.supervisor.clone();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Register pushMessage method
 | 
				
			||||||
 | 
					        module.register_async_method("pushMessage", move |params, _, _| {
 | 
				
			||||||
 | 
					            let supervisor = supervisor_clone.clone();
 | 
				
			||||||
 | 
					            async move {
 | 
				
			||||||
 | 
					                handle_push_message(supervisor, params).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Register messageStatus method (basic implementation)
 | 
				
			||||||
 | 
					        module.register_async_method("messageStatus", |params, _, _| async move {
 | 
				
			||||||
 | 
					            handle_message_status(params).await
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let handle = server.start(module);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        info!("Mycelium server started successfully on {}:{}", self.bind_address, self.port);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Keep the server running
 | 
				
			||||||
 | 
					        handle.stopped().await;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Handle pushMessage calls from Mycelium clients
 | 
				
			||||||
 | 
					async fn handle_push_message(
 | 
				
			||||||
 | 
					    supervisor: Arc<Mutex<Supervisor>>,
 | 
				
			||||||
 | 
					    params: jsonrpsee::types::Params<'_>,
 | 
				
			||||||
 | 
					) -> Result<Value, jsonrpsee::types::ErrorObjectOwned> {
 | 
				
			||||||
 | 
					    // Parse params as array first, then get the first element
 | 
				
			||||||
 | 
					    let params_array: Vec<Value> = params.parse()
 | 
				
			||||||
 | 
					        .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(e.to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let params_value = params_array.get(0)
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing params object".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    debug!("Received pushMessage: {}", params_value);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Extract message from params
 | 
				
			||||||
 | 
					    let message = params_value.get("message")
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing message".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Extract payload (base64 encoded supervisor JSON-RPC)
 | 
				
			||||||
 | 
					    let payload_b64 = message.get("payload")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing payload".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Extract topic and destination (for logging/debugging)
 | 
				
			||||||
 | 
					    let _topic = message.get("topic")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("supervisor.rpc");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let _dst = message.get("dst");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Check if this is a reply timeout request
 | 
				
			||||||
 | 
					    let reply_timeout = params_value.get("reply_timeout")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_u64());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Decode the supervisor JSON-RPC payload
 | 
				
			||||||
 | 
					    let payload_bytes = base64::engine::general_purpose::STANDARD
 | 
				
			||||||
 | 
					        .decode(payload_b64)
 | 
				
			||||||
 | 
					        .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(format!("invalid base64: {}", e))))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let supervisor_rpc: Value = serde_json::from_slice(&payload_bytes)
 | 
				
			||||||
 | 
					        .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(format!("invalid JSON: {}", e))))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    debug!("Decoded supervisor RPC: {}", supervisor_rpc);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Extract method and params from supervisor JSON-RPC
 | 
				
			||||||
 | 
					    let method = supervisor_rpc.get("method")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing method".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let rpc_params = supervisor_rpc.get("params")
 | 
				
			||||||
 | 
					        .cloned()
 | 
				
			||||||
 | 
					        .unwrap_or(json!([]));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let rpc_id = supervisor_rpc.get("id").cloned();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Route to appropriate supervisor method
 | 
				
			||||||
 | 
					    let result = route_supervisor_call(supervisor, method, rpc_params).await
 | 
				
			||||||
 | 
					        .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32603, "Internal error", Some(e.to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Generate message ID for tracking
 | 
				
			||||||
 | 
					    let message_id = format!("{:016x}", rand::random::<u64>());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if let Some(_timeout) = reply_timeout {
 | 
				
			||||||
 | 
					        // For sync calls, return the supervisor result as an InboundMessage
 | 
				
			||||||
 | 
					        let supervisor_response = json!({
 | 
				
			||||||
 | 
					            "jsonrpc": "2.0",
 | 
				
			||||||
 | 
					            "id": rpc_id,
 | 
				
			||||||
 | 
					            "result": result
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let response_b64 = base64::engine::general_purpose::STANDARD
 | 
				
			||||||
 | 
					            .encode(serde_json::to_string(&supervisor_response).unwrap().as_bytes());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Ok(json!({
 | 
				
			||||||
 | 
					            "id": message_id,
 | 
				
			||||||
 | 
					            "srcIp": "127.0.0.1",
 | 
				
			||||||
 | 
					            "payload": response_b64
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // For async calls, just return the message ID
 | 
				
			||||||
 | 
					        Ok(json!({
 | 
				
			||||||
 | 
					            "id": message_id
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Handle messageStatus calls
 | 
				
			||||||
 | 
					async fn handle_message_status(
 | 
				
			||||||
 | 
					    params: jsonrpsee::types::Params<'_>,
 | 
				
			||||||
 | 
					) -> Result<Value, jsonrpsee::types::ErrorObjectOwned> {
 | 
				
			||||||
 | 
					    // Parse params as array first, then get the first element
 | 
				
			||||||
 | 
					    let params_array: Vec<Value> = params.parse()
 | 
				
			||||||
 | 
					        .map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(e.to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let params_value = params_array.get(0)
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing params object".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let _message_id = params_value.get("id")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					        .ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing id".to_string())))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // For simplicity, always return "delivered" status
 | 
				
			||||||
 | 
					    Ok(json!({
 | 
				
			||||||
 | 
					        "status": "delivered"
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Route supervisor method calls to the appropriate supervisor functions
 | 
				
			||||||
 | 
					async fn route_supervisor_call(
 | 
				
			||||||
 | 
					    supervisor: Arc<Mutex<Supervisor>>,
 | 
				
			||||||
 | 
					    method: &str,
 | 
				
			||||||
 | 
					    params: Value,
 | 
				
			||||||
 | 
					) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
 | 
				
			||||||
 | 
					    let mut supervisor_guard = supervisor.lock().await;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    match method {
 | 
				
			||||||
 | 
					        "list_runners" => {
 | 
				
			||||||
 | 
					            let runners = supervisor_guard.list_runners();
 | 
				
			||||||
 | 
					            Ok(json!(runners))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "register_runner" => {
 | 
				
			||||||
 | 
					            if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
 | 
				
			||||||
 | 
					                let secret = param_obj.get("secret")
 | 
				
			||||||
 | 
					                    .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					                    .ok_or("missing secret")?;
 | 
				
			||||||
 | 
					                let name = param_obj.get("name")
 | 
				
			||||||
 | 
					                    .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					                    .ok_or("missing name")?;
 | 
				
			||||||
 | 
					                let queue = param_obj.get("queue")
 | 
				
			||||||
 | 
					                    .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					                    .ok_or("missing queue")?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                supervisor_guard.register_runner(secret, name, queue).await?;
 | 
				
			||||||
 | 
					                Ok(json!("success"))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid register_runner params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "start_runner" => {
 | 
				
			||||||
 | 
					            if let Some(actor_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                supervisor_guard.start_runner(actor_id).await?;
 | 
				
			||||||
 | 
					                Ok(json!("success"))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid start_runner params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "stop_runner" => {
 | 
				
			||||||
 | 
					            if let Some(arr) = params.as_array() {
 | 
				
			||||||
 | 
					                let actor_id = arr.get(0).and_then(|v| v.as_str()).ok_or("missing actor_id")?;
 | 
				
			||||||
 | 
					                let force = arr.get(1).and_then(|v| v.as_bool()).unwrap_or(false);
 | 
				
			||||||
 | 
					                supervisor_guard.stop_runner(actor_id, force).await?;
 | 
				
			||||||
 | 
					                Ok(json!("success"))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid stop_runner params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "get_runner_status" => {
 | 
				
			||||||
 | 
					            if let Some(actor_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                let status = supervisor_guard.get_runner_status(actor_id).await?;
 | 
				
			||||||
 | 
					                Ok(json!(format!("{:?}", status)))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid get_runner_status params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "get_all_runner_status" => {
 | 
				
			||||||
 | 
					            let statuses = supervisor_guard.get_all_runner_status().await?;
 | 
				
			||||||
 | 
					            let status_map: std::collections::HashMap<String, String> = statuses
 | 
				
			||||||
 | 
					                .into_iter()
 | 
				
			||||||
 | 
					                .map(|(id, status)| (id, format!("{:?}", status)))
 | 
				
			||||||
 | 
					                .collect();
 | 
				
			||||||
 | 
					            Ok(json!(status_map))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "job.run" => {
 | 
				
			||||||
 | 
					            if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
 | 
				
			||||||
 | 
					                let secret = param_obj.get("secret")
 | 
				
			||||||
 | 
					                    .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					                    .ok_or("missing secret")?;
 | 
				
			||||||
 | 
					                let job = param_obj.get("job")
 | 
				
			||||||
 | 
					                    .ok_or("missing job")?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // For now, return success - actual job execution would need more integration
 | 
				
			||||||
 | 
					                Ok(json!("job_queued"))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid job.run params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "job.status" => {
 | 
				
			||||||
 | 
					            if let Some(job_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                // For now, return a mock status
 | 
				
			||||||
 | 
					                Ok(json!({"status": "completed"}))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid job.status params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "job.result" => {
 | 
				
			||||||
 | 
					            if let Some(job_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
 | 
				
			||||||
 | 
					                // For now, return a mock result
 | 
				
			||||||
 | 
					                Ok(json!({"success": "job completed successfully"}))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Err("invalid job.result params".into())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        "rpc.discover" => {
 | 
				
			||||||
 | 
					            let methods = vec![
 | 
				
			||||||
 | 
					                "list_runners", "register_runner", "start_runner", "stop_runner",
 | 
				
			||||||
 | 
					                "get_runner_status", "get_all_runner_status", 
 | 
				
			||||||
 | 
					                "job.run", "job.status", "job.result", "rpc.discover"
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					            Ok(json!(methods))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            error!("Unknown method: {}", method);
 | 
				
			||||||
 | 
					            Err(format!("unknown method: {}", method).into())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -13,9 +13,9 @@ use log::{debug, info, error};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::supervisor::Supervisor;
 | 
					use crate::supervisor::Supervisor;
 | 
				
			||||||
use crate::runner::{Runner, RunnerError};
 | 
					use crate::runner::{Runner, RunnerError};
 | 
				
			||||||
 | 
					use crate::runner::{ProcessManagerError, ProcessStatus, LogInfo};
 | 
				
			||||||
use crate::job::Job;
 | 
					use crate::job::Job;
 | 
				
			||||||
use crate::ProcessManagerType;
 | 
					use crate::ProcessManagerType;
 | 
				
			||||||
use sal_service_manager::{ProcessStatus, LogInfo};
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::net::SocketAddr;
 | 
					use std::net::SocketAddr;
 | 
				
			||||||
@@ -191,10 +191,12 @@ pub enum ProcessStatusWrapper {
 | 
				
			|||||||
impl From<ProcessStatus> for ProcessStatusWrapper {
 | 
					impl From<ProcessStatus> for ProcessStatusWrapper {
 | 
				
			||||||
    fn from(status: ProcessStatus) -> Self {
 | 
					    fn from(status: ProcessStatus) -> Self {
 | 
				
			||||||
        match status {
 | 
					        match status {
 | 
				
			||||||
            ProcessStatus::Running => ProcessStatusWrapper::Running,
 | 
					            ProcessStatus::NotStarted => ProcessStatusWrapper::Stopped,
 | 
				
			||||||
            ProcessStatus::Stopped => ProcessStatusWrapper::Stopped,
 | 
					 | 
				
			||||||
            ProcessStatus::Starting => ProcessStatusWrapper::Starting,
 | 
					            ProcessStatus::Starting => ProcessStatusWrapper::Starting,
 | 
				
			||||||
 | 
					            ProcessStatus::Running => ProcessStatusWrapper::Running,
 | 
				
			||||||
            ProcessStatus::Stopping => ProcessStatusWrapper::Stopping,
 | 
					            ProcessStatus::Stopping => ProcessStatusWrapper::Stopping,
 | 
				
			||||||
 | 
					            ProcessStatus::Stopped => ProcessStatusWrapper::Stopped,
 | 
				
			||||||
 | 
					            ProcessStatus::Failed => ProcessStatusWrapper::Error("Process failed".to_string()),
 | 
				
			||||||
            ProcessStatus::Error(msg) => ProcessStatusWrapper::Error(msg),
 | 
					            ProcessStatus::Error(msg) => ProcessStatusWrapper::Error(msg),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -231,16 +233,6 @@ pub struct LogInfoWrapper {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<LogInfo> for LogInfoWrapper {
 | 
					impl From<LogInfo> for LogInfoWrapper {
 | 
				
			||||||
    fn from(log: LogInfo) -> Self {
 | 
					 | 
				
			||||||
        LogInfoWrapper {
 | 
					 | 
				
			||||||
            timestamp: log.timestamp,
 | 
					 | 
				
			||||||
            level: log.level,
 | 
					 | 
				
			||||||
            message: log.message,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl From<crate::runner::LogInfo> for LogInfoWrapper {
 | 
					 | 
				
			||||||
    fn from(log: crate::runner::LogInfo) -> Self {
 | 
					    fn from(log: crate::runner::LogInfo) -> Self {
 | 
				
			||||||
        LogInfoWrapper {
 | 
					        LogInfoWrapper {
 | 
				
			||||||
            timestamp: log.timestamp,
 | 
					            timestamp: log.timestamp,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,56 @@
 | 
				
			|||||||
//! Runner implementation for actor process management.
 | 
					//! Runner implementation for actor process management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use sal_service_manager::{ProcessManagerError as ServiceProcessManagerError, ProcessStatus, ProcessConfig};
 | 
					// use sal_service_manager::{ProcessManagerError as ServiceProcessManagerError, ProcessStatus, ProcessConfig};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple process status enum to replace sal_service_manager dependency
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					pub enum ProcessStatus {
 | 
				
			||||||
 | 
					    NotStarted,
 | 
				
			||||||
 | 
					    Starting,
 | 
				
			||||||
 | 
					    Running,
 | 
				
			||||||
 | 
					    Stopping,
 | 
				
			||||||
 | 
					    Stopped,
 | 
				
			||||||
 | 
					    Failed,
 | 
				
			||||||
 | 
					    Error(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple process config to replace sal_service_manager dependency
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct ProcessConfig {
 | 
				
			||||||
 | 
					    pub command: String,
 | 
				
			||||||
 | 
					    pub args: Vec<String>,
 | 
				
			||||||
 | 
					    pub working_dir: Option<String>,
 | 
				
			||||||
 | 
					    pub env_vars: Vec<(String, String)>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ProcessConfig {
 | 
				
			||||||
 | 
					    pub fn new(command: String, args: Vec<String>, working_dir: Option<String>, env_vars: Vec<(String, String)>) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            command,
 | 
				
			||||||
 | 
					            args,
 | 
				
			||||||
 | 
					            working_dir,
 | 
				
			||||||
 | 
					            env_vars,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple process manager error to replace sal_service_manager dependency
 | 
				
			||||||
 | 
					#[derive(Debug, thiserror::Error)]
 | 
				
			||||||
 | 
					pub enum ProcessManagerError {
 | 
				
			||||||
 | 
					    #[error("Process execution failed: {0}")]
 | 
				
			||||||
 | 
					    ExecutionFailed(String),
 | 
				
			||||||
 | 
					    #[error("Process not found: {0}")]
 | 
				
			||||||
 | 
					    ProcessNotFound(String),
 | 
				
			||||||
 | 
					    #[error("IO error: {0}")]
 | 
				
			||||||
 | 
					    IoError(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Represents the current status of an actor/runner (alias for ProcessStatus)
 | 
					/// Represents the current status of an actor/runner (alias for ProcessStatus)
 | 
				
			||||||
pub type RunnerStatus = ProcessStatus;
 | 
					pub type RunnerStatus = ProcessStatus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Log information structure
 | 
					/// Log information structure with serialization support
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
pub struct LogInfo {
 | 
					pub struct LogInfo {
 | 
				
			||||||
    pub timestamp: String,
 | 
					    pub timestamp: String,
 | 
				
			||||||
    pub level: String,
 | 
					    pub level: String,
 | 
				
			||||||
@@ -96,7 +139,7 @@ pub enum RunnerError {
 | 
				
			|||||||
    #[error("Process manager error: {source}")]
 | 
					    #[error("Process manager error: {source}")]
 | 
				
			||||||
    ProcessManagerError {
 | 
					    ProcessManagerError {
 | 
				
			||||||
        #[from]
 | 
					        #[from]
 | 
				
			||||||
        source: ServiceProcessManagerError,
 | 
					        source: ProcessManagerError,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #[error("Configuration error: {reason}")]
 | 
					    #[error("Configuration error: {reason}")]
 | 
				
			||||||
@@ -120,7 +163,7 @@ pub enum RunnerError {
 | 
				
			|||||||
    #[error("Job error: {source}")]
 | 
					    #[error("Job error: {source}")]
 | 
				
			||||||
    JobError {
 | 
					    JobError {
 | 
				
			||||||
        #[from]
 | 
					        #[from]
 | 
				
			||||||
        source: crate::JobError,
 | 
					        source: hero_job::JobError,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #[error("Job '{job_id}' not found")]
 | 
					    #[error("Job '{job_id}' not found")]
 | 
				
			||||||
@@ -135,9 +178,17 @@ pub type RunnerConfig = Runner;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Convert Runner to ProcessConfig
 | 
					/// Convert Runner to ProcessConfig
 | 
				
			||||||
pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
 | 
					pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
 | 
				
			||||||
    ProcessConfig::new(config.id.clone(), config.command.clone())
 | 
					    let args = vec![
 | 
				
			||||||
        .with_arg("--id".to_string())
 | 
					        "--id".to_string(),
 | 
				
			||||||
        .with_arg(config.id.clone())
 | 
					        config.id.clone(),
 | 
				
			||||||
        .with_arg("--redis-url".to_string())
 | 
					        "--redis-url".to_string(),
 | 
				
			||||||
        .with_arg(config.redis_url.clone())
 | 
					        config.redis_url.clone(),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    ProcessConfig::new(
 | 
				
			||||||
 | 
					        config.command.to_string_lossy().to_string(),
 | 
				
			||||||
 | 
					        args,
 | 
				
			||||||
 | 
					        Some("/tmp".to_string()), // Default working directory since Runner doesn't have working_dir field
 | 
				
			||||||
 | 
					        vec![]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,85 @@
 | 
				
			|||||||
//! Main supervisor implementation for managing multiple actor runners.
 | 
					//! Main supervisor implementation for managing multiple actor runners.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
 | 
					use crate::runner::{ProcessManagerError, ProcessConfig, ProcessStatus};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple trait to replace sal_service_manager ProcessManager
 | 
				
			||||||
 | 
					trait ProcessManager: Send + Sync {
 | 
				
			||||||
 | 
					    fn start(&self, config: &ProcessConfig) -> Result<(), ProcessManagerError>;
 | 
				
			||||||
 | 
					    fn stop(&self, process_id: &str) -> Result<(), ProcessManagerError>;
 | 
				
			||||||
 | 
					    fn status(&self, process_id: &str) -> Result<ProcessStatus, ProcessManagerError>;
 | 
				
			||||||
 | 
					    fn logs(&self, process_id: &str) -> Result<Vec<String>, ProcessManagerError>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple process manager implementation
 | 
				
			||||||
 | 
					struct SimpleProcessManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SimpleProcessManager {
 | 
				
			||||||
 | 
					    fn new() -> Self {
 | 
				
			||||||
 | 
					        Self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ProcessManager for SimpleProcessManager {
 | 
				
			||||||
 | 
					    fn start(&self, _config: &ProcessConfig) -> Result<(), ProcessManagerError> {
 | 
				
			||||||
 | 
					        // Simplified implementation - just return success for now
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn stop(&self, _process_id: &str) -> Result<(), ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn status(&self, _process_id: &str) -> Result<ProcessStatus, ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(ProcessStatus::Running)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn logs(&self, _process_id: &str) -> Result<Vec<String>, ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(vec!["No logs available".to_string()])
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tmux process manager implementation
 | 
				
			||||||
 | 
					struct TmuxProcessManager {
 | 
				
			||||||
 | 
					    session_name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TmuxProcessManager {
 | 
				
			||||||
 | 
					    fn new(session_name: String) -> Self {
 | 
				
			||||||
 | 
					        Self { session_name }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ProcessManager for TmuxProcessManager {
 | 
				
			||||||
 | 
					    fn start(&self, _config: &ProcessConfig) -> Result<(), ProcessManagerError> {
 | 
				
			||||||
 | 
					        // Simplified implementation - just return success for now
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn stop(&self, _process_id: &str) -> Result<(), ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn status(&self, _process_id: &str) -> Result<ProcessStatus, ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(ProcessStatus::Running)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn logs(&self, _process_id: &str) -> Result<Vec<String>, ProcessManagerError> {
 | 
				
			||||||
 | 
					        Ok(vec!["No logs available".to_string()])
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
use tokio::sync::Mutex;
 | 
					use tokio::sync::Mutex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{client::{Client, ClientBuilder}, job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
 | 
					// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
 | 
				
			||||||
 | 
					use hero_job::{Client, client::ClientBuilder};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Process manager type for a runner
 | 
					/// Process manager type for a runner
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
pub enum ProcessManagerType {
 | 
					pub enum ProcessManagerType {
 | 
				
			||||||
    /// Simple process manager for direct process spawning
 | 
					    /// Simple process manager for direct process spawning
 | 
				
			||||||
    Simple,
 | 
					    Simple,
 | 
				
			||||||
@@ -337,7 +406,7 @@ impl Supervisor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// Delete a job by ID
 | 
					    /// Delete a job by ID
 | 
				
			||||||
    pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
 | 
					    pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
 | 
				
			||||||
        self.client.delete_job(&job_id).await
 | 
					        self.client.delete_job(&job_id).await.map_err(RunnerError::from)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// List all managed runners
 | 
					    /// List all managed runners
 | 
				
			||||||
@@ -355,7 +424,7 @@ impl Supervisor {
 | 
				
			|||||||
            
 | 
					            
 | 
				
			||||||
            let process_config = runner_to_process_config(runner);
 | 
					            let process_config = runner_to_process_config(runner);
 | 
				
			||||||
            let mut pm = self.process_manager.lock().await;
 | 
					            let mut pm = self.process_manager.lock().await;
 | 
				
			||||||
            pm.start_process(&process_config).await?;
 | 
					            pm.start(&process_config)?;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            info!("Successfully started actor {}", runner.id);
 | 
					            info!("Successfully started actor {}", runner.id);
 | 
				
			||||||
            Ok(())
 | 
					            Ok(())
 | 
				
			||||||
@@ -374,7 +443,7 @@ impl Supervisor {
 | 
				
			|||||||
            info!("Stopping actor {}", runner.id);
 | 
					            info!("Stopping actor {}", runner.id);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            let mut pm = self.process_manager.lock().await;
 | 
					            let mut pm = self.process_manager.lock().await;
 | 
				
			||||||
            pm.stop_process(&runner.id, force).await?;
 | 
					            pm.stop(&runner.id)?;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            info!("Successfully stopped actor {}", runner.id);
 | 
					            info!("Successfully stopped actor {}", runner.id);
 | 
				
			||||||
            Ok(())
 | 
					            Ok(())
 | 
				
			||||||
@@ -389,7 +458,7 @@ impl Supervisor {
 | 
				
			|||||||
    pub async fn get_runner_status(&self, actor_id: &str) -> RunnerResult<RunnerStatus> {
 | 
					    pub async fn get_runner_status(&self, actor_id: &str) -> RunnerResult<RunnerStatus> {
 | 
				
			||||||
        if let Some(runner) = self.runners.get(actor_id) {
 | 
					        if let Some(runner) = self.runners.get(actor_id) {
 | 
				
			||||||
            let pm = self.process_manager.lock().await;
 | 
					            let pm = self.process_manager.lock().await;
 | 
				
			||||||
            let status = pm.process_status(&runner.id).await?;
 | 
					            let status = pm.status(&runner.id)?;
 | 
				
			||||||
            Ok(status)
 | 
					            Ok(status)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            Err(RunnerError::ActorNotFound {
 | 
					            Err(RunnerError::ActorNotFound {
 | 
				
			||||||
@@ -407,13 +476,13 @@ impl Supervisor {
 | 
				
			|||||||
    ) -> RunnerResult<Vec<LogInfo>> {
 | 
					    ) -> RunnerResult<Vec<LogInfo>> {
 | 
				
			||||||
        if let Some(runner) = self.runners.get(actor_id) {
 | 
					        if let Some(runner) = self.runners.get(actor_id) {
 | 
				
			||||||
            let pm = self.process_manager.lock().await;
 | 
					            let pm = self.process_manager.lock().await;
 | 
				
			||||||
            let logs = pm.process_logs(&runner.id, lines, follow).await?;
 | 
					            let logs = pm.logs(&runner.id)?;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // Convert sal_service_manager::LogInfo to our LogInfo
 | 
					            // Convert strings to LogInfo
 | 
				
			||||||
            let converted_logs = logs.into_iter().map(|log| LogInfo {
 | 
					            let converted_logs = logs.into_iter().map(|log_line| LogInfo {
 | 
				
			||||||
                timestamp: log.timestamp,
 | 
					                timestamp: chrono::Utc::now().to_rfc3339(),
 | 
				
			||||||
                level: log.level,
 | 
					                level: "INFO".to_string(),
 | 
				
			||||||
                message: log.message,
 | 
					                message: log_line,
 | 
				
			||||||
            }).collect();
 | 
					            }).collect();
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            Ok(converted_logs)
 | 
					            Ok(converted_logs)
 | 
				
			||||||
@@ -521,7 +590,6 @@ impl Supervisor {
 | 
				
			|||||||
            match self.get_runner_status(actor_id).await {
 | 
					            match self.get_runner_status(actor_id).await {
 | 
				
			||||||
                Ok(status) => results.push((actor_id.clone(), status)),
 | 
					                Ok(status) => results.push((actor_id.clone(), status)),
 | 
				
			||||||
                Err(_) => {
 | 
					                Err(_) => {
 | 
				
			||||||
                    use sal_service_manager::ProcessStatus;
 | 
					 | 
				
			||||||
                    results.push((actor_id.clone(), ProcessStatus::Stopped));
 | 
					                    results.push((actor_id.clone(), ProcessStatus::Stopped));
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -651,7 +719,7 @@ impl Supervisor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// List all job IDs from Redis
 | 
					    /// List all job IDs from Redis
 | 
				
			||||||
    pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
 | 
					    pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
 | 
				
			||||||
        self.client.list_jobs().await
 | 
					        self.client.list_jobs().await.map_err(RunnerError::from)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// List all jobs with full details from Redis
 | 
					    /// List all jobs with full details from Redis
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user