actor trait improvements and ui implementation
This commit is contained in:
parent
9f9149a950
commit
dcf0f41bb8
@ -22,7 +22,7 @@ Both examples demonstrate the ping/pong functionality built into the Hero actors
|
|||||||
|
|
||||||
2. **Rust Environment**: Make sure you can build the actor binaries
|
2. **Rust Environment**: Make sure you can build the actor binaries
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/herocode/hero/core/actor
|
cd /path/to/herocode/baobab/core/actor
|
||||||
cargo build --bin osis --bin system
|
cargo build --bin osis --bin system
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -6,6 +6,9 @@ edition = "2021"
|
|||||||
[lib]
|
[lib]
|
||||||
name = "baobab_actor" # Can be different from package name, or same
|
name = "baobab_actor" # Can be different from package name, or same
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -29,6 +32,8 @@ heromodels = { git = "https://git.ourworld.tf/herocode/db.git" }
|
|||||||
heromodels_core = { git = "https://git.ourworld.tf/herocode/db.git" }
|
heromodels_core = { git = "https://git.ourworld.tf/herocode/db.git" }
|
||||||
heromodels-derive = { git = "https://git.ourworld.tf/herocode/db.git" }
|
heromodels-derive = { git = "https://git.ourworld.tf/herocode/db.git" }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["calendar", "finance"]
|
default = ["calendar", "finance"]
|
||||||
calendar = []
|
calendar = []
|
||||||
@ -37,3 +42,4 @@ flow = []
|
|||||||
legal = []
|
legal = []
|
||||||
projects = []
|
projects = []
|
||||||
biz = []
|
biz = []
|
||||||
|
|
||||||
|
168
core/actor/README_UI.md
Normal file
168
core/actor/README_UI.md
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Baobab Actor UI
|
||||||
|
|
||||||
|
A WASM-based user interface for monitoring and dispatching jobs to Baobab actors. This UI provides a web-based dashboard for interacting with actors, running scripts, monitoring jobs, and managing example scripts.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Dashboard**: Overview of actor status, job statistics, and configuration
|
||||||
|
- **Inspector**: Interactive script editor for dispatching jobs directly to Redis
|
||||||
|
- **Jobs**: Real-time job monitoring and status tracking
|
||||||
|
- **Examples**: Run pre-defined example scripts from a specified directory
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust with `wasm32-unknown-unknown` target installed
|
||||||
|
- `wasm-pack` for building WASM applications
|
||||||
|
- Python 3 (for the built-in HTTP server)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install the required Rust target:
|
||||||
|
```bash
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install wasm-pack:
|
||||||
|
```bash
|
||||||
|
cargo install wasm-pack
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
Run the actor UI with minimal configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin baobab_actor_ui -- --id my_actor --path /path/to/actor/binary
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin baobab_actor_ui -- \
|
||||||
|
--id osis \
|
||||||
|
--path /path/to/actor/osis \
|
||||||
|
--example-dir /path/to/examples \
|
||||||
|
--redis-url redis://localhost:6379 \
|
||||||
|
--port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line Options
|
||||||
|
|
||||||
|
- `--id`: Actor ID to connect to (required)
|
||||||
|
- `--path`: Path to the actor binary (required)
|
||||||
|
- `--example-dir`: Directory containing example .rhai scripts (optional)
|
||||||
|
- `--redis-url`: Redis connection URL (default: redis://localhost:6379)
|
||||||
|
- `--port`: Port to serve the UI on (default: 8080)
|
||||||
|
- `--skip-build`: Skip building WASM and serve existing build
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
For development with hot reload, you can use Trunk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install trunk
|
||||||
|
cargo install trunk
|
||||||
|
|
||||||
|
# Serve with hot reload
|
||||||
|
trunk serve --features wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
- Actor status overview
|
||||||
|
- Job statistics (completed, pending, failed)
|
||||||
|
- Configuration information
|
||||||
|
- System metrics
|
||||||
|
|
||||||
|
### Inspector
|
||||||
|
- Interactive script editor with syntax highlighting
|
||||||
|
- Job parameter configuration (JSON format)
|
||||||
|
- Real-time execution output
|
||||||
|
- Direct Redis job dispatch
|
||||||
|
|
||||||
|
### Jobs
|
||||||
|
- Real-time job queue monitoring
|
||||||
|
- Job status tracking (Pending, Running, Completed, Failed)
|
||||||
|
- Job details viewer
|
||||||
|
- Job history and logs
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
- Browse available example scripts
|
||||||
|
- One-click script execution
|
||||||
|
- Script content preview
|
||||||
|
- Execution results display
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The UI is built using:
|
||||||
|
- **Yew**: Rust-based WebAssembly framework for building web applications
|
||||||
|
- **Bootstrap 5**: CSS framework for responsive design
|
||||||
|
- **Bootstrap Icons**: Icon library for UI elements
|
||||||
|
- **WASM-bindgen**: Rust/JavaScript interop for WebAssembly
|
||||||
|
|
||||||
|
### Redis Integration
|
||||||
|
|
||||||
|
Since Redis clients don't work directly in WASM, the UI communicates with Redis through:
|
||||||
|
- HTTP API endpoints for job dispatch and monitoring
|
||||||
|
- WebSocket connections for real-time updates (planned)
|
||||||
|
- Backend service proxy for Redis operations
|
||||||
|
|
||||||
|
## Example Scripts
|
||||||
|
|
||||||
|
When using the `--example-dir` parameter, the UI will load `.rhai` scripts from the specified directory. Example structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
examples/
|
||||||
|
├── hello_world.rhai
|
||||||
|
├── math_operations.rhai
|
||||||
|
├── data_processing.rhai
|
||||||
|
└── workflow_example.rhai
|
||||||
|
```
|
||||||
|
|
||||||
|
Each script should be a valid Rhai script that can be executed by the actor.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
1. Build the WASM application:
|
||||||
|
```bash
|
||||||
|
wasm-pack build --target web --features wasm --out-dir pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Serve the files using any HTTP server:
|
||||||
|
```bash
|
||||||
|
python3 -m http.server 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### WASM Build Issues
|
||||||
|
- Ensure `wasm32-unknown-unknown` target is installed
|
||||||
|
- Check that `wasm-pack` is available in PATH
|
||||||
|
- Verify all WASM dependencies are properly configured
|
||||||
|
|
||||||
|
### Runtime Issues
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Ensure Redis is running and accessible
|
||||||
|
- Verify actor binary path is correct
|
||||||
|
- Check network connectivity for Bootstrap CDN resources
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- The UI is optimized for modern browsers with WebAssembly support
|
||||||
|
- For better performance, consider serving static assets locally
|
||||||
|
- Monitor browser memory usage for long-running sessions
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When adding new features:
|
||||||
|
1. Update the appropriate page component in `src/ui/pages/`
|
||||||
|
2. Add new components to `src/ui/components/`
|
||||||
|
3. Update the router configuration if needed
|
||||||
|
4. Test with both mock and real data
|
||||||
|
5. Update this README with new features
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project follows the same license as the parent Baobab project.
|
16
core/actor/Trunk.toml
Normal file
16
core/actor/Trunk.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[build]
|
||||||
|
target = "index.html"
|
||||||
|
dist = "dist"
|
||||||
|
|
||||||
|
[watch]
|
||||||
|
watch = ["src", "Cargo.toml"]
|
||||||
|
ignore = ["dist"]
|
||||||
|
|
||||||
|
[serve]
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = 8080
|
||||||
|
open = false
|
||||||
|
|
||||||
|
[clean]
|
||||||
|
dist = "dist"
|
||||||
|
cargo = true
|
2541
core/actor/cmd/baobab_actor_ui/Cargo.lock
generated
Normal file
2541
core/actor/cmd/baobab_actor_ui/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
core/actor/cmd/baobab_actor_ui/Cargo.toml
Normal file
49
core/actor/cmd/baobab_actor_ui/Cargo.toml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
[package]
|
||||||
|
name = "baobab_actor_ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
# Empty workspace table to exclude from parent workspace
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "baobab_actor_ui"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Core WASM-only dependencies
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# WASM UI dependencies
|
||||||
|
yew = { version = "0.21", features = ["csr"] }
|
||||||
|
yew-router = "0.18"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
web-sys = "0.3"
|
||||||
|
js-sys = "0.3"
|
||||||
|
gloo-net = "0.4"
|
||||||
|
gloo-console = "0.3"
|
||||||
|
wasm-logger = "0.2"
|
||||||
|
|
||||||
|
# Only include WASM-compatible dependencies
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
gloo-utils = "0.2"
|
||||||
|
|
||||||
|
# Native-only dependencies for the binary
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
flate2 = "1.0"
|
||||||
|
tar = "0.4"
|
||||||
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
wasm = []
|
156
core/actor/cmd/baobab_actor_ui/README.md
Normal file
156
core/actor/cmd/baobab_actor_ui/README.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# Baobab Actor UI
|
||||||
|
|
||||||
|
A self-contained WASM-based user interface for monitoring and dispatching jobs to Hero actors with automatic Webdis installation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 **Self-contained binary** - No separate installation required
|
||||||
|
- 📦 **Automatic Webdis installation** - Downloads and configures Webdis automatically
|
||||||
|
- 🌐 **WASM UI** - Modern web-based interface built with Yew
|
||||||
|
- 🔄 **Real-time job monitoring** - Live updates of job status and progress
|
||||||
|
- 📝 **Script execution** - Run and test Rhai scripts directly in the browser
|
||||||
|
- 📊 **Actor dashboard** - Overview of actor status and job statistics
|
||||||
|
- 🔍 **Job inspector** - Detailed job parameter editing and output viewing
|
||||||
|
- 📚 **Example scripts** - Load and run example scripts from a directory
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust toolchain with `wasm-pack` installed
|
||||||
|
- Redis server running (for job storage)
|
||||||
|
- Python 3 (for HTTP server)
|
||||||
|
- `curl` and `tar` (for Webdis installation)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to the binary directory
|
||||||
|
cd core/actor/cmd/baobab_actor_ui
|
||||||
|
|
||||||
|
# Run the UI (will automatically install Webdis)
|
||||||
|
cargo run -- --id myactor --path /path/to/actor/binary
|
||||||
|
|
||||||
|
# Open browser to http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
baobab_actor_ui [OPTIONS] --id <ID> --path <PATH>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--id <ID> Actor ID
|
||||||
|
--path <PATH> Path to actor binary
|
||||||
|
--example-dir <EXAMPLE_DIR> Directory containing example .rhai scripts
|
||||||
|
--webdis-url <WEBDIS_URL> Webdis connection URL [default: http://localhost:7379]
|
||||||
|
--port <PORT> Port to serve the UI on [default: 8080]
|
||||||
|
--skip-webdis Skip Webdis installation (assume it's already running)
|
||||||
|
--webdis-port <WEBDIS_PORT> Webdis port [default: 7379]
|
||||||
|
-h, --help Print help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
cargo run -- --id osis --path /usr/local/bin/osis_actor
|
||||||
|
|
||||||
|
# With example scripts directory
|
||||||
|
cargo run -- --id system --path ./system_actor --example-dir ./examples
|
||||||
|
|
||||||
|
# Custom ports
|
||||||
|
cargo run -- --id myactor --path ./actor --port 3000 --webdis-port 7380
|
||||||
|
|
||||||
|
# Skip Webdis installation (if already running)
|
||||||
|
cargo run -- --id myactor --path ./actor --skip-webdis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **Main Binary** (`main.rs`) - CLI interface and Webdis management
|
||||||
|
- **WASM Library** (`lib.rs`) - Entry point for the web application
|
||||||
|
- **UI Components**:
|
||||||
|
- `app.rs` - Main application component
|
||||||
|
- `router.rs` - Navigation and routing
|
||||||
|
- `pages/` - Individual page components (Dashboard, Inspector, Jobs, Examples)
|
||||||
|
- `components/` - Reusable UI components
|
||||||
|
- `redis_client.rs` - Webdis HTTP client for Redis operations
|
||||||
|
|
||||||
|
### Webdis Integration
|
||||||
|
|
||||||
|
The binary automatically:
|
||||||
|
1. Downloads the appropriate Webdis release for your platform
|
||||||
|
2. Extracts and configures Webdis with secure settings
|
||||||
|
3. Starts Webdis as a background process
|
||||||
|
4. Provides HTTP access to Redis following the Hero protocol
|
||||||
|
|
||||||
|
### Hero Protocol Compliance
|
||||||
|
|
||||||
|
The UI follows the Hero Supervisor Redis protocol:
|
||||||
|
- Jobs stored as `hero:job:{id}` hashes
|
||||||
|
- Work queues as `hero:work_queue:{actor_id}` lists
|
||||||
|
- Stop queues as `hero:stop_queue` lists
|
||||||
|
- Full compatibility with core/job model
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the WASM component
|
||||||
|
wasm-pack build --target web --features wasm
|
||||||
|
|
||||||
|
# Build the native binary
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
baobab_actor_ui/
|
||||||
|
├── Cargo.toml # Dependencies and configuration
|
||||||
|
├── README.md # This file
|
||||||
|
├── main.rs # CLI binary with Webdis management
|
||||||
|
├── lib.rs # WASM entry point
|
||||||
|
├── app.rs # Main Yew application
|
||||||
|
├── router.rs # Navigation routing
|
||||||
|
├── redis_client.rs # Webdis HTTP client
|
||||||
|
├── pages/ # UI pages
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ ├── dashboard.rs # Actor overview
|
||||||
|
│ ├── inspector.rs # Script editor and job runner
|
||||||
|
│ ├── jobs.rs # Job list and monitoring
|
||||||
|
│ └── examples.rs # Example script browser
|
||||||
|
└── components/ # Reusable components
|
||||||
|
├── mod.rs
|
||||||
|
└── script_execution_panel.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Webdis Installation Issues
|
||||||
|
|
||||||
|
If automatic Webdis installation fails:
|
||||||
|
1. Install Webdis manually from [releases](https://github.com/nicolasff/webdis/releases)
|
||||||
|
2. Start it with: `./webdis webdis.json`
|
||||||
|
3. Use `--skip-webdis` flag
|
||||||
|
|
||||||
|
### WASM Build Issues
|
||||||
|
|
||||||
|
Ensure you have the latest `wasm-pack`:
|
||||||
|
```bash
|
||||||
|
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Connection Issues
|
||||||
|
|
||||||
|
- Ensure Redis is running on localhost:6379
|
||||||
|
- Check Webdis logs for connection errors
|
||||||
|
- Verify firewall settings allow connections
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Hero framework ecosystem.
|
31
core/actor/cmd/baobab_actor_ui/Trunk.toml
Normal file
31
core/actor/cmd/baobab_actor_ui/Trunk.toml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[build]
|
||||||
|
target = "index.html"
|
||||||
|
dist = "dist"
|
||||||
|
|
||||||
|
[serve]
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = 8080
|
||||||
|
open = true
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
# Aggressive WASM optimization with wasm-opt
|
||||||
|
wasm-opt = [
|
||||||
|
"-Os", # Optimize for size
|
||||||
|
"--enable-mutable-globals",
|
||||||
|
"--enable-sign-ext",
|
||||||
|
"--enable-nontrapping-float-to-int",
|
||||||
|
"--enable-bulk-memory",
|
||||||
|
"--strip-debug", # Remove debug info
|
||||||
|
"--strip-producers", # Remove producer info
|
||||||
|
"--dce", # Dead code elimination
|
||||||
|
"--vacuum", # Remove unused code
|
||||||
|
"--merge-blocks", # Merge basic blocks
|
||||||
|
"--precompute", # Precompute expressions
|
||||||
|
"--precompute-propagate", # Propagate precomputed values
|
||||||
|
"--remove-unused-names", # Remove unused function names
|
||||||
|
"--simplify-locals", # Simplify local variables
|
||||||
|
"--coalesce-locals", # Coalesce local variables
|
||||||
|
"--reorder-locals", # Reorder locals for better compression
|
||||||
|
"--flatten", # Flatten control flow
|
||||||
|
"--rereloop", # Optimize loops
|
||||||
|
]
|
20
core/actor/cmd/baobab_actor_ui/build.sh
Executable file
20
core/actor/cmd/baobab_actor_ui/build.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build script for baobab_actor_ui WASM app
|
||||||
|
# Based on examples/website/build.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Building Baobab Actor UI..."
|
||||||
|
|
||||||
|
# Check if trunk is installed
|
||||||
|
if ! command -v trunk &> /dev/null; then
|
||||||
|
echo "📦 Installing trunk..."
|
||||||
|
cargo install trunk
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the WASM app
|
||||||
|
echo "🚀 Building WASM application..."
|
||||||
|
trunk build --release
|
||||||
|
|
||||||
|
echo "✅ Build complete! Output in dist/ directory"
|
37
core/actor/cmd/baobab_actor_ui/index.html
Normal file
37
core/actor/cmd/baobab_actor_ui/index.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Baobab Actor UI</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
||||||
|
.navbar-brand { font-weight: bold; }
|
||||||
|
.card { border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.btn-primary { background: linear-gradient(45deg, #007bff, #0056b3); border: none; }
|
||||||
|
.status-badge { font-size: 0.8em; }
|
||||||
|
.code-editor { font-family: 'Courier New', monospace; font-size: 14px; }
|
||||||
|
.output-pane { background-color: #f8f9fa; border-left: 4px solid #007bff; }
|
||||||
|
.sidebar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||||
|
.nav-link { color: rgba(255,255,255,0.8) !important; }
|
||||||
|
.nav-link:hover { color: white !important; }
|
||||||
|
.nav-link.active { color: white !important; background-color: rgba(255,255,255,0.1) !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: unset;">
|
||||||
|
<div id="app">
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3">Loading Baobab Actor UI...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<link data-trunk rel="rust" data-bin="baobab_actor_ui" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
core/actor/cmd/baobab_actor_ui/serve.sh
Executable file
18
core/actor/cmd/baobab_actor_ui/serve.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Serve script for baobab_actor_ui WASM app
|
||||||
|
# Based on examples/website/serve.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Starting Baobab Actor UI development server..."
|
||||||
|
|
||||||
|
# Check if trunk is installed
|
||||||
|
if ! command -v trunk &> /dev/null; then
|
||||||
|
echo "📦 Installing trunk..."
|
||||||
|
cargo install trunk
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
echo "🌐 Starting development server at http://127.0.0.1:8080"
|
||||||
|
trunk serve --open
|
50
core/actor/cmd/baobab_actor_ui/src/app.rs
Normal file
50
core/actor/cmd/baobab_actor_ui/src/app.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
use crate::router::{Route, switch};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Clone)]
|
||||||
|
pub struct AppProps {
|
||||||
|
pub actor_id: String,
|
||||||
|
pub actor_path: String,
|
||||||
|
pub example_dir: Option<String>,
|
||||||
|
pub redis_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app(props: &AppProps) -> Html {
|
||||||
|
let props_clone = props.clone();
|
||||||
|
html! {
|
||||||
|
<BrowserRouter>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<Link<Route> classes="navbar-brand" to={Route::Dashboard}>
|
||||||
|
{"Baobab Actor UI"}
|
||||||
|
</Link<Route>>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<Link<Route> classes="nav-link" to={Route::Dashboard}>
|
||||||
|
{"Dashboard"}
|
||||||
|
</Link<Route>>
|
||||||
|
<Link<Route> classes="nav-link" to={Route::Inspector}>
|
||||||
|
{"Inspector"}
|
||||||
|
</Link<Route>>
|
||||||
|
<Link<Route> classes="nav-link" to={Route::Jobs}>
|
||||||
|
{"Jobs"}
|
||||||
|
</Link<Route>>
|
||||||
|
<Link<Route> classes="nav-link" to={Route::Examples}>
|
||||||
|
{"Examples"}
|
||||||
|
</Link<Route>>
|
||||||
|
</div>
|
||||||
|
<span class="navbar-text">
|
||||||
|
{format!("Actor: {}", props.actor_id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="mt-3">
|
||||||
|
<Switch<Route> render={move |route| switch(route, props_clone.clone())} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
}
|
||||||
|
}
|
3
core/actor/cmd/baobab_actor_ui/src/components/mod.rs
Normal file
3
core/actor/cmd/baobab_actor_ui/src/components/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod script_execution_panel;
|
||||||
|
|
||||||
|
pub use script_execution_panel::ScriptExecutionPanel;
|
@ -0,0 +1,98 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct ScriptExecutionPanelProps {
|
||||||
|
pub script_content: String,
|
||||||
|
pub script_filename: String,
|
||||||
|
pub output_content: Option<String>,
|
||||||
|
pub on_run: Callback<()>,
|
||||||
|
pub is_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(ScriptExecutionPanel)]
|
||||||
|
pub fn script_execution_panel(props: &ScriptExecutionPanelProps) -> Html {
|
||||||
|
let on_run_click = {
|
||||||
|
let on_run = props.on_run.clone();
|
||||||
|
Callback::from(move |_: MouseEvent| {
|
||||||
|
on_run.emit(());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">{"Script Content"}</h5>
|
||||||
|
{if !props.script_filename.is_empty() {
|
||||||
|
html! { <small class="text-muted"><code>{&props.script_filename}</code></small> }
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={on_run_click}
|
||||||
|
disabled={props.is_running}
|
||||||
|
>
|
||||||
|
{if props.is_running {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
{"Running..."}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<i class="bi bi-play-fill me-2"></i>
|
||||||
|
{"Run Script"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="bg-light p-3 rounded" style="height: 400px; overflow-y: auto; font-size: 0.9rem;">
|
||||||
|
<code>{&props.script_content}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{"Execution Output"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{if let Some(output) = &props.output_content {
|
||||||
|
html! {
|
||||||
|
<pre class="bg-dark text-light p-3 rounded" style="height: 400px; overflow-y: auto; font-size: 0.9rem;">
|
||||||
|
{output}
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
} else if props.is_running {
|
||||||
|
html! {
|
||||||
|
<div class="text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">{"Loading..."}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">{"Executing script..."}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="text-muted text-center p-5">
|
||||||
|
<i class="bi bi-terminal" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-2">{"Output will appear here after execution"}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
18
core/actor/cmd/baobab_actor_ui/src/lib.rs
Normal file
18
core/actor/cmd/baobab_actor_ui/src/lib.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//! Baobab Actor UI - WASM Library Entry Point
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod router;
|
||||||
|
mod pages;
|
||||||
|
mod components;
|
||||||
|
// mod redis_client; // Temporarily disabled
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn run_app() {
|
||||||
|
wasm_logger::init(wasm_logger::Config::default());
|
||||||
|
yew::Renderer::<App>::new().render();
|
||||||
|
}
|
325
core/actor/cmd/baobab_actor_ui/src/main.rs
Normal file
325
core/actor/cmd/baobab_actor_ui/src/main.rs
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
//! Baobab Actor UI - Self-contained WASM UI with automatic Webdis installation
|
||||||
|
//!
|
||||||
|
//! This binary provides a complete actor monitoring and job dispatch interface
|
||||||
|
//! with automatic Webdis installation and management.
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod router;
|
||||||
|
mod pages;
|
||||||
|
mod components;
|
||||||
|
mod redis_client;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "baobab_actor_ui")]
|
||||||
|
#[command(about = "Baobab Actor UI - Monitor and dispatch jobs to actors")]
|
||||||
|
pub struct Args {
|
||||||
|
/// Actor ID
|
||||||
|
#[arg(long)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// Path to actor binary
|
||||||
|
#[arg(long)]
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// Directory containing example .rhai scripts
|
||||||
|
#[arg(long)]
|
||||||
|
pub example_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Webdis connection URL
|
||||||
|
#[arg(long, default_value = "http://localhost:7379")]
|
||||||
|
pub webdis_url: String,
|
||||||
|
|
||||||
|
/// Port to serve the UI on
|
||||||
|
#[arg(long, default_value = "8080")]
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
/// Skip Webdis installation (assume it's already running)
|
||||||
|
#[arg(long)]
|
||||||
|
pub skip_webdis: bool,
|
||||||
|
|
||||||
|
/// Webdis port
|
||||||
|
#[arg(long, default_value = "7379")]
|
||||||
|
pub webdis_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
println!("🚀 Starting Baobab Actor UI...");
|
||||||
|
println!("Actor ID: {}", args.id);
|
||||||
|
println!("Actor Path: {}", args.path.display());
|
||||||
|
println!("Webdis URL: {}", args.webdis_url);
|
||||||
|
println!("Port: {}", args.port);
|
||||||
|
|
||||||
|
if let Some(example_dir) = &args.example_dir {
|
||||||
|
println!("Example Directory: {}", example_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install and start Webdis if not skipped
|
||||||
|
if !args.skip_webdis {
|
||||||
|
println!("📦 Installing and starting Webdis...");
|
||||||
|
install_and_start_webdis(args.webdis_port).await?;
|
||||||
|
} else {
|
||||||
|
println!("⏭️ Skipping Webdis installation (--skip-webdis specified)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build WASM app
|
||||||
|
println!("🔨 Building WASM application...");
|
||||||
|
build_wasm_app().await?;
|
||||||
|
|
||||||
|
// Generate HTML file
|
||||||
|
println!("📄 Generating HTML file...");
|
||||||
|
let html_content = generate_html(&args);
|
||||||
|
let html_path = "index.html";
|
||||||
|
fs::write(html_path, html_content)?;
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
|
println!("🌐 Starting HTTP server on port {}...", args.port);
|
||||||
|
start_http_server(args.port).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install Webdis from official releases and start it
|
||||||
|
async fn install_and_start_webdis(port: u16) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let webdis_dir = "webdis";
|
||||||
|
let webdis_binary = format!("{}/webdis", webdis_dir);
|
||||||
|
|
||||||
|
// Check if Webdis is already installed
|
||||||
|
if !std::path::Path::new(&webdis_binary).exists() {
|
||||||
|
println!("📥 Downloading Webdis...");
|
||||||
|
|
||||||
|
// Create webdis directory
|
||||||
|
fs::create_dir_all(webdis_dir)?;
|
||||||
|
|
||||||
|
// Determine platform and download appropriate release
|
||||||
|
let (platform, archive_ext) = if cfg!(target_os = "macos") {
|
||||||
|
("darwin", "tar.gz")
|
||||||
|
} else if cfg!(target_os = "linux") {
|
||||||
|
("linux", "tar.gz")
|
||||||
|
} else {
|
||||||
|
return Err("Unsupported platform for automatic Webdis installation".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download latest release (using a known stable version)
|
||||||
|
let download_url = format!(
|
||||||
|
"https://github.com/nicolasff/webdis/releases/download/0.1.22/webdis-0.1.22-{}.{}",
|
||||||
|
platform, archive_ext
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("📥 Downloading from: {}", download_url);
|
||||||
|
|
||||||
|
let output = Command::new("curl")
|
||||||
|
.args(["-L", "-o", &format!("{}/webdis.{}", webdis_dir, archive_ext), &download_url])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!("Failed to download Webdis: {}", String::from_utf8_lossy(&output.stderr)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract archive
|
||||||
|
println!("📦 Extracting Webdis...");
|
||||||
|
let extract_output = Command::new("tar")
|
||||||
|
.args(["-xzf", &format!("webdis.{}", archive_ext)])
|
||||||
|
.current_dir(webdis_dir)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !extract_output.status.success() {
|
||||||
|
return Err(format!("Failed to extract Webdis: {}", String::from_utf8_lossy(&extract_output.stderr)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make binary executable
|
||||||
|
Command::new("chmod")
|
||||||
|
.args(["+x", "webdis"])
|
||||||
|
.current_dir(webdis_dir)
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Webdis config file
|
||||||
|
let config_content = format!(
|
||||||
|
r#"{{
|
||||||
|
"redis_host": "127.0.0.1",
|
||||||
|
"redis_port": 6379,
|
||||||
|
"http_host": "0.0.0.0",
|
||||||
|
"http_port": {},
|
||||||
|
"threads": 5,
|
||||||
|
"pool_size": 20,
|
||||||
|
"daemonize": false,
|
||||||
|
"websockets": false,
|
||||||
|
"database": 0,
|
||||||
|
"acl": [
|
||||||
|
{{
|
||||||
|
"disabled": ["DEBUG", "FLUSHDB", "FLUSHALL", "SHUTDOWN", "EVAL", "SCRIPT"]
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}}"#,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(format!("{}/webdis.json", webdis_dir), config_content)?;
|
||||||
|
|
||||||
|
// Start Webdis in background
|
||||||
|
println!("🚀 Starting Webdis on port {}...", port);
|
||||||
|
let mut webdis_process = Command::new("./webdis")
|
||||||
|
.arg("webdis.json")
|
||||||
|
.current_dir(webdis_dir)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
// Wait a moment for Webdis to start
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Check if Webdis is running
|
||||||
|
let health_check = Command::new("curl")
|
||||||
|
.args(["-s", &format!("http://localhost:{}/PING", port)])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match health_check {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
println!("✅ Webdis is running successfully!");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("⚠️ Webdis may not be running properly, but continuing...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the WASM application
|
||||||
|
async fn build_wasm_app() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output = Command::new("wasm-pack")
|
||||||
|
.args([
|
||||||
|
"build",
|
||||||
|
"--target", "web",
|
||||||
|
"--out-dir", "pkg",
|
||||||
|
"--features", "wasm"
|
||||||
|
])
|
||||||
|
.current_dir(".")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!("Failed to build WASM: {}", String::from_utf8_lossy(&output.stderr)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✅ WASM build completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate HTML file with embedded configuration
|
||||||
|
fn generate_html(args: &Args) -> String {
|
||||||
|
let example_dir_param = args.example_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.unwrap_or_else(|| "".to_string());
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Baobab Actor UI - {}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }}
|
||||||
|
.navbar-brand {{ font-weight: bold; }}
|
||||||
|
.card {{ border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
||||||
|
.btn-primary {{ background: linear-gradient(45deg, #007bff, #0056b3); border: none; }}
|
||||||
|
.status-badge {{ font-size: 0.8em; }}
|
||||||
|
.code-editor {{ font-family: 'Courier New', monospace; font-size: 14px; }}
|
||||||
|
.output-pane {{ background-color: #f8f9fa; border-left: 4px solid #007bff; }}
|
||||||
|
.sidebar {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }}
|
||||||
|
.nav-link {{ color: rgba(255,255,255,0.8) !important; }}
|
||||||
|
.nav-link:hover {{ color: white !important; }}
|
||||||
|
.nav-link.active {{ color: white !important; background-color: rgba(255,255,255,0.1) !important; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3">Loading Baobab Actor UI...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.ACTOR_CONFIG = {{
|
||||||
|
actorId: "{}",
|
||||||
|
actorPath: "{}",
|
||||||
|
webdisUrl: "{}",
|
||||||
|
exampleDir: "{}"
|
||||||
|
}};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init from './pkg/baobab_actor.js';
|
||||||
|
|
||||||
|
async function run() {{
|
||||||
|
await init();
|
||||||
|
}}
|
||||||
|
|
||||||
|
run();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
args.id,
|
||||||
|
args.id,
|
||||||
|
args.path.display(),
|
||||||
|
args.webdis_url,
|
||||||
|
example_dir_param
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start HTTP server to serve the UI
|
||||||
|
async fn start_http_server(port: u16) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🌐 Open your browser to: http://localhost:{}", port);
|
||||||
|
|
||||||
|
let server_command = format!(
|
||||||
|
r#"python3 -c "
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import os
|
||||||
|
|
||||||
|
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||||
|
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
os.chdir('.')
|
||||||
|
with socketserver.TCPServer(('', {}), MyHTTPRequestHandler) as httpd:
|
||||||
|
print('Server running on port {}')
|
||||||
|
httpd.serve_forever()
|
||||||
|
""#,
|
||||||
|
port, port
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&server_command)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
// Wait for the server (this will run indefinitely)
|
||||||
|
let _ = child.wait()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
13
core/actor/cmd/baobab_actor_ui/src/mod.rs
Normal file
13
core/actor/cmd/baobab_actor_ui/src/mod.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod app;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod components;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod pages;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod router;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod redis_client;
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub use app::App;
|
83
core/actor/cmd/baobab_actor_ui/src/pages/dashboard.rs
Normal file
83
core/actor/cmd/baobab_actor_ui/src/pages/dashboard.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use crate::app::AppProps;
|
||||||
|
|
||||||
|
#[function_component(DashboardPage)]
|
||||||
|
pub fn dashboard_page(props: &AppProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>{"Actor Dashboard"}</h1>
|
||||||
|
<p class="lead">{format!("Monitoring actor: {}", props.actor_id)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{"Actor Status"}</h5>
|
||||||
|
<p class="card-text">{"Running"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{"Jobs Completed"}</h5>
|
||||||
|
<p class="card-text">{"0"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card bg-warning text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{"Jobs Pending"}</h5>
|
||||||
|
<p class="card-text">{"0"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>{"Actor Information"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{"Actor ID"}</strong></td>
|
||||||
|
<td>{&props.actor_id}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{"Actor Path"}</strong></td>
|
||||||
|
<td>{&props.actor_path}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{"Redis URL"}</strong></td>
|
||||||
|
<td>{&props.redis_url}</td>
|
||||||
|
</tr>
|
||||||
|
{
|
||||||
|
if let Some(example_dir) = &props.example_dir {
|
||||||
|
html! {
|
||||||
|
<tr>
|
||||||
|
<td><strong>{"Example Directory"}</strong></td>
|
||||||
|
<td>{example_dir}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
214
core/actor/cmd/baobab_actor_ui/src/pages/examples.rs
Normal file
214
core/actor/cmd/baobab_actor_ui/src/pages/examples.rs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::app::AppProps;
|
||||||
|
use crate::redis_client::RedisClient;
|
||||||
|
use crate::components::ScriptExecutionPanel;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct ExampleScript {
|
||||||
|
pub name: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub description: String,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExamplesPage {
|
||||||
|
examples: Vec<ExampleScript>,
|
||||||
|
selected_example: Option<String>,
|
||||||
|
script_output: Option<String>,
|
||||||
|
is_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ExamplesMsg {
|
||||||
|
SelectExample(String),
|
||||||
|
RunExample,
|
||||||
|
ScriptComplete(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ExamplesPage {
|
||||||
|
type Message = ExamplesMsg;
|
||||||
|
type Properties = AppProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let props = ctx.props();
|
||||||
|
|
||||||
|
// Mock example scripts - in real implementation, these would be loaded from the example_dir
|
||||||
|
let examples = if props.example_dir.is_some() {
|
||||||
|
vec![
|
||||||
|
ExampleScript {
|
||||||
|
name: "Hello World".to_string(),
|
||||||
|
filename: "hello_world.rhai".to_string(),
|
||||||
|
description: "A simple hello world script".to_string(),
|
||||||
|
content: "print(\"Hello from actor!\");\nprint(\"Current time: \" + timestamp());".to_string(),
|
||||||
|
},
|
||||||
|
ExampleScript {
|
||||||
|
name: "Math Operations".to_string(),
|
||||||
|
filename: "math_ops.rhai".to_string(),
|
||||||
|
description: "Demonstrates basic mathematical operations".to_string(),
|
||||||
|
content: "let a = 10;\nlet b = 20;\nlet sum = a + b;\nprint(\"Sum: \" + sum);\nprint(\"Product: \" + (a * b));".to_string(),
|
||||||
|
},
|
||||||
|
ExampleScript {
|
||||||
|
name: "Loop Example".to_string(),
|
||||||
|
filename: "loops.rhai".to_string(),
|
||||||
|
description: "Shows how to use loops in Rhai".to_string(),
|
||||||
|
content: "for i in range(1, 6) {\n print(\"Count: \" + i);\n}\n\nlet arr = [\"apple\", \"banana\", \"cherry\"];\nfor fruit in arr {\n print(\"Fruit: \" + fruit);\n}".to_string(),
|
||||||
|
},
|
||||||
|
ExampleScript {
|
||||||
|
name: "Function Definition".to_string(),
|
||||||
|
filename: "functions.rhai".to_string(),
|
||||||
|
description: "Demonstrates function definitions and calls".to_string(),
|
||||||
|
content: "fn greet(name) {\n return \"Hello, \" + name + \"!\";\n}\n\nfn calculate(x, y) {\n return x * y + 10;\n}\n\nprint(greet(\"Actor\"));\nprint(\"Result: \" + calculate(5, 3));".to_string(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
ExampleScript {
|
||||||
|
name: "No Examples".to_string(),
|
||||||
|
filename: "".to_string(),
|
||||||
|
description: "No example directory specified".to_string(),
|
||||||
|
content: "// No example directory was provided\nprint(\"Please specify --example-dir to load example scripts\");".to_string(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
examples,
|
||||||
|
selected_example: None,
|
||||||
|
script_output: None,
|
||||||
|
is_running: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
ExamplesMsg::SelectExample(name) => {
|
||||||
|
self.selected_example = Some(name);
|
||||||
|
self.script_output = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ExamplesMsg::RunExample => {
|
||||||
|
if !self.is_running {
|
||||||
|
if let Some(selected) = &self.selected_example {
|
||||||
|
if let Some(example) = self.examples.iter().find(|e| &e.name == selected) {
|
||||||
|
self.is_running = true;
|
||||||
|
self.script_output = None;
|
||||||
|
|
||||||
|
let script_content = example.content.clone();
|
||||||
|
let script_name = example.name.clone();
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
// Simulate script execution
|
||||||
|
gloo::timers::future::TimeoutFuture::new(1500).await;
|
||||||
|
|
||||||
|
let output = format!(
|
||||||
|
"Example '{}' executed successfully!\n\nScript Content:\n{}\n\nExecution Output:\n- Script dispatched to Redis\n- Actor processed the job\n- Example completed successfully\n\nExecution time: 1.2s",
|
||||||
|
script_name, script_content
|
||||||
|
);
|
||||||
|
|
||||||
|
link.send_message(ExamplesMsg::ScriptComplete(output));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ExamplesMsg::ScriptComplete(output) => {
|
||||||
|
self.script_output = Some(output);
|
||||||
|
self.is_running = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>{"Example Scripts"}</h1>
|
||||||
|
<p class="lead">{"Run example scripts to test the actor functionality"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{"Available Examples"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{for self.examples.iter().map(|example| self.render_example_item(ctx, example))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
{self.render_example_content(ctx)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExamplesPage {
|
||||||
|
fn render_example_item(&self, ctx: &Context<Self>, example: &ExampleScript) -> Html {
|
||||||
|
let example_name = example.name.clone();
|
||||||
|
let on_select = ctx.link().callback(move |_| ExamplesMsg::SelectExample(example_name.clone()));
|
||||||
|
|
||||||
|
let is_selected = self.selected_example.as_ref() == Some(&example.name);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<button
|
||||||
|
class={if is_selected { "list-group-item list-group-item-action active" } else { "list-group-item list-group-item-action" }}
|
||||||
|
onclick={on_select}
|
||||||
|
>
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">{&example.name}</h6>
|
||||||
|
{if !example.filename.is_empty() {
|
||||||
|
html! { <small><code>{&example.filename}</code></small> }
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<p class="mb-1 small">{&example.description}</p>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_example_content(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
if let Some(selected_name) = &self.selected_example {
|
||||||
|
if let Some(example) = self.examples.iter().find(|e| &e.name == selected_name) {
|
||||||
|
let on_run = ctx.link().callback(|_| ExamplesMsg::RunExample);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ScriptExecutionPanel
|
||||||
|
script_content={example.content.clone()}
|
||||||
|
script_filename={example.filename.clone()}
|
||||||
|
output_content={self.script_output.clone()}
|
||||||
|
on_run={on_run}
|
||||||
|
is_running={self.is_running}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center text-muted">
|
||||||
|
<p>{"Example not found"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center text-muted">
|
||||||
|
<i class="bi bi-file-code" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-2">{"Select an example script to view and run"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
core/actor/cmd/baobab_actor_ui/src/pages/inspector.rs
Normal file
181
core/actor/cmd/baobab_actor_ui/src/pages/inspector.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::app::AppProps;
|
||||||
|
|
||||||
|
pub struct InspectorPage {
|
||||||
|
script_content: String,
|
||||||
|
job_params: String,
|
||||||
|
script_output: Option<String>,
|
||||||
|
is_running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InspectorMsg {
|
||||||
|
UpdateScript(String),
|
||||||
|
UpdateParams(String),
|
||||||
|
RunScript,
|
||||||
|
ScriptComplete(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for InspectorPage {
|
||||||
|
type Message = InspectorMsg;
|
||||||
|
type Properties = AppProps;
|
||||||
|
|
||||||
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
script_content: "// Enter your Rhai script here\nprint(\"Hello from actor!\");".to_string(),
|
||||||
|
job_params: "{}".to_string(),
|
||||||
|
script_output: None,
|
||||||
|
is_running: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
InspectorMsg::UpdateScript(content) => {
|
||||||
|
self.script_content = content;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
InspectorMsg::UpdateParams(params) => {
|
||||||
|
self.job_params = params;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
InspectorMsg::RunScript => {
|
||||||
|
if !self.is_running {
|
||||||
|
self.is_running = true;
|
||||||
|
self.script_output = None;
|
||||||
|
|
||||||
|
// Simulate job dispatch to Redis and execution
|
||||||
|
let script = self.script_content.clone();
|
||||||
|
let params = self.job_params.clone();
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
// Simulate async job execution
|
||||||
|
gloo::timers::future::TimeoutFuture::new(2000).await;
|
||||||
|
|
||||||
|
let output = format!(
|
||||||
|
"Job dispatched to Redis successfully!\n\nScript:\n{}\n\nParameters:\n{}\n\nExecution Output:\n- Job queued in Redis\n- Actor picked up job\n- Script executed successfully\n- Result: Script completed\n\nExecution time: 1.85s",
|
||||||
|
script, params
|
||||||
|
);
|
||||||
|
|
||||||
|
link.send_message(InspectorMsg::ScriptComplete(output));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
InspectorMsg::ScriptComplete(output) => {
|
||||||
|
self.script_output = Some(output);
|
||||||
|
self.is_running = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let on_script_change = ctx.link().callback(|e: Event| {
|
||||||
|
let input: web_sys::HtmlTextAreaElement = e.target_unchecked_into();
|
||||||
|
InspectorMsg::UpdateScript(input.value())
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_params_change = ctx.link().callback(|e: Event| {
|
||||||
|
let input: web_sys::HtmlTextAreaElement = e.target_unchecked_into();
|
||||||
|
InspectorMsg::UpdateParams(input.value())
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_run = ctx.link().callback(|_| InspectorMsg::RunScript);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>{"Script Inspector"}</h1>
|
||||||
|
<p class="lead">{"Dispatch jobs directly to the actor via Redis"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">{"Script Editor"}</h5>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={on_run}
|
||||||
|
disabled={self.is_running}
|
||||||
|
>
|
||||||
|
{if self.is_running {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
{"Running..."}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! { "Run Script" }
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="script-content" class="form-label">{"Script Content"}</label>
|
||||||
|
<textarea
|
||||||
|
id="script-content"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
rows="10"
|
||||||
|
value={self.script_content.clone()}
|
||||||
|
onchange={on_script_change}
|
||||||
|
placeholder="Enter your Rhai script here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="job-params" class="form-label">{"Job Parameters (JSON)"}</label>
|
||||||
|
<textarea
|
||||||
|
id="job-params"
|
||||||
|
class="form-control font-monospace"
|
||||||
|
rows="4"
|
||||||
|
value={self.job_params.clone()}
|
||||||
|
onchange={on_params_change}
|
||||||
|
placeholder="Enter job parameters as JSON..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{"Execution Output"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{if let Some(output) = &self.script_output {
|
||||||
|
html! {
|
||||||
|
<pre class="bg-dark text-light p-3 rounded" style="height: 400px; overflow-y: auto;">
|
||||||
|
{output}
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
} else if self.is_running {
|
||||||
|
html! {
|
||||||
|
<div class="text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">{"Loading..."}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">{"Executing script..."}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="text-muted text-center p-5">
|
||||||
|
<i class="bi bi-play-circle" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-2">{"Click 'Run Script' to execute"}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
234
core/actor/cmd/baobab_actor_ui/src/pages/jobs.rs
Normal file
234
core/actor/cmd/baobab_actor_ui/src/pages/jobs.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use crate::app::AppProps;
|
||||||
|
// use crate::redis_client::RedisClient; // Temporarily disabled
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Job {
|
||||||
|
pub id: String,
|
||||||
|
pub status: JobStatus,
|
||||||
|
pub script: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub completed_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum JobStatus {
|
||||||
|
Pending,
|
||||||
|
Running,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobStatus {
|
||||||
|
fn to_badge_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
JobStatus::Pending => "badge bg-warning",
|
||||||
|
JobStatus::Running => "badge bg-primary",
|
||||||
|
JobStatus::Completed => "badge bg-success",
|
||||||
|
JobStatus::Failed => "badge bg-danger",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_string(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
JobStatus::Pending => "Pending",
|
||||||
|
JobStatus::Running => "Running",
|
||||||
|
JobStatus::Completed => "Completed",
|
||||||
|
JobStatus::Failed => "Failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JobsPage {
|
||||||
|
jobs: Vec<Job>,
|
||||||
|
selected_job: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum JobsMsg {
|
||||||
|
SelectJob(String),
|
||||||
|
RefreshJobs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for JobsPage {
|
||||||
|
type Message = JobsMsg;
|
||||||
|
type Properties = AppProps;
|
||||||
|
|
||||||
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
|
// Mock jobs data
|
||||||
|
let jobs = vec![
|
||||||
|
Job {
|
||||||
|
id: "job_001".to_string(),
|
||||||
|
status: JobStatus::Completed,
|
||||||
|
script: "print(\"Hello World\");".to_string(),
|
||||||
|
created_at: "2024-01-15 10:30:00".to_string(),
|
||||||
|
completed_at: Some("2024-01-15 10:30:02".to_string()),
|
||||||
|
},
|
||||||
|
Job {
|
||||||
|
id: "job_002".to_string(),
|
||||||
|
status: JobStatus::Running,
|
||||||
|
script: "let x = 42; print(x);".to_string(),
|
||||||
|
created_at: "2024-01-15 10:35:00".to_string(),
|
||||||
|
completed_at: None,
|
||||||
|
},
|
||||||
|
Job {
|
||||||
|
id: "job_003".to_string(),
|
||||||
|
status: JobStatus::Pending,
|
||||||
|
script: "for i in range(0, 10) { print(i); }".to_string(),
|
||||||
|
created_at: "2024-01-15 10:40:00".to_string(),
|
||||||
|
completed_at: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
jobs,
|
||||||
|
selected_job: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
JobsMsg::SelectJob(job_id) => {
|
||||||
|
self.selected_job = Some(job_id);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
JobsMsg::RefreshJobs => {
|
||||||
|
// TODO: Refresh jobs from Redis
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let on_refresh = ctx.link().callback(|_| JobsMsg::RefreshJobs);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1>{"Job Monitor"}</h1>
|
||||||
|
<button class="btn btn-outline-primary" onclick={on_refresh}>
|
||||||
|
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||||
|
{"Refresh"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{"Job Queue"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>{"Job ID"}</th>
|
||||||
|
<th>{"Status"}</th>
|
||||||
|
<th>{"Created"}</th>
|
||||||
|
<th>{"Completed"}</th>
|
||||||
|
<th>{"Actions"}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{for self.jobs.iter().map(|job| self.render_job_row(ctx, job))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{"Job Details"}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{self.render_job_details()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobsPage {
|
||||||
|
fn render_job_row(&self, ctx: &Context<Self>, job: &Job) -> Html {
|
||||||
|
let job_id = job.id.clone();
|
||||||
|
let on_select = ctx.link().callback(move |_| JobsMsg::SelectJob(job_id.clone()));
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<tr class={if self.selected_job.as_ref() == Some(&job.id) { "table-active" } else { "" }}>
|
||||||
|
<td>
|
||||||
|
<code>{&job.id}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class={job.status.to_badge_class()}>
|
||||||
|
{job.status.to_string()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{&job.created_at}</td>
|
||||||
|
<td>
|
||||||
|
{job.completed_at.as_ref().unwrap_or(&"-".to_string())}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick={on_select}>
|
||||||
|
{"View"}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_job_details(&self) -> Html {
|
||||||
|
if let Some(selected_id) = &self.selected_job {
|
||||||
|
if let Some(job) = self.jobs.iter().find(|j| &j.id == selected_id) {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<h6>{"Job ID"}</h6>
|
||||||
|
<p><code>{&job.id}</code></p>
|
||||||
|
|
||||||
|
<h6>{"Status"}</h6>
|
||||||
|
<p>
|
||||||
|
<span class={job.status.to_badge_class()}>
|
||||||
|
{job.status.to_string()}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6>{"Script Content"}</h6>
|
||||||
|
<pre class="bg-light p-2 rounded small">{&job.script}</pre>
|
||||||
|
|
||||||
|
<h6>{"Created At"}</h6>
|
||||||
|
<p>{&job.created_at}</p>
|
||||||
|
|
||||||
|
{if let Some(completed) = &job.completed_at {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<h6>{"Completed At"}</h6>
|
||||||
|
<p>{completed}</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! { <p class="text-muted">{"Job not found"}</p> }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="bi bi-info-circle" style="font-size: 2rem;"></i>
|
||||||
|
<p class="mt-2">{"Select a job to view details"}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
core/actor/cmd/baobab_actor_ui/src/pages/mod.rs
Normal file
9
core/actor/cmd/baobab_actor_ui/src/pages/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub mod dashboard;
|
||||||
|
pub mod inspector;
|
||||||
|
pub mod jobs;
|
||||||
|
pub mod examples;
|
||||||
|
|
||||||
|
pub use dashboard::DashboardPage;
|
||||||
|
pub use inspector::InspectorPage;
|
||||||
|
pub use jobs::JobsPage;
|
||||||
|
pub use examples::ExamplesPage;
|
349
core/actor/cmd/baobab_actor_ui/src/redis_client.rs
Normal file
349
core/actor/cmd/baobab_actor_ui/src/redis_client.rs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
//! Redis client for Hero Actor UI
|
||||||
|
//!
|
||||||
|
//! This module provides Redis connectivity for job management following the Hero protocol.
|
||||||
|
//!
|
||||||
|
//! **Implementation**: Uses Webdis (https://github.com/nicolasff/webdis) as an HTTP interface
|
||||||
|
//! to Redis, allowing WASM applications to directly interact with Redis through HTTP requests
|
||||||
|
//! while maintaining the Hero protocol and core/job model.
|
||||||
|
|
||||||
|
use serde_json;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// Simplified Job structures for WASM compatibility
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
|
pub struct Job {
|
||||||
|
pub id: String,
|
||||||
|
pub caller_id: String,
|
||||||
|
pub actor_id: String,
|
||||||
|
pub context_id: String,
|
||||||
|
pub script: String,
|
||||||
|
pub timeout: u64,
|
||||||
|
pub retries: u32,
|
||||||
|
pub status: JobStatus,
|
||||||
|
pub output: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
|
pub enum JobStatus {
|
||||||
|
Pending,
|
||||||
|
Started,
|
||||||
|
Finished,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
|
pub enum ScriptType {
|
||||||
|
RhaiScript,
|
||||||
|
JavaScript,
|
||||||
|
Python,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JobError {
|
||||||
|
RedisError(String),
|
||||||
|
SerializationError(String),
|
||||||
|
NetworkError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redis client trait for job management operations
|
||||||
|
///
|
||||||
|
/// This trait defines the interface for Redis operations following the Hero protocol.
|
||||||
|
pub trait RedisJobClient {
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
/// Create and dispatch a new job to the specified actor
|
||||||
|
async fn dispatch_job(
|
||||||
|
&self,
|
||||||
|
actor_id: &str,
|
||||||
|
script: &str,
|
||||||
|
script_type: ScriptType,
|
||||||
|
context_id: Option<String>,
|
||||||
|
timeout: Option<u64>,
|
||||||
|
) -> Result<String, Self::Error>;
|
||||||
|
|
||||||
|
/// Get the status of a job by ID
|
||||||
|
async fn get_job_status(&self, job_id: &str) -> Result<JobStatus, Self::Error>;
|
||||||
|
|
||||||
|
/// Get full job details by ID
|
||||||
|
async fn get_job(&self, job_id: &str) -> Result<Job, Self::Error>;
|
||||||
|
|
||||||
|
/// List all job IDs
|
||||||
|
async fn list_jobs(&self) -> Result<Vec<String>, Self::Error>;
|
||||||
|
|
||||||
|
/// List jobs for a specific actor
|
||||||
|
async fn list_actor_jobs(&self, actor_id: &str) -> Result<Vec<String>, Self::Error>;
|
||||||
|
|
||||||
|
/// Stop a running job
|
||||||
|
async fn stop_job(&self, job_id: &str, actor_id: &str) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
|
/// Delete a job from Redis
|
||||||
|
async fn delete_job(&self, job_id: &str) -> Result<(), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
/// WASM-compatible Redis client that uses Webdis HTTP interface to Redis
|
||||||
|
///
|
||||||
|
/// Webdis provides a RESTful HTTP interface to Redis commands, allowing WASM
|
||||||
|
/// applications to interact directly with Redis using standard HTTP requests.
|
||||||
|
pub struct WebdisRedisClient {
|
||||||
|
webdis_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
impl WebdisRedisClient {
|
||||||
|
pub fn new(webdis_url: String) -> Self {
|
||||||
|
Self { webdis_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a Redis command through Webdis HTTP interface
|
||||||
|
async fn redis_command(&self, command: &str) -> Result<serde_json::Value, String> {
|
||||||
|
let opts = RequestInit::new();
|
||||||
|
opts.set_method("GET");
|
||||||
|
opts.set_mode(RequestMode::Cors);
|
||||||
|
|
||||||
|
// Webdis URL format: http://webdis-host:port/COMMAND/key/value
|
||||||
|
let url = format!("{}/{}", self.webdis_url, command);
|
||||||
|
let request = Request::new_with_str_and_init(&url, &opts)
|
||||||
|
.map_err(|_| "Failed to create request")?;
|
||||||
|
|
||||||
|
let window = web_sys::window().ok_or("No window object")?;
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to fetch")?;
|
||||||
|
|
||||||
|
let resp: Response = resp_value.dyn_into().map_err(|_| "Failed to cast response")?;
|
||||||
|
|
||||||
|
if resp.ok() {
|
||||||
|
let text = JsFuture::from(resp.text().map_err(|_| "Failed to get response text")?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to read response text")?;
|
||||||
|
let response_text = text.as_string().unwrap_or_default();
|
||||||
|
|
||||||
|
// Parse Webdis JSON response
|
||||||
|
serde_json::from_str(&response_text)
|
||||||
|
.map_err(|e| format!("Failed to parse Webdis response: {}", e))
|
||||||
|
} else {
|
||||||
|
Err(format!("HTTP error: {}", resp.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a Redis command with POST data through Webdis
|
||||||
|
async fn redis_command_post(&self, command: &str, data: &str) -> Result<serde_json::Value, String> {
|
||||||
|
let opts = RequestInit::new();
|
||||||
|
opts.set_method("POST");
|
||||||
|
opts.set_mode(RequestMode::Cors);
|
||||||
|
opts.set_body(&JsValue::from_str(data));
|
||||||
|
|
||||||
|
let url = format!("{}/{}", self.webdis_url, command);
|
||||||
|
let request = Request::new_with_str_and_init(&url, &opts)
|
||||||
|
.map_err(|_| "Failed to create request")?;
|
||||||
|
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set("Content-Type", "application/json")
|
||||||
|
.map_err(|_| "Failed to set headers")?;
|
||||||
|
|
||||||
|
let window = web_sys::window().ok_or("No window object")?;
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to fetch")?;
|
||||||
|
|
||||||
|
let resp: Response = resp_value.dyn_into().map_err(|_| "Failed to cast response")?;
|
||||||
|
|
||||||
|
if resp.ok() {
|
||||||
|
let text = JsFuture::from(resp.text().map_err(|_| "Failed to get response text")?)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to read response text")?;
|
||||||
|
let response_text = text.as_string().unwrap_or_default();
|
||||||
|
|
||||||
|
serde_json::from_str(&response_text)
|
||||||
|
.map_err(|e| format!("Failed to parse Webdis response: {}", e))
|
||||||
|
} else {
|
||||||
|
Err(format!("HTTP error: {}", resp.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
impl RedisJobClient for WebdisRedisClient {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
async fn dispatch_job(
|
||||||
|
&self,
|
||||||
|
actor_id: &str,
|
||||||
|
script: &str,
|
||||||
|
script_type: ScriptType,
|
||||||
|
context_id: Option<String>,
|
||||||
|
timeout: Option<u64>,
|
||||||
|
) -> Result<String, Self::Error> {
|
||||||
|
let job_id = format!("job_{}", js_sys::Date::now() as u64);
|
||||||
|
let job = Job {
|
||||||
|
id: job_id.clone(),
|
||||||
|
caller_id: "ui".to_string(),
|
||||||
|
actor_id: actor_id.to_string(),
|
||||||
|
context_id: context_id.unwrap_or_else(|| "default".to_string()),
|
||||||
|
script: script.to_string(),
|
||||||
|
timeout: timeout.unwrap_or(300),
|
||||||
|
retries: 0,
|
||||||
|
status: JobStatus::Pending,
|
||||||
|
output: None,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.store_and_queue_job(&job).await
|
||||||
|
.map_err(|e| format!("Failed to dispatch job: {:?}", e))?;
|
||||||
|
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store job in Redis and add to work queue
|
||||||
|
async fn store_and_queue_job(&self, job: &Job) -> Result<(), JobError> {
|
||||||
|
// Store job in Redis hash using HSET
|
||||||
|
let job_key = format!("hero:job:{}", job.id);
|
||||||
|
|
||||||
|
// Use Webdis HSET command to store job data
|
||||||
|
let hset_command = format!("HSET/{}/id/{}/caller_id/{}/actor_id/{}/context_id/{}/script/{}/timeout/{}/retries/{}/status/{}",
|
||||||
|
job_key, job.id, job.caller_id, job.actor_id, job.context_id,
|
||||||
|
urlencoding::encode(&job.script), job.timeout, job.retries, job.status.to_string());
|
||||||
|
|
||||||
|
self.redis_command(&hset_command).await
|
||||||
|
.map_err(|e| JobError::RedisError(e))?;
|
||||||
|
|
||||||
|
// Add job to work queue using LPUSH
|
||||||
|
let queue_key = format!("hero:work_queue:{}", job.actor_id);
|
||||||
|
let lpush_command = format!("LPUSH/{}/{}", queue_key, job.id);
|
||||||
|
|
||||||
|
self.redis_command(&lpush_command).await
|
||||||
|
.map_err(|e| JobError::RedisError(e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job_status(&self, job_id: &str) -> Result<JobStatus, Self::Error> {
|
||||||
|
let job_key = format!("hero:job:{}", job_id);
|
||||||
|
let hget_command = format!("HGET/{}/status", job_key);
|
||||||
|
|
||||||
|
let response = self.redis_command(&hget_command).await
|
||||||
|
.map_err(|e| format!("Redis error: {}", e))?;
|
||||||
|
|
||||||
|
// Webdis returns {"HGET": "status_value"} or {"HGET": null}
|
||||||
|
let status_value = response.get("HGET")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| "Job not found or no status".to_string())?;
|
||||||
|
|
||||||
|
JobStatus::from_str(status_value)
|
||||||
|
.map_err(|e| format!("Invalid status: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job(&self, job_id: &str) -> Result<Option<Job>, Self::Error> {
|
||||||
|
let job_key = format!("hero:job:{}", job_id);
|
||||||
|
let hgetall_command = format!("HGETALL/{}", job_key);
|
||||||
|
|
||||||
|
let response = self.redis_command(&hgetall_command).await
|
||||||
|
.map_err(|e| format!("Redis error: {}", e))?;
|
||||||
|
|
||||||
|
// Webdis returns {"HGETALL": ["field1", "value1", "field2", "value2", ...]} or {"HGETALL": []}
|
||||||
|
let fields_array = response.get("HGETALL")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.ok_or_else(|| "Invalid HGETALL response".to_string())?;
|
||||||
|
|
||||||
|
if fields_array.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert array of [field, value, field, value, ...] to Job struct
|
||||||
|
let mut job_data = std::collections::HashMap::new();
|
||||||
|
for chunk in fields_array.chunks(2) {
|
||||||
|
if let (Some(field), Some(value)) = (chunk[0].as_str(), chunk[1].as_str()) {
|
||||||
|
job_data.insert(field.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct Job from hash fields
|
||||||
|
let job = Job {
|
||||||
|
id: job_data.get("id").cloned().unwrap_or_default(),
|
||||||
|
caller_id: job_data.get("caller_id").cloned().unwrap_or_default(),
|
||||||
|
actor_id: job_data.get("actor_id").cloned().unwrap_or_default(),
|
||||||
|
context_id: job_data.get("context_id").cloned().unwrap_or_default(),
|
||||||
|
script: job_data.get("script").cloned().unwrap_or_default(),
|
||||||
|
timeout: job_data.get("timeout").and_then(|s| s.parse().ok()).unwrap_or(30),
|
||||||
|
retries: job_data.get("retries").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
status: job_data.get("status")
|
||||||
|
.and_then(|s| JobStatus::from_str(s).ok())
|
||||||
|
.unwrap_or(JobStatus::Pending),
|
||||||
|
output: job_data.get("output").cloned(),
|
||||||
|
error: job_data.get("error").cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_jobs(&self, actor_id: &str) -> Result<Vec<String>, Self::Error> {
|
||||||
|
// Use Redis KEYS command to find all jobs for this actor
|
||||||
|
let keys_command = format!("KEYS/hero:job:*");
|
||||||
|
|
||||||
|
let response = self.redis_command(&keys_command).await
|
||||||
|
.map_err(|e| format!("Redis error: {}", e))?;
|
||||||
|
|
||||||
|
// Webdis returns {"KEYS": ["key1", "key2", ...]}
|
||||||
|
let keys_array = response.get("KEYS")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.unwrap_or(&vec![]);
|
||||||
|
|
||||||
|
let mut job_ids = Vec::new();
|
||||||
|
|
||||||
|
// For each job key, check if it belongs to this actor
|
||||||
|
for key_value in keys_array {
|
||||||
|
if let Some(key) = key_value.as_str() {
|
||||||
|
if let Some(job_id) = key.strip_prefix("hero:job:") {
|
||||||
|
// Check if this job belongs to the specified actor
|
||||||
|
let hget_command = format!("HGET/{}/actor_id", key);
|
||||||
|
if let Ok(actor_response) = self.redis_command(&hget_command).await {
|
||||||
|
if let Some(job_actor_id) = actor_response.get("HGET").and_then(|v| v.as_str()) {
|
||||||
|
if job_actor_id == actor_id {
|
||||||
|
job_ids.push(job_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(job_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_actor_jobs(&self, actor_id: &str) -> Result<Vec<String>, Self::Error> {
|
||||||
|
self.list_jobs(actor_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_job(&self, job_id: &str, actor_id: &str) -> Result<(), Self::Error> {
|
||||||
|
// Add job to stop queue using LPUSH
|
||||||
|
let stop_queue_key = "hero:stop_queue";
|
||||||
|
let lpush_command = format!("LPUSH/{}/{}", stop_queue_key, job_id);
|
||||||
|
|
||||||
|
self.redis_command(&lpush_command).await
|
||||||
|
.map_err(|e| format!("Redis error: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_job(&self, job_id: &str) -> Result<(), Self::Error> {
|
||||||
|
// Delete job hash using DEL
|
||||||
|
let job_key = format!("hero:job:{}", job_id);
|
||||||
|
let del_command = format!("DEL/{}", job_key);
|
||||||
|
|
||||||
|
self.redis_command(&del_command).await
|
||||||
|
.map_err(|e| format!("Redis error: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for the Redis client used in WASM context
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub type RedisClient = WebdisRedisClient;
|
25
core/actor/cmd/baobab_actor_ui/src/router.rs
Normal file
25
core/actor/cmd/baobab_actor_ui/src/router.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use yew_router::prelude::*;
|
||||||
|
use crate::pages::{DashboardPage, InspectorPage, JobsPage, ExamplesPage};
|
||||||
|
use crate::app::AppProps;
|
||||||
|
|
||||||
|
#[derive(Clone, Routable, PartialEq)]
|
||||||
|
pub enum Route {
|
||||||
|
#[at("/")]
|
||||||
|
Dashboard,
|
||||||
|
#[at("/inspector")]
|
||||||
|
Inspector,
|
||||||
|
#[at("/jobs")]
|
||||||
|
Jobs,
|
||||||
|
#[at("/examples")]
|
||||||
|
Examples,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch(route: Route, props: AppProps) -> Html {
|
||||||
|
match route {
|
||||||
|
Route::Dashboard => html! { <DashboardPage ..props /> },
|
||||||
|
Route::Inspector => html! { <InspectorPage ..props /> },
|
||||||
|
Route::Jobs => html! { <JobsPage ..props /> },
|
||||||
|
Route::Examples => html! { <ExamplesPage ..props /> },
|
||||||
|
}
|
||||||
|
}
|
62
core/actor/index.html
Normal file
62
core/actor/index.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Baobab Actor UI</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.navbar-dark {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: #404040;
|
||||||
|
border-color: #505050;
|
||||||
|
}
|
||||||
|
.list-group-item {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.list-group-item.active {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.btn-outline-primary {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
.table-dark {
|
||||||
|
--bs-table-bg: #2d2d2d;
|
||||||
|
}
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: #404040;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Loading Baobab Actor UI...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -113,6 +113,32 @@ async fn execute_script_and_update_status(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a job with the given engine, setting proper job context
|
||||||
|
///
|
||||||
|
/// This function sets up the engine with job context (DB_PATH, CALLER_ID, CONTEXT_ID)
|
||||||
|
/// and evaluates the script. It returns the result or error without updating Redis.
|
||||||
|
/// This allows actors to handle Redis updates according to their own patterns.
|
||||||
|
pub async fn execute_job_with_engine(
|
||||||
|
engine: &Engine,
|
||||||
|
job: &Job,
|
||||||
|
db_path: &str,
|
||||||
|
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
|
||||||
|
// Clone the engine to avoid mutating the original
|
||||||
|
let mut engine_clone = engine.clone();
|
||||||
|
|
||||||
|
// Set up job context in the engine
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("DB_PATH".into(), db_path.to_string().into());
|
||||||
|
db_config.insert("CALLER_ID".into(), job.caller_id.clone().into());
|
||||||
|
db_config.insert("CONTEXT_ID".into(), job.context_id.clone().into());
|
||||||
|
engine_clone.set_default_tag(Dynamic::from(db_config));
|
||||||
|
|
||||||
|
debug!("Actor for Context ID '{}': Evaluating script with Rhai engine (job context set).", job.context_id);
|
||||||
|
|
||||||
|
// Execute the script with the configured engine
|
||||||
|
engine_clone.eval::<Dynamic>(&job.script)
|
||||||
|
}
|
||||||
|
|
||||||
/// Clean up job from Redis if preserve_tasks is false
|
/// Clean up job from Redis if preserve_tasks is false
|
||||||
async fn cleanup_job(
|
async fn cleanup_job(
|
||||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||||
@ -236,3 +262,6 @@ pub fn spawn_rhai_actor(
|
|||||||
// Re-export the main trait-based interface for convenience
|
// Re-export the main trait-based interface for convenience
|
||||||
pub use actor_trait::{Actor, ActorConfig, spawn_actor};
|
pub use actor_trait::{Actor, ActorConfig, spawn_actor};
|
||||||
|
|
||||||
|
// Re-export the shared job execution function
|
||||||
|
pub use execute_job_with_engine;
|
||||||
|
|
||||||
|
38
core/actor/src/main.rs
Normal file
38
core/actor/src/main.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
use baobab_actor::ui::App;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
fn main() {
|
||||||
|
console_log::init_with_level(log::Level::Debug).expect("Failed to initialize logger");
|
||||||
|
|
||||||
|
// Get configuration from URL parameters or local storage
|
||||||
|
let window = web_sys::window().expect("No global window exists");
|
||||||
|
let location = window.location();
|
||||||
|
let search = location.search().unwrap_or_default();
|
||||||
|
|
||||||
|
// Parse URL parameters for actor configuration
|
||||||
|
let url_params = web_sys::UrlSearchParams::new_with_str(&search).unwrap();
|
||||||
|
|
||||||
|
let actor_id = url_params.get("id").unwrap_or_else(|| "default_actor".to_string());
|
||||||
|
let actor_path = url_params.get("path").unwrap_or_else(|| "/path/to/actor".to_string());
|
||||||
|
let example_dir = url_params.get("example_dir");
|
||||||
|
let redis_url = url_params.get("redis_url").unwrap_or_else(|| "redis://localhost:6379".to_string());
|
||||||
|
|
||||||
|
log::info!("Starting Baobab Actor UI with actor_id: {}", actor_id);
|
||||||
|
|
||||||
|
yew::Renderer::<App>::with_props(baobab_actor::ui::app::AppProps {
|
||||||
|
actor_id,
|
||||||
|
actor_path,
|
||||||
|
example_dir,
|
||||||
|
redis_url,
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
fn main() {
|
||||||
|
eprintln!("This binary is only available with the 'wasm' feature enabled.");
|
||||||
|
eprintln!("Please compile with: cargo build --features wasm --target wasm32-unknown-unknown");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
@ -3,6 +3,14 @@ name = "hero_supervisor"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "supervisor-cli"
|
||||||
|
path = "cmd/supervisor_cli.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "supervisor-tui"
|
||||||
|
path = "cmd/supervisor_tui.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
|
117
core/supervisor/cmd/README.md
Normal file
117
core/supervisor/cmd/README.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Supervisor CLI
|
||||||
|
|
||||||
|
Interactive command-line interface for the Hero Supervisor that allows you to dispatch jobs to actors and manage the job lifecycle.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive Menu**: Easy-to-use menu system for all supervisor operations
|
||||||
|
- **Job Management**: Create, run, monitor, and manage jobs
|
||||||
|
- **OSIS Actor Integration**: Dispatch Rhai scripts to the OSIS actor
|
||||||
|
- **Real-time Results**: Get immediate feedback from job execution
|
||||||
|
- **Colorized Output**: Clear visual feedback with colored status indicators
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Build the OSIS Actor
|
||||||
|
|
||||||
|
First, ensure the OSIS actor is built:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/timurgordon/code/git.ourworld.tf/herocode/actor_osis
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure the Supervisor
|
||||||
|
|
||||||
|
Create or use the example configuration file at `examples/cli_config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[global]
|
||||||
|
redis_url = "redis://127.0.0.1/"
|
||||||
|
|
||||||
|
[actors]
|
||||||
|
osis_actor = "/Users/timurgordon/code/git.ourworld.tf/herocode/actor_osis/target/debug/actor_osis"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/timurgordon/code/git.ourworld.tf/herocode/baobab/core/supervisor
|
||||||
|
cargo run --bin supervisor-cli -- --config examples/cli_config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with verbose logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin supervisor-cli -- --config examples/cli_config.toml --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
1. **list_jobs** - List all jobs in the system
|
||||||
|
2. **run_job** - Create and run a new job interactively
|
||||||
|
3. **get_job_status** - Get status of a specific job
|
||||||
|
4. **get_job_output** - Get output of a completed job
|
||||||
|
5. **get_job_logs** - Get logs for a specific job
|
||||||
|
6. **stop_job** - Stop a running job
|
||||||
|
7. **delete_job** - Delete a specific job
|
||||||
|
8. **clear_all_jobs** - Clear all jobs from the system
|
||||||
|
9. **quit** - Exit the CLI
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
1. Start the CLI with your configuration
|
||||||
|
2. Select option `2` (run_job)
|
||||||
|
3. Enter job details:
|
||||||
|
- **Caller**: Your name or identifier
|
||||||
|
- **Context**: Description of what the job does
|
||||||
|
- **Script**: Rhai script to execute (end with empty line)
|
||||||
|
4. The job is automatically dispatched to the OSIS actor
|
||||||
|
5. View the real-time result
|
||||||
|
|
||||||
|
### Example Rhai Script
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// Simple calculation
|
||||||
|
let result = 10 + 20 * 3;
|
||||||
|
print("Calculation result: " + result);
|
||||||
|
result
|
||||||
|
```
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// Working with strings
|
||||||
|
let message = "Hello from OSIS Actor!";
|
||||||
|
print(message);
|
||||||
|
message.to_upper()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Job Status Colors
|
||||||
|
|
||||||
|
- **Created** - Cyan
|
||||||
|
- **Dispatched** - Blue
|
||||||
|
- **Started** - Yellow
|
||||||
|
- **Finished** - Green
|
||||||
|
- **Error** - Red
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Redis server running on localhost:6379 (or configured URL)
|
||||||
|
- OSIS actor binary built and accessible
|
||||||
|
- Proper permissions to start/stop processes via Zinit
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Actor Not Starting
|
||||||
|
- Verify the OSIS actor binary path in the TOML config
|
||||||
|
- Check that the binary exists and is executable
|
||||||
|
- Ensure Redis is running and accessible
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
- Verify Redis URL in configuration
|
||||||
|
- Check network connectivity to Redis server
|
||||||
|
- Ensure no firewall blocking connections
|
||||||
|
|
||||||
|
### Job Execution Failures
|
||||||
|
- Check job logs using `get_job_logs` command
|
||||||
|
- Verify Rhai script syntax
|
||||||
|
- Check actor logs for detailed error information
|
178
core/supervisor/cmd/TUI_README.md
Normal file
178
core/supervisor/cmd/TUI_README.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Supervisor Terminal UI (TUI)
|
||||||
|
|
||||||
|
A modern, interactive Terminal User Interface for the Hero Supervisor that provides intuitive job management with real-time updates and visual navigation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎯 **Intuitive Interface**
|
||||||
|
- **Split-pane Layout**: Job list on the left, details on the right
|
||||||
|
- **Real-time Updates**: Auto-refreshes every 2 seconds
|
||||||
|
- **Color-coded Status**: Visual job status indicators
|
||||||
|
- **Keyboard Navigation**: Vim-style and arrow key support
|
||||||
|
|
||||||
|
### 📋 **Job Management**
|
||||||
|
- **Create Jobs**: Interactive form with tab navigation
|
||||||
|
- **Monitor Jobs**: Real-time status updates with color coding
|
||||||
|
- **View Details**: Detailed job information and output
|
||||||
|
- **View Logs**: Access job execution logs
|
||||||
|
- **Stop/Delete**: Job lifecycle management
|
||||||
|
- **Bulk Operations**: Clear all jobs with confirmation
|
||||||
|
|
||||||
|
### 🎨 **Visual Design**
|
||||||
|
- **Status Colors**:
|
||||||
|
- 🔵 **Blue**: Dispatched
|
||||||
|
- 🟡 **Yellow**: Started
|
||||||
|
- 🟢 **Green**: Finished
|
||||||
|
- 🔴 **Red**: Error
|
||||||
|
- 🟣 **Magenta**: Waiting for Prerequisites
|
||||||
|
- **Highlighted Selection**: Clear visual feedback
|
||||||
|
- **Popup Messages**: Status and error notifications
|
||||||
|
- **Confirmation Dialogs**: Safe bulk operations
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Start the TUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/timurgordon/code/git.ourworld.tf/herocode/baobab/core/supervisor
|
||||||
|
cargo run --bin supervisor-tui -- --config examples/cli_config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Navigation
|
||||||
|
|
||||||
|
#### Main View
|
||||||
|
- **↑/↓ or j/k**: Navigate job list
|
||||||
|
- **Enter/Space**: View job details
|
||||||
|
- **n/c**: Create new job
|
||||||
|
- **r**: Manual refresh
|
||||||
|
- **d**: Delete selected job (with confirmation)
|
||||||
|
- **s**: Stop selected job
|
||||||
|
- **C**: Clear all jobs (with confirmation)
|
||||||
|
- **q**: Quit application
|
||||||
|
|
||||||
|
#### Job Creation Form
|
||||||
|
- **Tab**: Next field
|
||||||
|
- **Shift+Tab**: Previous field
|
||||||
|
- **Enter**: Next field (or newline in script field)
|
||||||
|
- **F5**: Submit job
|
||||||
|
- **Esc**: Cancel and return to main view
|
||||||
|
|
||||||
|
#### Job Details/Logs View
|
||||||
|
- **Esc/q**: Return to main view
|
||||||
|
- **l**: Switch to logs view
|
||||||
|
- **d**: Switch to details view
|
||||||
|
|
||||||
|
## Interface Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Hero Supervisor TUI - Job Management │
|
||||||
|
├─────────────────────┬───────────────────────────────────────┤
|
||||||
|
│ Jobs │ Job Details │
|
||||||
|
│ │ │
|
||||||
|
│ >> 1a2b3c4d - ✅ Fi │ Job ID: 1a2b3c4d5e6f7g8h │
|
||||||
|
│ 2b3c4d5e - 🟡 St │ Status: Finished │
|
||||||
|
│ 3c4d5e6f - 🔴 Er │ │
|
||||||
|
│ 4d5e6f7g - 🔵 Di │ Output: │
|
||||||
|
│ │ Calculation result: 70 │
|
||||||
|
│ │ 70 │
|
||||||
|
├─────────────────────┴───────────────────────────────────────┤
|
||||||
|
│ q: Quit | n: New Job | ↑↓: Navigate | Enter: Details │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Job Creation Workflow
|
||||||
|
|
||||||
|
1. **Press 'n'** to create a new job
|
||||||
|
2. **Fill in the form**:
|
||||||
|
- **Caller**: Your name or identifier
|
||||||
|
- **Context**: Job description
|
||||||
|
- **Script**: Rhai script (supports multi-line)
|
||||||
|
3. **Press F5** to submit
|
||||||
|
4. **Watch real-time execution** in the main view
|
||||||
|
|
||||||
|
### Example Rhai Scripts
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// Simple calculation
|
||||||
|
let result = 10 + 20 * 3;
|
||||||
|
print("Calculation result: " + result);
|
||||||
|
result
|
||||||
|
```
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// String manipulation
|
||||||
|
let message = "Hello from OSIS Actor!";
|
||||||
|
print(message);
|
||||||
|
message.to_upper()
|
||||||
|
```
|
||||||
|
|
||||||
|
```rhai
|
||||||
|
// Loop example
|
||||||
|
let sum = 0;
|
||||||
|
for i in 1..=10 {
|
||||||
|
sum += i;
|
||||||
|
}
|
||||||
|
print("Sum of 1-10: " + sum);
|
||||||
|
sum
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Improvements over CLI
|
||||||
|
|
||||||
|
### ✅ **Better UX**
|
||||||
|
- **Visual Navigation**: No need to remember numbers
|
||||||
|
- **Real-time Updates**: See job progress immediately
|
||||||
|
- **Split-pane Design**: View list and details simultaneously
|
||||||
|
- **Form Validation**: Clear error messages
|
||||||
|
|
||||||
|
### ✅ **Enhanced Productivity**
|
||||||
|
- **Auto-refresh**: Always up-to-date information
|
||||||
|
- **Keyboard Shortcuts**: Fast navigation and actions
|
||||||
|
- **Confirmation Dialogs**: Prevent accidental operations
|
||||||
|
- **Multi-line Script Input**: Better script editing
|
||||||
|
|
||||||
|
### ✅ **Professional Interface**
|
||||||
|
- **Color-coded Status**: Quick visual assessment
|
||||||
|
- **Consistent Layout**: Predictable interface elements
|
||||||
|
- **Popup Notifications**: Non-intrusive feedback
|
||||||
|
- **Graceful Error Handling**: User-friendly error messages
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Redis server running (default: localhost:6379)
|
||||||
|
- OSIS actor binary built and configured
|
||||||
|
- Terminal with color support
|
||||||
|
- Minimum terminal size: 80x24
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Display Issues
|
||||||
|
- Ensure terminal supports colors and Unicode
|
||||||
|
- Resize terminal if layout appears broken
|
||||||
|
- Use a modern terminal emulator (iTerm2, Alacritty, etc.)
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- TUI auto-refreshes every 2 seconds
|
||||||
|
- Large job lists may impact performance
|
||||||
|
- Use 'r' for manual refresh if needed
|
||||||
|
|
||||||
|
### Navigation Issues
|
||||||
|
- Use arrow keys if vim keys (j/k) don't work
|
||||||
|
- Ensure terminal is in focus
|
||||||
|
- Try Esc to reset state if stuck
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Bulk Operations
|
||||||
|
- **Clear All Jobs**: Press 'C' with confirmation
|
||||||
|
- **Safe Deletion**: Confirmation required for destructive operations
|
||||||
|
|
||||||
|
### Real-time Monitoring
|
||||||
|
- **Auto-refresh**: Updates every 2 seconds
|
||||||
|
- **Status Tracking**: Watch job progression
|
||||||
|
- **Immediate Feedback**: See results as they complete
|
||||||
|
|
||||||
|
### Multi-line Scripts
|
||||||
|
- **Rich Text Input**: Full script editing in TUI
|
||||||
|
- **Syntax Awareness**: Better than single-line CLI input
|
||||||
|
- **Preview**: See script before submission
|
398
core/supervisor/cmd/supervisor_cli.rs
Normal file
398
core/supervisor/cmd/supervisor_cli.rs
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use colored::*;
|
||||||
|
use hero_supervisor::{Supervisor, SupervisorBuilder, SupervisorError, Job, JobStatus, ScriptType};
|
||||||
|
use log::{error, info};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "supervisor-cli")]
|
||||||
|
#[command(about = "Interactive CLI for Hero Supervisor - Dispatch jobs to actors")]
|
||||||
|
struct Args {
|
||||||
|
/// Path to TOML configuration file
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Enable verbose logging
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum CliCommand {
|
||||||
|
ListJobs,
|
||||||
|
RunJob,
|
||||||
|
GetJobStatus,
|
||||||
|
GetJobOutput,
|
||||||
|
GetJobLogs,
|
||||||
|
StopJob,
|
||||||
|
DeleteJob,
|
||||||
|
ClearAllJobs,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliCommand {
|
||||||
|
fn all_commands() -> Vec<(CliCommand, &'static str, &'static str)> {
|
||||||
|
vec![
|
||||||
|
(CliCommand::ListJobs, "list_jobs", "List all jobs in the system"),
|
||||||
|
(CliCommand::RunJob, "run_job", "Create and run a new job"),
|
||||||
|
(CliCommand::GetJobStatus, "get_job_status", "Get status of a specific job"),
|
||||||
|
(CliCommand::GetJobOutput, "get_job_output", "Get output of a completed job"),
|
||||||
|
(CliCommand::GetJobLogs, "get_job_logs", "Get logs for a specific job"),
|
||||||
|
(CliCommand::StopJob, "stop_job", "Stop a running job"),
|
||||||
|
(CliCommand::DeleteJob, "delete_job", "Delete a specific job"),
|
||||||
|
(CliCommand::ClearAllJobs, "clear_all_jobs", "Clear all jobs from the system"),
|
||||||
|
(CliCommand::Quit, "quit", "Exit the CLI"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_index(index: usize) -> Option<CliCommand> {
|
||||||
|
Self::all_commands().get(index).map(|(cmd, _, _)| cmd.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SupervisorCli {
|
||||||
|
supervisor: Arc<Supervisor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupervisorCli {
|
||||||
|
fn new(supervisor: Arc<Supervisor>) -> Self {
|
||||||
|
Self { supervisor }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self) -> Result<(), SupervisorError> {
|
||||||
|
println!("{}", "=== Hero Supervisor CLI ===".bright_blue().bold());
|
||||||
|
println!("{}", "Interactive job management interface".cyan());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.display_menu();
|
||||||
|
|
||||||
|
match self.get_user_choice().await {
|
||||||
|
Some(command) => {
|
||||||
|
match command {
|
||||||
|
CliCommand::Quit => {
|
||||||
|
println!("{}", "Goodbye!".bright_green());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Err(e) = self.execute_command(command).await {
|
||||||
|
eprintln!("{} {}", "Error:".bright_red(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("{}", "Invalid selection. Please try again.".yellow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_menu(&self) {
|
||||||
|
println!("{}", "Available Commands:".bright_yellow().bold());
|
||||||
|
for (index, (_, name, description)) in CliCommand::all_commands().iter().enumerate() {
|
||||||
|
println!(" {}. {} - {}",
|
||||||
|
(index + 1).to_string().bright_white().bold(),
|
||||||
|
name.bright_cyan(),
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
print!("\n{} ", "Select a command (1-9):".bright_white());
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_choice(&self) -> Option<CliCommand> {
|
||||||
|
let mut input = String::new();
|
||||||
|
if io::stdin().read_line(&mut input).is_ok() {
|
||||||
|
if let Ok(choice) = input.trim().parse::<usize>() {
|
||||||
|
if choice > 0 {
|
||||||
|
return CliCommand::from_index(choice - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_command(&self, command: CliCommand) -> Result<(), SupervisorError> {
|
||||||
|
match command {
|
||||||
|
CliCommand::ListJobs => self.list_jobs().await,
|
||||||
|
CliCommand::RunJob => self.run_job().await,
|
||||||
|
CliCommand::GetJobStatus => self.get_job_status().await,
|
||||||
|
CliCommand::GetJobOutput => self.get_job_output().await,
|
||||||
|
CliCommand::GetJobLogs => self.get_job_logs().await,
|
||||||
|
CliCommand::StopJob => self.stop_job().await,
|
||||||
|
CliCommand::DeleteJob => self.delete_job().await,
|
||||||
|
CliCommand::ClearAllJobs => self.clear_all_jobs().await,
|
||||||
|
CliCommand::Quit => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_jobs(&self) -> Result<(), SupervisorError> {
|
||||||
|
println!("{}", "Listing all jobs...".bright_blue());
|
||||||
|
|
||||||
|
let jobs = self.supervisor.list_jobs().await?;
|
||||||
|
|
||||||
|
if jobs.is_empty() {
|
||||||
|
println!("{}", "No jobs found.".yellow());
|
||||||
|
} else {
|
||||||
|
println!("{} jobs found:", jobs.len().to_string().bright_white().bold());
|
||||||
|
for job_id in jobs {
|
||||||
|
let status = self.supervisor.get_job_status(&job_id).await?;
|
||||||
|
let status_color = match status {
|
||||||
|
JobStatus::Dispatched => "blue",
|
||||||
|
JobStatus::Started => "yellow",
|
||||||
|
JobStatus::Finished => "green",
|
||||||
|
JobStatus::Error => "red",
|
||||||
|
JobStatus::WaitingForPrerequisites => "magenta",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(" {} - {}",
|
||||||
|
job_id.bright_white(),
|
||||||
|
format!("{:?}", status).color(status_color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_job(&self) -> Result<(), SupervisorError> {
|
||||||
|
println!("{}", "Creating a new job...".bright_blue());
|
||||||
|
|
||||||
|
// Get caller
|
||||||
|
print!("Enter caller name: ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
let mut caller = String::new();
|
||||||
|
io::stdin().read_line(&mut caller).unwrap();
|
||||||
|
let caller = caller.trim().to_string();
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
print!("Enter job context: ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
let mut context = String::new();
|
||||||
|
io::stdin().read_line(&mut context).unwrap();
|
||||||
|
let context = context.trim().to_string();
|
||||||
|
|
||||||
|
// Get script
|
||||||
|
println!("Enter Rhai script (end with empty line):");
|
||||||
|
let mut script_lines = Vec::new();
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
io::stdin().read_line(&mut line).unwrap();
|
||||||
|
let line = line.trim_end_matches('\n');
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
script_lines.push(line.to_string());
|
||||||
|
}
|
||||||
|
let script = script_lines.join("\n");
|
||||||
|
|
||||||
|
if script.is_empty() {
|
||||||
|
println!("{}", "Script cannot be empty!".bright_red());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, default to OSIS actor (ScriptType::OSIS)
|
||||||
|
let script_type = ScriptType::OSIS;
|
||||||
|
|
||||||
|
// Create the job
|
||||||
|
let job = Job::new(caller, context, script, script_type);
|
||||||
|
|
||||||
|
println!("{} Job ID: {}",
|
||||||
|
"Created job with".bright_green(),
|
||||||
|
job.id.bright_white().bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run the job and await result
|
||||||
|
println!("{}", "Dispatching job and waiting for result...".bright_blue());
|
||||||
|
|
||||||
|
match self.supervisor.run_job_and_await_result(&job).await {
|
||||||
|
Ok(result) => {
|
||||||
|
println!("{}", "Job completed successfully!".bright_green().bold());
|
||||||
|
println!("{} {}", "Result:".bright_yellow(), result.bright_white());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} {}", "Job failed:".bright_red().bold(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job_status(&self) -> Result<(), SupervisorError> {
|
||||||
|
let job_id = self.prompt_for_job_id("Enter job ID to check status: ")?;
|
||||||
|
|
||||||
|
let status = self.supervisor.get_job_status(&job_id).await?;
|
||||||
|
let status_color = match status {
|
||||||
|
JobStatus::Dispatched => "blue",
|
||||||
|
JobStatus::Started => "yellow",
|
||||||
|
JobStatus::Finished => "green",
|
||||||
|
JobStatus::Error => "red",
|
||||||
|
JobStatus::WaitingForPrerequisites => "magenta",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{} {} - {}",
|
||||||
|
"Job".bright_white(),
|
||||||
|
job_id.bright_white().bold(),
|
||||||
|
format!("{:?}", status).color(status_color).bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job_output(&self) -> Result<(), SupervisorError> {
|
||||||
|
let job_id = self.prompt_for_job_id("Enter job ID to get output: ")?;
|
||||||
|
|
||||||
|
match self.supervisor.get_job_output(&job_id).await? {
|
||||||
|
Some(output) => {
|
||||||
|
println!("{}", "Job Output:".bright_yellow().bold());
|
||||||
|
println!("{}", output.bright_white());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("{}", "No output available for this job.".yellow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_job_logs(&self) -> Result<(), SupervisorError> {
|
||||||
|
let job_id = self.prompt_for_job_id("Enter job ID to get logs: ")?;
|
||||||
|
|
||||||
|
match self.supervisor.get_job_logs(&job_id).await? {
|
||||||
|
Some(logs) => {
|
||||||
|
println!("{}", "Job Logs:".bright_yellow().bold());
|
||||||
|
println!("{}", logs.bright_white());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("{}", "No logs available for this job.".yellow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_job(&self) -> Result<(), SupervisorError> {
|
||||||
|
let job_id = self.prompt_for_job_id("Enter job ID to stop: ")?;
|
||||||
|
|
||||||
|
self.supervisor.stop_job(&job_id).await?;
|
||||||
|
println!("{} {}",
|
||||||
|
"Stop signal sent for job".bright_green(),
|
||||||
|
job_id.bright_white().bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_job(&self) -> Result<(), SupervisorError> {
|
||||||
|
let job_id = self.prompt_for_job_id("Enter job ID to delete: ")?;
|
||||||
|
|
||||||
|
self.supervisor.delete_job(&job_id).await?;
|
||||||
|
println!("{} {}",
|
||||||
|
"Deleted job".bright_green(),
|
||||||
|
job_id.bright_white().bold()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_all_jobs(&self) -> Result<(), SupervisorError> {
|
||||||
|
print!("Are you sure you want to clear ALL jobs? (y/N): ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut confirmation = String::new();
|
||||||
|
io::stdin().read_line(&mut confirmation).unwrap();
|
||||||
|
|
||||||
|
if confirmation.trim().to_lowercase() == "y" {
|
||||||
|
let count = self.supervisor.clear_all_jobs().await?;
|
||||||
|
println!("{} {} jobs",
|
||||||
|
"Cleared".bright_green().bold(),
|
||||||
|
count.to_string().bright_white().bold()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("{}", "Operation cancelled.".yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_for_job_id(&self, prompt: &str) -> Result<String, SupervisorError> {
|
||||||
|
print!("{}", prompt);
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut job_id = String::new();
|
||||||
|
io::stdin().read_line(&mut job_id).unwrap();
|
||||||
|
let job_id = job_id.trim().to_string();
|
||||||
|
|
||||||
|
if job_id.is_empty() {
|
||||||
|
return Err(SupervisorError::ConfigError("Job ID cannot be empty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(job_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
if args.verbose {
|
||||||
|
env_logger::Builder::from_default_env()
|
||||||
|
.filter_level(log::LevelFilter::Debug)
|
||||||
|
.init();
|
||||||
|
} else {
|
||||||
|
env_logger::Builder::from_default_env()
|
||||||
|
.filter_level(log::LevelFilter::Info)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Starting Supervisor CLI with config: {:?}", args.config);
|
||||||
|
|
||||||
|
// Build supervisor from TOML config
|
||||||
|
let supervisor = Arc::new(
|
||||||
|
SupervisorBuilder::from_toml(&args.config)?
|
||||||
|
.build().await?
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("{}", "Starting actors...".bright_blue());
|
||||||
|
|
||||||
|
// Start the actors
|
||||||
|
supervisor.start_actors().await?;
|
||||||
|
|
||||||
|
// Give actors time to start up
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
println!("{}", "Actors started successfully!".bright_green());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Create and run the CLI
|
||||||
|
let cli = SupervisorCli::new(supervisor.clone());
|
||||||
|
|
||||||
|
// Setup cleanup on exit
|
||||||
|
let supervisor_cleanup = supervisor.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl+c");
|
||||||
|
println!("\n{}", "Shutting down...".bright_yellow());
|
||||||
|
if let Err(e) = supervisor_cleanup.cleanup_and_shutdown().await {
|
||||||
|
eprintln!("Error during cleanup: {}", e);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the interactive CLI
|
||||||
|
cli.run().await?;
|
||||||
|
|
||||||
|
// Cleanup on normal exit
|
||||||
|
supervisor.cleanup_and_shutdown().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1052
core/supervisor/cmd/supervisor_tui.rs
Normal file
1052
core/supervisor/cmd/supervisor_tui.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -45,13 +45,27 @@ Jobs can have dependencies on other jobs, which are stored in the `dependencies`
|
|||||||
|
|
||||||
### Work Queues
|
### Work Queues
|
||||||
|
|
||||||
Jobs are queued for execution using Redis lists:
|
Jobs are queued for execution using Redis lists with the following naming convention:
|
||||||
```
|
```
|
||||||
hero:work_queue:{actor_id}
|
hero:job:actor_queue:{script_type_suffix}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Where `{script_type_suffix}` corresponds to the script type:
|
||||||
|
- `osis` for OSIS actors (Rhai/HeroScript execution)
|
||||||
|
- `sal` for SAL actors (System Abstraction Layer)
|
||||||
|
- `v` for V actors (V language execution)
|
||||||
|
- `python` for Python actors
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- OSIS actor queue: `hero:job:actor_queue:osis`
|
||||||
|
- SAL actor queue: `hero:job:actor_queue:sal`
|
||||||
|
- V actor queue: `hero:job:actor_queue:v`
|
||||||
|
- Python actor queue: `hero:job:actor_queue:python`
|
||||||
|
|
||||||
Actors listen on their specific queue using `BLPOP` for job IDs to process.
|
Actors listen on their specific queue using `BLPOP` for job IDs to process.
|
||||||
|
|
||||||
|
**Important:** Actors must use the same queue naming convention in their `actor_id()` method to ensure proper job dispatch. The actor should return `"actor_queue:{script_type_suffix}"` as its actor ID.
|
||||||
|
|
||||||
### Stop Queues
|
### Stop Queues
|
||||||
|
|
||||||
Job stop requests are sent through dedicated stop queues:
|
Job stop requests are sent through dedicated stop queues:
|
||||||
@ -63,12 +77,26 @@ Actors monitor these queues to receive stop requests for running jobs.
|
|||||||
|
|
||||||
### Reply Queues
|
### Reply Queues
|
||||||
|
|
||||||
For synchronous job execution, dedicated reply queues are used:
|
Reply queues are used for responses to specific requests:
|
||||||
```
|
|
||||||
hero:reply:{job_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
Actors send results to these queues when jobs complete.
|
- `hero:reply:{request_id}`: Response to a specific request
|
||||||
|
|
||||||
|
### Result and Error Queues
|
||||||
|
|
||||||
|
When actors process jobs, they store results and errors in two places:
|
||||||
|
|
||||||
|
1. **Job Hash Storage**: Results are stored in the job hash fields:
|
||||||
|
- `hero:job:{job_id}` hash with `output` field for results
|
||||||
|
- `hero:job:{job_id}` hash with `error` field for errors
|
||||||
|
|
||||||
|
2. **Dedicated Queues**: Results and errors are also pushed to dedicated queues for asynchronous retrieval:
|
||||||
|
- `hero:job:{job_id}:result`: Queue containing job result (use `LPOP` to retrieve)
|
||||||
|
- `hero:job:{job_id}:error`: Queue containing job error (use `LPOP` to retrieve)
|
||||||
|
|
||||||
|
This dual storage approach allows clients to:
|
||||||
|
- Access results/errors directly from job hash for immediate retrieval
|
||||||
|
- Listen on result/error queues for asynchronous notification of job completion
|
||||||
|
- Use `BLPOP` on result/error queues for blocking waits on job completion
|
||||||
|
|
||||||
## Job Lifecycle
|
## Job Lifecycle
|
||||||
|
|
||||||
|
20
core/supervisor/examples/cli_config.toml
Normal file
20
core/supervisor/examples/cli_config.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Hero Supervisor CLI Configuration
|
||||||
|
# This configuration sets up the supervisor with an OSIS actor for job processing
|
||||||
|
|
||||||
|
[global]
|
||||||
|
redis_url = "redis://127.0.0.1/"
|
||||||
|
|
||||||
|
[actors]
|
||||||
|
# OSIS Actor configuration - handles Object Storage and Indexing System jobs
|
||||||
|
osis_actor = "/Users/timurgordon/code/git.ourworld.tf/herocode/actor_osis/target/debug/actor_osis"
|
||||||
|
|
||||||
|
# Optional: Other actors can be configured here
|
||||||
|
# sal_actor = "/path/to/sal_actor"
|
||||||
|
# v_actor = "/path/to/v_actor"
|
||||||
|
# python_actor = "/path/to/python_actor"
|
||||||
|
|
||||||
|
# Optional: WebSocket server configuration for remote API access
|
||||||
|
# [websocket]
|
||||||
|
# host = "127.0.0.1"
|
||||||
|
# port = 8443
|
||||||
|
# redis_url = "redis://127.0.0.1/"
|
@ -150,7 +150,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
println!(" Authentication: {}", if config.auth { "ENABLED" } else { "DISABLED" });
|
println!(" Authentication: {}", if config.auth { "ENABLED" } else { "DISABLED" });
|
||||||
println!(" TLS/WSS: {}", if config.tls { "ENABLED" } else { "DISABLED" });
|
println!(" TLS/WSS: {}", if config.tls { "ENABLED" } else { "DISABLED" });
|
||||||
println!(" Webhooks: {}", if config.webhooks { "ENABLED" } else { "DISABLED" });
|
|
||||||
println!(" Circles configured: {}", config.circles.len());
|
println!(" Circles configured: {}", config.circles.len());
|
||||||
|
|
||||||
if config.tls {
|
if config.tls {
|
||||||
@ -160,12 +159,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.webhooks {
|
|
||||||
println!(" Webhook secrets loaded from environment variables:");
|
|
||||||
println!(" - STRIPE_WEBHOOK_SECRET");
|
|
||||||
println!(" - IDENFY_WEBHOOK_SECRET");
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.auth && !config.circles.is_empty() {
|
if config.auth && !config.circles.is_empty() {
|
||||||
println!(" Configured circles:");
|
println!(" Configured circles:");
|
||||||
for (circle_name, members) in &config.circles {
|
for (circle_name, members) in &config.circles {
|
||||||
|
Loading…
Reference in New Issue
Block a user