Compare commits

...

2 Commits

Author SHA1 Message Date
fab2d199d1 Update git remote URL from git.ourworld.tf to git.threefold.info 2025-06-15 16:21:00 +02:00
ae6966edb2 ... 2025-05-31 11:15:44 +03:00
25 changed files with 2906 additions and 0 deletions

274
examples/example_old.v Normal file
View File

@@ -0,0 +1,274 @@
module main
import zinit
// Example 1: Get the OpenRPC API specification
fn example_rpc_discover(mut client zinit.Client) {
println('1. Getting API specification...')
spec := client.rpc_discover() or {
eprintln('Error getting API spec: ${err}')
return
}
println('API Title: ${spec['info'] or { map[string]interface{}{} }['title'] or { 'Unknown' }}')
println('API Version: ${spec['info'] or { map[string]interface{}{} }['version'] or { 'Unknown' }}')
println()
}
// Example 2: List all services
fn example_service_list(mut client zinit.Client) {
println('2. Listing all services...')
services := client.service_list() or {
eprintln('Error listing services: ${err}')
return
}
if services.len == 0 {
println('No services found.')
} else {
for name, state in services {
println(' ${name}: ${state}')
}
}
println()
}
// Example 3: Get detailed status for a specific service
fn example_service_status(mut client zinit.Client) {
println('3. Getting service status...')
// Try to get status for a service (replace 'redis' with an actual service name)
service_name := 'redis'
status := client.service_status(service_name) or {
eprintln('Error getting service status: ${err}')
return
}
println('Service: ${status.name}')
println(' State: ${status.state}')
println(' Target: ${status.target}')
println(' PID: ${status.pid}')
if status.after.len > 0 {
println(' Dependencies:')
for dep_name, dep_state in status.after {
println(' ${dep_name}: ${dep_state}')
}
}
println()
}
// Example 4: Create a new service configuration
fn example_service_create(mut client zinit.Client) {
println('4. Creating a new service...')
// Create a simple service configuration
config := zinit.ServiceConfig{
exec: '/usr/bin/echo "Hello from my service"'
oneshot: true
log: 'stdout'
env: {
'MY_VAR': 'my_value'
'PATH': '/usr/bin:/bin'
}
shutdown_timeout: 30
}
service_name := 'example_service'
result := client.service_create(service_name, config) or {
eprintln('Error creating service: ${err}')
return
}
println('Service created: ${result}')
println()
}
// Example 5: Service management operations
fn example_service_management(mut client zinit.Client) {
println('5. Service management operations...')
service_name := 'example_service'
// Monitor the service
client.service_monitor(service_name) or {
eprintln('Error monitoring service: ${err}')
return
}
println('Service ${service_name} is now being monitored')
// Start the service
client.service_start(service_name) or {
eprintln('Error starting service: ${err}')
return
}
println('Service ${service_name} started')
// Wait a moment for the service to run (since it's oneshot)
// In a real application, you might want to check the status instead
// Stop the service (if it's still running)
client.service_stop(service_name) or {
// This might fail if the service already finished (oneshot)
println('Note: Could not stop service (might have already finished): ${err}')
}
// Forget the service (stop monitoring)
client.service_forget(service_name) or {
eprintln('Error forgetting service: ${err}')
return
}
println('Service ${service_name} is no longer being monitored')
// Delete the service configuration
client.service_delete(service_name) or {
eprintln('Error deleting service: ${err}')
return
}
println('Service ${service_name} configuration deleted')
println()
}
// Example 6: Get service statistics
fn example_service_stats(mut client zinit.Client) {
println('6. Getting service statistics...')
// Try to get stats for a running service
service_name := 'redis' // Replace with an actual running service
stats := client.service_stats(service_name) or {
eprintln('Error getting service stats: ${err}')
return
}
println('Service: ${stats.name}')
println(' PID: ${stats.pid}')
println(' Memory Usage: ${stats.memory_usage} bytes (${stats.memory_usage / 1024 / 1024} MB)')
println(' CPU Usage: ${stats.cpu_usage}%')
if stats.children.len > 0 {
println(' Child Processes:')
for child in stats.children {
println(' PID ${child.pid}: ${child.memory_usage} bytes, ${child.cpu_usage}% CPU')
}
}
println()
}
// Example 7: Log streaming
fn example_log_streaming(mut client zinit.Client) {
println('7. Getting current logs...')
// Get all current logs
logs := client.stream_current_logs(none) or {
eprintln('Error getting logs: ${err}')
return
}
if logs.len == 0 {
println('No logs available')
} else {
println('Recent logs:')
for i, log_entry in logs {
println(' ${i + 1}: ${log_entry}')
if i >= 4 { // Show only first 5 logs
println(' ... (${logs.len - 5} more logs)')
break
}
}
}
// Get logs for a specific service
service_logs := client.stream_current_logs('redis') or {
eprintln('Error getting service logs: ${err}')
return
}
if service_logs.len > 0 {
println('Redis service logs:')
for log_entry in service_logs {
println(' ${log_entry}')
}
}
println()
}
// Example 8: System operations
fn example_system_operations(mut client zinit.Client) {
println('8. System operations...')
// Start HTTP server (be careful with this in production)
server_result := client.system_start_http_server('127.0.0.1:8080') or {
eprintln('Error starting HTTP server: ${err}')
return
}
println('HTTP server: ${server_result}')
// Stop HTTP server
client.system_stop_http_server() or {
eprintln('Error stopping HTTP server: ${err}')
return
}
println('HTTP server stopped')
// Note: system_shutdown() and system_reboot() are commented out
// as they would actually shut down or reboot the system!
// client.system_shutdown() or {
// eprintln('Error shutting down system: ${err}')
// return
// }
// println('System shutdown initiated')
// client.system_reboot() or {
// eprintln('Error rebooting system: ${err}')
// return
// }
// println('System reboot initiated')
println('System operations completed (shutdown/reboot skipped for safety)')
println()
}
// Create a new zinit client with default socket path (/tmp/zinit.sock)
mut client := zinit.new_default_client()
// Alternatively, you can specify a custom socket path:
// mut client := zinit.new_client('/custom/path/to/zinit.sock')
// Ensure we disconnect when done
defer {
client.disconnect()
}
println('=== Zinit Client Example ===\n')
// Example 1: Get API specification
example_rpc_discover(mut client)
// Example 2: List all services
example_service_list(mut client)
// Example 3: Get service status
example_service_status(mut client)
// Example 4: Create a new service
example_service_create(mut client)
// Example 5: Service management operations
example_service_management(mut client)
// Example 6: Get service statistics
example_service_stats(mut client)
// Example 7: Log streaming
example_log_streaming(mut client)
// Example 8: System operations
example_system_operations(mut client)

124
examples/zinit_client_example.vsh Executable file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.heroweb.clients.zinit
import x.json2
// Create a new Zinit client with the default socket path
mut client := zinit.new_default_client()
println('Connected to Zinit via OpenRPC')
// Example 1: Get the OpenRPC API specification
println('\n=== Getting API Specification ===')
api_spec_response := client.rpc_discover() or {
println('Error getting API spec: ${err}')
return
}
println('API Specification (first 100 chars): ${json2.encode(api_spec_response.spec)[..100]}...')
// Example 2: List all services
println('\n=== Listing Services ===')
service_list_response := client.service_list() or {
println('Error listing services: ${err}')
return
}
println('Services:')
for name, state in service_list_response.services {
println('- ${name}: ${state}')
}
// Example 3: Get detailed status of a service (if any exist)
if service_list_response.services.len > 0 {
service_name := service_list_response.services.keys()[0]
println('\n=== Getting Status for Service: ${service_name} ===')
status := client.service_status(service_name) or {
println('Error getting status: ${err}')
return
}
println('Service Status:')
println('- Name: ${status.name}')
println('- PID: ${status.pid}')
println('- State: ${status.state}')
println('- Target: ${status.target}')
println('- Dependencies:')
for dep_name, dep_state in status.after {
println(' - ${dep_name}: ${dep_state}')
}
// Example 4: Get service stats
println('\n=== Getting Stats for Service: ${service_name} ===')
stats := client.service_stats(service_name) or {
println('Error getting stats: ${err}')
println('Note: Stats are only available for running services')
return
}
println('Service Stats:')
println('- Memory Usage: ${stats.memory_usage} bytes (${zinit.format_memory_usage(stats.memory_usage)})')
println('- CPU Usage: ${stats.cpu_usage}% (${zinit.format_cpu_usage(stats.cpu_usage)})')
if stats.children.len > 0 {
println('- Child Processes:')
for child in stats.children {
println(' - PID: ${child.pid}, Memory: ${zinit.format_memory_usage(child.memory_usage)}, CPU: ${zinit.format_cpu_usage(child.cpu_usage)}')
}
}
// Example 5: Get logs for a service
println('\n=== Getting Logs for Service: ${service_name} ===')
logs_response := client.stream_current_logs(service_name) or {
println('Error getting logs: ${err}')
return
}
println('Service logs:')
for log in logs_response.logs {
println('- ${log}')
}
} else {
println('\nNo services found to query')
}
// Example 6: Create a new service (commented out for safety)
/*
println('\n=== Creating a New Service ===')
new_service_config := zinit.ServiceConfig{
exec: '/bin/echo "Hello from Zinit"'
oneshot: true
after: []string{}
log: zinit.log_stdout
env: {
'ENV_VAR': 'value'
}
}
create_response := client.service_create('example_service', new_service_config) or {
println('Error creating service: ${err}')
return
}
println('Service created: ${create_response.path}')
// Start the service
client.service_start('example_service') or {
println('Error starting service: ${err}')
return
}
println('Service started')
// Delete the service when done
client.service_stop('example_service') or {
println('Error stopping service: ${err}')
return
}
client.service_forget('example_service') or {
println('Error forgetting service: ${err}')
return
}
delete_response := client.service_delete('example_service') or {
println('Error deleting service: ${err}')
return
}
println('Service deleted: ${delete_response.result}')
*/
println('\nZinit OpenRPC client example completed')

66
examples/zinit_rpc_example.vsh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.clients.zinit
import json
// We'll use the ServiceStatusResponse from the zinit module
// No need to define our own struct
// Create a client using the Unix socket transport
mut cl := jsonrpc.new_unix_socket_client("/tmp/zinit.sock")
// Example 1: Discover the API using rpc_discover
// Create a request for rpc_discover method with empty parameters
discover_request := jsonrpc.new_request_generic('rpc.discover', []string{})
// Send the request and receive the OpenRPC specification as a JSON string
println('Sending rpc_discover request...')
println('This will return the OpenRPC specification for the API')
// Use map[string]string for the result to avoid json2.Any issues
api_spec_raw := cl.send[[]string, string](discover_request)!
// Parse the JSON string manually
println('API Specification (raw):')
println(api_spec_raw)
// Example 2: List all services
// Create a request for service_list method with empty parameters
list_request := jsonrpc.new_request_generic('service_list', []string{})
// Send the request and receive a map of service names to states
println('\nSending service_list request...')
service_list := cl.send[[]string, map[string]string](list_request)!
// Display the service list
println('Service List:')
println(service_list)
// Example 3: Get status of a specific service
// First, check if we have any services to query
if service_list.len > 0 {
// Get the first service name from the list
service_name := service_list.keys()[0]
// Create a request for service_status method with the service name as parameter
// The parameter for service_status is a single string (service name)
status_request := jsonrpc.new_request_generic('service_status', service_name)
// Send the request and receive a ServiceStatusResponse object
println('\nSending service_status request for service: $service_name')
service_status := cl.send[string, zinit.ServiceStatusResponse](status_request)!
// Display the service status details
println('Service Status:')
println('- Name: ${service_status.name}')
println('- PID: ${service_status.pid}')
println('- State: ${service_status.state}')
println('- Target: ${service_status.target}')
println('- Dependencies:')
for dep_name, dep_state in service_status.after {
println(' - $dep_name: $dep_state')
}
} else {
println('\nNo services found to query status')
}

1
herolib Symbolic link
View File

@@ -0,0 +1 @@
../../../github/freeflowuniverse/herolib

205
src/agentui/agentui.v Normal file
View File

@@ -0,0 +1,205 @@
module main
import veb
import json
// Context struct must embed veb.Context
pub struct Context {
veb.Context
pub mut:
user_id string
}
// App struct for shared data
pub struct App {
pub mut:
process_controller ProcessController
job_controller JobController
system_controller SystemController
openrpc_controller OpenRPCController
}
// Main function
fn main() {
mut app := &App{
process_controller: ProcessController{}
job_controller: JobController{}
system_controller: SystemController{}
openrpc_controller: OpenRPCController{}
}
// Use veb.run with App and Context types
veb.run[App, Context](mut app, 8082)
}
// Middleware to run before each request
pub fn (mut ctx Context) before_request() bool {
ctx.user_id = ctx.get_cookie('id') or { '0' }
return true
}
// Index endpoint - Dashboard overview
@['/']
pub fn (app &App) index(mut ctx Context) veb.Result {
stats := app.system_controller.get_system_stats()
processes := app.process_controller.get_all_processes()
jobs := app.job_controller.get_all_jobs()
return $veb.html()
}
// Processes endpoint
@['/processes']
pub fn (app &App) processes(mut ctx Context) veb.Result {
processes := app.process_controller.get_all_processes()
return $veb.html()
}
// Process details endpoint
@['/processes/:pid']
pub fn (app &App) process_details(mut ctx Context, pid string) veb.Result {
pid_int := pid.int()
process := app.process_controller.get_process_by_pid(pid_int) or {
return ctx.text('Process not found')
}
return $veb.html()
}
// Jobs endpoint
@['/jobs']
pub fn (app &App) jobs(mut ctx Context) veb.Result {
jobs := app.job_controller.get_all_jobs()
return $veb.html()
}
// Job details endpoint
@['/jobs/:id']
pub fn (app &App) job_details(mut ctx Context, id string) veb.Result {
id_int := id.u32()
job := app.job_controller.get_job_by_id(id_int) or {
return ctx.text('Job not found')
}
return $veb.html()
}
// OpenRPC endpoint
@['/openrpc']
pub fn (app &App) openrpc(mut ctx Context) veb.Result {
specs := app.openrpc_controller.get_all_specs()
return $veb.html()
}
// OpenRPC spec details endpoint
@['/openrpc/:name']
pub fn (app &App) openrpc_spec(mut ctx Context, name string) veb.Result {
spec := app.openrpc_controller.get_spec_by_name(name) or {
return ctx.text('OpenRPC specification not found')
}
methods := app.openrpc_controller.get_methods_for_spec(name)
return $veb.html()
}
// API endpoints
// API endpoint to get all processes
@['/api/processes'; get]
pub fn (app &App) api_processes(mut ctx Context) veb.Result {
processes := app.process_controller.get_all_processes()
json_result := json.encode(processes)
return ctx.json(json_result)
}
// API endpoint to get process by PID
@['/api/processes/:pid'; get]
pub fn (app &App) api_process_by_pid(mut ctx Context, pid string) veb.Result {
pid_int := pid.int()
process := app.process_controller.get_process_by_pid(pid_int) or {
return ctx.text('Process not found')
}
json_result := json.encode(process)
return ctx.json(json_result)
}
// API endpoint to kill a process
@['/api/processes/:pid/kill'; post]
pub fn (app &App) api_kill_process(mut ctx Context, pid string) veb.Result {
pid_int := pid.int()
success := app.process_controller.kill_process(pid_int)
return ctx.json('{"success": $success}')
}
// API endpoint to get all jobs
@['/api/jobs'; get]
pub fn (app &App) api_jobs(mut ctx Context) veb.Result {
jobs := app.job_controller.get_all_jobs()
json_result := json.encode(jobs)
return ctx.json(json_result)
}
// API endpoint to get job by ID
@['/api/jobs/:id'; get]
pub fn (app &App) api_job_by_id(mut ctx Context, id string) veb.Result {
id_int := id.u32()
job := app.job_controller.get_job_by_id(id_int) or {
return ctx.text('Job not found')
}
json_result := json.encode(job)
return ctx.json(json_result)
}
// API endpoint to get jobs by status
@['/api/jobs/status/:status'; get]
pub fn (app &App) api_jobs_by_status(mut ctx Context, status string) veb.Result {
status_enum := match status {
'new' { JobStatus.new }
'active' { JobStatus.active }
'done' { JobStatus.done }
'error' { JobStatus.error }
else { JobStatus.new }
}
jobs := app.job_controller.get_jobs_by_status(status_enum)
json_result := json.encode(jobs)
return ctx.json(json_result)
}
// API endpoint to get jobs by circle
@['/api/jobs/circle/:id'; get]
pub fn (app &App) api_jobs_by_circle(mut ctx Context, id string) veb.Result {
jobs := app.job_controller.get_jobs_by_circle(id)
json_result := json.encode(jobs)
return ctx.json(json_result)
}
// API endpoint to get system stats
@['/api/stats'; get]
pub fn (app &App) api_stats(mut ctx Context) veb.Result {
stats := app.system_controller.get_system_stats()
json_result := json.encode(stats)
return ctx.json(json_result)
}
// API endpoint to get all OpenRPC specs
@['/api/openrpc'; get]
pub fn (app &App) api_openrpc_specs(mut ctx Context) veb.Result {
specs := app.openrpc_controller.get_all_specs()
json_result := json.encode(specs)
return ctx.json(json_result)
}
// API endpoint to get OpenRPC spec by name
@['/api/openrpc/:name'; get]
pub fn (app &App) api_openrpc_spec_by_name(mut ctx Context, name string) veb.Result {
spec := app.openrpc_controller.get_spec_by_name(name) or {
return ctx.text('OpenRPC specification not found')
}
json_result := json.encode(spec)
return ctx.json(json_result)
}
// API endpoint to execute an RPC call
@['/api/openrpc/:name/:method'; post]
pub fn (app &App) api_execute_rpc(mut ctx Context, name string, method string) veb.Result {
params := ctx.req.body
result := app.openrpc_controller.execute_rpc(name, method, params)
return ctx.json(result)
}

View File

View File

@@ -0,0 +1,51 @@
// JobController handles job-related operations
pub struct JobController {
pub:
// Will add dependencies here when connecting to openrpc
}
// get_all_jobs returns all jobs in the system
pub fn (jc &JobController) get_all_jobs() []JobInfo {
// For now using fake data, will be replaced with openrpc calls
return get_all_jobs()
}
// get_job_by_id returns a specific job by ID
pub fn (jc &JobController) get_job_by_id(id u32) ?JobInfo {
jobs := get_all_jobs()
for job in jobs {
if job.job_id == id {
return job
}
}
return error('Job not found')
}
// get_jobs_by_status returns jobs with a specific status
pub fn (jc &JobController) get_jobs_by_status(status JobStatus) []JobInfo {
jobs := get_all_jobs()
mut filtered_jobs := []JobInfo{}
for job in jobs {
if job.status == status {
filtered_jobs << job
}
}
return filtered_jobs
}
// get_jobs_by_circle returns jobs for a specific circle
pub fn (jc &JobController) get_jobs_by_circle(circle_id string) []JobInfo {
jobs := get_all_jobs()
mut filtered_jobs := []JobInfo{}
for job in jobs {
if job.circle_id == circle_id {
filtered_jobs << job
}
}
return filtered_jobs
}

View File

@@ -0,0 +1,31 @@
module controllers
// ProcessController handles process-related operations
pub struct ProcessController {
pub:
// Will add dependencies here when connecting to openrpc
}
// get_all_processes returns all processes in the system
pub fn (pc &ProcessController) get_all_processes() ![]Process {
// For now using fake data, will be replaced with openrpc calls
return get_all_processes()
}
// get_process_by_pid returns a specific process by PID
pub fn (pc &ProcessController) get_process_by_pid(pid int) !Process {
processes := get_all_processes()
for process in processes {
if process.pid == pid {
return process
}
}
return error('Process not found')
}
// kill_process attempts to kill a process
pub fn (pc &ProcessController) kill_process(pid int) bool {
// Fake implementation for now
// Will be replaced with actual implementation using openrpc
return true
}

View File

@@ -0,0 +1,38 @@
module controllers
import freeflowuniverse.herolib.schemas.openrpc
// OpenRPCController handles OpenRPC-related operations
pub struct OpenRPCController {
pub:
// Will add dependencies here when connecting to openrpc
}
// get_all_specs returns all OpenRPC specifications
pub fn (oc &OpenRPCController) get_all_specs() []OpenRPCSpec {
// For now using fake data, will be replaced with openrpc calls
return get_all_openrpc_specs()
}
// get_spec_by_name returns a specific OpenRPC specification by name
pub fn (oc &OpenRPCController) get_spec_by_name(name string) !openrpc.OpenRPC {
specs := get_all_openrpc_specs()
for spec in specs {
if spec.title.to_lower() == name.to_lower() {
return spec
}
}
return error('OpenRPC specification not found')
}
// get_methods_for_spec returns all methods for a specific OpenRPC specification
pub fn (oc &OpenRPCController) get_methods_for_spec(spec_name string) []OpenRPCMethod {
spec := oc.get_spec_by_name(spec_name) or { return [] }
return spec.methods
}
// execute_rpc executes an RPC call
pub fn (oc &OpenRPCController) execute_rpc(spec_name string, method_name string, params string) string {
// Fake implementation for now
// Will be replaced with actual implementation using openrpc
return '{"result": "Success", "data": {"id": 123, "name": "Test Result"}}'
}

View File

@@ -0,0 +1,13 @@
module controllers
// SystemController handles system-related operations
pub struct SystemController {
pub:
// Will add dependencies here when connecting to openrpc
}
// get_system_stats returns the current system statistics
pub fn (sc &SystemController) get_system_stats() SystemStats {
// For now using fake data, will be replaced with openrpc calls
return get_system_stats()
}

97
src/agentui/models/jobs.v Normal file
View File

@@ -0,0 +1,97 @@
module main
import time
// JobStatus represents the status of a job
pub enum JobStatus {
new
active
done
error
}
// ParamsType represents the type of parameters for a job
pub enum ParamsType {
hero_script
open_rpc
rhai_script
ai
}
// JobInfo represents job information for the UI
pub struct JobInfo {
pub:
job_id u32
session_key string
circle_id string
topic string
params_type ParamsType
status JobStatus
time_scheduled time.Time
time_start time.Time
time_end time.Time
duration string
error string
has_error bool
}
// get_all_jobs returns a list of mock jobs
pub fn get_all_jobs() []JobInfo {
now := time.now()
return [
JobInfo{
job_id: 1
circle_id: 'circle1'
topic: 'email'
params_type: .hero_script
status: .done
time_scheduled: now.add(-30 * time.minute)
time_start: now.add(-29 * time.minute)
time_end: now.add(-28 * time.minute)
duration: '1m0s'
error: ''
has_error: false
},
JobInfo{
job_id: 2
circle_id: 'circle1'
topic: 'backup'
params_type: .open_rpc
status: .active
time_scheduled: now.add(-15 * time.minute)
time_start: now.add(-14 * time.minute)
time_end: time.Time{}
duration: '14m0s'
error: ''
has_error: false
},
JobInfo{
job_id: 3
circle_id: 'circle2'
topic: 'sync'
params_type: .rhai_script
status: .error
time_scheduled: now.add(-45 * time.minute)
time_start: now.add(-44 * time.minute)
time_end: now.add(-43 * time.minute)
duration: '1m0s'
error: 'Failed to connect to remote server'
has_error: true
},
JobInfo{
job_id: 4
circle_id: 'circle2'
topic: 'email'
params_type: .hero_script
status: .new
time_scheduled: now.add(-5 * time.minute)
time_start: time.Time{}
time_end: time.Time{}
duration: 'Not started'
error: ''
has_error: false
}
]
}

View File

@@ -0,0 +1,68 @@
module main
import time
// OpenRPCMethod represents a method in an OpenRPC specification
pub struct OpenRPCMethod {
pub:
name string
description string
summary string
}
// OpenRPCSpec represents an OpenRPC specification
pub struct OpenRPCSpec {
pub:
title string
description string
version string
methods []OpenRPCMethod
}
// Mock data functions
// get_all_openrpc_specs returns a list of mock OpenRPC specifications
pub fn get_all_openrpc_specs() []OpenRPCSpec {
return [
OpenRPCSpec{
title: 'Process Manager'
description: 'API for managing system processes'
version: '1.0.0'
methods: [
OpenRPCMethod{
name: 'get_processes'
description: 'Get a list of all processes'
summary: 'List processes'
},
OpenRPCMethod{
name: 'kill_process'
description: 'Kill a process by PID'
summary: 'Kill process'
}
]
},
OpenRPCSpec{
title: 'Job Manager'
description: 'API for managing jobs'
version: '1.0.0'
methods: [
OpenRPCMethod{
name: 'get_jobs'
description: 'Get a list of all jobs'
summary: 'List jobs'
},
OpenRPCMethod{
name: 'get_job'
description: 'Get a job by ID'
summary: 'Get job'
},
OpenRPCMethod{
name: 'create_job'
description: 'Create a new job'
summary: 'Create job'
}
]
}
]
}

View File

@@ -0,0 +1,50 @@
module main
// Process represents a single running process with its relevant details
pub struct Process {
pub:
pid int
name string // CPU usage percentage
memory f64 // Memory usage in MB
cpu f64
}
// get_all_processes returns a list of mock processes
pub fn get_all_processes() []Process {
// Fake data for now, will be replaced with openrpc calls later
return [
Process{
pid: 1001
name: 'SystemIdleProcess'
cpu: 95.5
memory: 0.1
},
Process{
pid: 1002
name: 'explorer.exe'
cpu: 1.2
memory: 150.7
},
Process{
pid: 1003
name: 'chrome.exe'
cpu: 25.8
memory: 512.3
},
Process{
pid: 1004
name: 'code.exe'
cpu: 5.1
memory: 350.0
},
Process{
pid: 1005
name: 'v.exe'
cpu: 0.5
memory: 80.2
}
]
}

View File

@@ -0,0 +1,28 @@
module main
import time
// SystemStats represents system statistics
pub struct SystemStats {
pub:
cpu_usage f32 [json: 'cpu_usage']
memory_usage f32 [json: 'memory_usage']
disk_usage f32 [json: 'disk_usage']
uptime string [json: 'uptime']
process_count int [json: 'process_count']
job_count int [json: 'job_count']
}
// get_system_stats returns mock system statistics
pub fn get_system_stats() SystemStats {
// Fake data for now, will be replaced with openrpc calls later
return SystemStats{
cpu_usage: 45.2
memory_usage: 62.7
disk_usage: 38.5
uptime: '3 days, 7 hours, 22 minutes'
process_count: 87
job_count: 4
}
}

View File

@@ -0,0 +1,728 @@
/* Base styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f7f9;
padding: 0;
margin: 0;
}
header {
background-color: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
nav {
display: flex;
gap: 1rem;
}
nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
nav a:hover, nav a.active {
background-color: #34495e;
}
nav a.active {
font-weight: bold;
background-color: #1a252f;
}
main {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
footer {
text-align: center;
padding: 1rem;
background-color: #2c3e50;
color: white;
margin-top: 2rem;
}
h1, h2, h3 {
margin-bottom: 1rem;
}
/* Common components */
.btn {
display: inline-block;
padding: 0.5rem 1rem;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 0.9rem;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #2980b9;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.btn-primary {
background-color: #3498db;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-warning {
background-color: #f39c12;
}
.btn-warning:hover {
background-color: #e67e22;
}
.btn-danger {
background-color: #e74c3c;
}
.btn-danger:hover {
background-color: #c0392b;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
background-color: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.data-table th, .data-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
.data-table th {
background-color: #f8f9fa;
font-weight: bold;
color: #2c3e50;
}
.data-table tr:hover {
background-color: #f8f9fa;
}
.data-table tr:last-child td {
border-bottom: none;
}
.filter-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
background-color: white;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.filter-controls input[type="text"] {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
}
.filter-buttons {
display: flex;
gap: 0.5rem;
}
.filter-btn {
padding: 0.5rem 1rem;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover {
background-color: #e9ecef;
}
.filter-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.sort-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sort-controls select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.sort-controls button {
padding: 0.5rem;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.sort-controls button:hover {
background-color: #e9ecef;
}
.detail-section {
margin: 1.5rem 0;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.detail-item {
display: flex;
margin-bottom: 1rem;
border-bottom: 1px solid #ecf0f1;
padding-bottom: 0.5rem;
}
.detail-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.label {
font-weight: bold;
width: 150px;
color: #7f8c8d;
}
.value {
flex: 1;
}
.error-item {
background-color: #fdedec;
border-radius: 4px;
padding: 0.5rem;
}
.error-value {
color: #e74c3c;
}
/* Dashboard styles */
.dashboard-overview {
display: flex;
flex-direction: column;
gap: 2rem;
}
.stats-section, .processes-section, .jobs-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.stat-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #2c3e50;
margin: 1rem 0;
}
.stat-bar {
height: 10px;
background-color: #ecf0f1;
border-radius: 5px;
overflow: hidden;
margin-top: 1rem;
}
.stat-fill {
height: 100%;
background-color: #3498db;
}
.view-all {
font-size: 0.9rem;
color: #3498db;
text-decoration: none;
margin-left: 1rem;
}
.view-all:hover {
text-decoration: underline;
}
/* Process styles */
.processes-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.process-details-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.process-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.process-actions {
display: flex;
gap: 0.5rem;
}
.detail-bar {
height: 10px;
background-color: #ecf0f1;
border-radius: 5px;
overflow: hidden;
margin-top: 0.5rem;
}
.detail-fill {
height: 100%;
background-color: #3498db;
}
.process-info {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
margin-top: 1rem;
}
.info-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
}
.chart-placeholder {
height: 200px;
background-color: #ecf0f1;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
}
.placeholder-text {
color: #7f8c8d;
font-style: italic;
}
/* Job styles */
.jobs-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.job-details-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.job-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
border-left: 5px solid #3498db;
}
.job-header.status-new {
border-left-color: #3498db;
}
.job-header.status-active {
border-left-color: #2ecc71;
}
.job-header.status-done {
border-left-color: #27ae60;
}
.job-header.status-error {
border-left-color: #e74c3c;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.status-badge.status-new {
background-color: #ebf5fb;
color: #3498db;
}
.status-badge.status-active {
background-color: #e9f7ef;
color: #2ecc71;
}
.status-badge.status-done {
background-color: #eafaf1;
color: #27ae60;
}
.status-badge.status-error {
background-color: #fdedec;
color: #e74c3c;
}
.job-timeline {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.timeline {
position: relative;
padding: 1rem 0;
}
.timeline::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 20px;
width: 2px;
background-color: #ecf0f1;
}
.timeline-item {
position: relative;
padding-left: 50px;
margin-bottom: 2rem;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-icon {
position: absolute;
left: 11px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #3498db;
}
.timeline-icon.scheduled {
background-color: #3498db;
}
.timeline-icon.started {
background-color: #2ecc71;
}
.timeline-icon.completed {
background-color: #27ae60;
}
.timeline-icon.error {
background-color: #e74c3c;
}
.timeline-content {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.timeline-content h4 {
margin-bottom: 0.5rem;
}
.error-message {
color: #e74c3c;
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #fdedec;
border-radius: 4px;
}
/* OpenRPC styles */
.openrpc-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.specs-list {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.specs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1rem;
}
.spec-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
transition: transform 0.3s, box-shadow 0.3s;
}
.spec-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.spec-version {
font-size: 0.9rem;
color: #7f8c8d;
margin-bottom: 0.5rem;
}
.spec-description {
margin-bottom: 1rem;
}
.spec-methods {
margin-bottom: 1rem;
}
.openrpc-info {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.openrpc-info ul {
margin-left: 1.5rem;
margin-top: 0.5rem;
}
.openrpc-spec-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.spec-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.spec-meta {
font-size: 0.9rem;
}
.spec-description {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.methods-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.methods-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1rem;
}
.method-card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
}
.method-name {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.method-summary {
font-weight: bold;
margin-bottom: 0.5rem;
}
.method-description {
margin-bottom: 1rem;
color: #7f8c8d;
}
.method-executor {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.method-executor.hidden {
display: none;
}
.executor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #7f8c8d;
}
.executor-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
.params-section, .result-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
#params-editor {
width: 100%;
height: 150px;
font-family: monospace;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
#result-display {
width: 100%;
min-height: 150px;
background-color: #2c3e50;
color: white;
padding: 1rem;
border-radius: 4px;
overflow: auto;
font-family: monospace;
white-space: pre-wrap;
}
.executor-actions {
display: flex;
justify-content: flex-end;
}
/* Status colors for table rows */
tr.status-new {
border-left: 4px solid #3498db;
}
tr.status-active {
border-left: 4px solid #2ecc71;
}
tr.status-done {
border-left: 4px solid #27ae60;
}
tr.status-error {
border-left: 4px solid #e74c3c;
}

View File

@@ -0,0 +1,174 @@
// Main JavaScript file for Hero Agent UI
document.addEventListener('DOMContentLoaded', function() {
console.log('Hero Agent UI loaded');
// Fetch latest data for the page if needed
refreshPageData();
// Set up auto-refresh for dashboard if on that page
if (window.location.pathname === '/') {
setInterval(refreshDashboard, 10000); // Refresh every 10 seconds
}
});
// Refresh data based on current page
function refreshPageData() {
const path = window.location.pathname;
if (path === '/') {
refreshDashboard();
} else if (path === '/processes') {
refreshProcesses();
} else if (path.startsWith('/processes/')) {
const pid = path.split('/').pop();
refreshProcessDetails(pid);
} else if (path === '/jobs') {
refreshJobs();
} else if (path.startsWith('/jobs/')) {
const jobId = path.split('/').pop();
refreshJobDetails(jobId);
} else if (path === '/openrpc') {
refreshOpenRPCSpecs();
} else if (path.startsWith('/openrpc/')) {
const specName = path.split('/').pop();
refreshOpenRPCSpecDetails(specName);
}
}
// Fetch and update dashboard data
function refreshDashboard() {
// Fetch system stats
fetch('/api/stats')
.then(response => response.json())
.then(data => {
console.log('Dashboard stats refreshed');
// In a real implementation, we would update the DOM here
// For now, this is just a placeholder
updateStatBars(data);
})
.catch(error => {
console.error('Error refreshing dashboard stats:', error);
});
// Fetch processes
fetch('/api/processes')
.then(response => response.json())
.then(data => {
console.log('Dashboard processes refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing dashboard processes:', error);
});
// Fetch jobs
fetch('/api/jobs')
.then(response => response.json())
.then(data => {
console.log('Dashboard jobs refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing dashboard jobs:', error);
});
}
// Update stat bars with new data
function updateStatBars(stats) {
// This is a placeholder function that would update the stat bars
// with the new data from the API
if (!stats) return;
const cpuBar = document.querySelector('.stat-fill[data-stat="cpu"]');
const memoryBar = document.querySelector('.stat-fill[data-stat="memory"]');
const diskBar = document.querySelector('.stat-fill[data-stat="disk"]');
if (cpuBar) cpuBar.style.width = `${stats.cpu_usage}%`;
if (memoryBar) memoryBar.style.width = `${stats.memory_usage}%`;
if (diskBar) diskBar.style.width = `${stats.disk_usage}%`;
}
// Fetch and update processes list
function refreshProcesses() {
fetch('/api/processes')
.then(response => response.json())
.then(data => {
console.log('Processes refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing processes:', error);
});
}
// Fetch and update process details
function refreshProcessDetails(pid) {
fetch(`/api/processes/${pid}`)
.then(response => response.json())
.then(data => {
console.log('Process details refreshed');
// In a real implementation, we would update the DOM here
// Update CPU usage bar
const cpuBar = document.querySelector('.detail-fill');
if (cpuBar && data.cpu) {
cpuBar.style.width = `${data.cpu}%`;
}
})
.catch(error => {
console.error('Error refreshing process details:', error);
});
}
// Fetch and update jobs list
function refreshJobs() {
fetch('/api/jobs')
.then(response => response.json())
.then(data => {
console.log('Jobs refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing jobs:', error);
});
}
// Fetch and update job details
function refreshJobDetails(jobId) {
fetch(`/api/jobs/${jobId}`)
.then(response => response.json())
.then(data => {
console.log('Job details refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing job details:', error);
});
}
// Fetch and update OpenRPC specs
function refreshOpenRPCSpecs() {
fetch('/api/openrpc')
.then(response => response.json())
.then(data => {
console.log('OpenRPC specs refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing OpenRPC specs:', error);
});
}
// Fetch and update OpenRPC spec details
function refreshOpenRPCSpecDetails(specName) {
fetch(`/api/openrpc/${specName}`)
.then(response => response.json())
.then(data => {
console.log('OpenRPC spec details refreshed');
// In a real implementation, we would update the DOM here
})
.catch(error => {
console.error('Error refreshing OpenRPC spec details:', error);
});
}

View File

@@ -0,0 +1,100 @@
<html>
<head>
<title>Agent Details - Hero Agent UI</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>Agent Details</h1>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
</nav>
</header>
<main>
<div class="agent-details">
<h2>@agent.name</h2>
<div class="detail-section">
<div class="detail-item">
<span class="label">ID:</span>
<span class="value">@agent.id</span>
</div>
<div class="detail-item">
<span class="label">Type:</span>
<span class="value">@agent.type_</span>
</div>
<div class="detail-item">
<span class="label">Status:</span>
<span class="value status-@agent.status">@agent.status</span>
</div>
<div class="detail-item">
<span class="label">Created:</span>
<span class="value">@agent.created_at</span>
</div>
<div class="detail-item">
<span class="label">Last Active:</span>
<span class="value">@agent.last_active</span>
</div>
</div>
<div class="agent-actions">
<button class="btn btn-primary" onclick="startAgent('@agent.id')">Start</button>
<button class="btn btn-warning" onclick="restartAgent('@agent.id')">Restart</button>
<button class="btn btn-danger" onclick="stopAgent('@agent.id')">Stop</button>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
function startAgent(id) {
fetch(`/api/agents/${id}/start`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Agent started successfully');
location.reload();
} else {
alert('Failed to start agent');
}
});
}
function restartAgent(id) {
fetch(`/api/agents/${id}/restart`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Agent restarted successfully');
location.reload();
} else {
alert('Failed to restart agent');
}
});
}
function stopAgent(id) {
fetch(`/api/agents/${id}/stop`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Agent stopped successfully');
location.reload();
} else {
alert('Failed to stop agent');
}
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
<html>
<head>
<title>Dashboard - Hero Agent UI</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>System Dashboard</h1>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
</nav>
</header>
<main>
<div class="dashboard-stats">
<div class="stat-card">
<h3>CPU Usage</h3>
<div class="stat-value">@stats.cpu_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.cpu_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Memory Usage</h3>
<div class="stat-value">@stats.memory_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.memory_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Disk Usage</h3>
<div class="stat-value">@stats.disk_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.disk_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Uptime</h3>
<div class="stat-value">@stats.uptime</div>
</div>
<div class="stat-card">
<h3>Agent Count</h3>
<div class="stat-value">@stats.agent_count</div>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,134 @@
<html>
<head>
<title>Hero Agent UI - Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>Hero Agent Dashboard</h1>
<nav>
<a href="/" class="active">Dashboard</a>
<a href="/processes">Processes</a>
<a href="/jobs">Jobs</a>
<a href="/openrpc">OpenRPC</a>
</nav>
</header>
<main>
<div class="dashboard-overview">
<div class="stats-section">
<h2>System Statistics</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>CPU Usage</h3>
<div class="stat-value">@stats.cpu_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.cpu_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Memory Usage</h3>
<div class="stat-value">@stats.memory_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.memory_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Disk Usage</h3>
<div class="stat-value">@stats.disk_usage%</div>
<div class="stat-bar">
<div class="stat-fill" style="width: @stats.disk_usage%;"></div>
</div>
</div>
<div class="stat-card">
<h3>Uptime</h3>
<div class="stat-value">@stats.uptime</div>
</div>
</div>
</div>
<div class="processes-section">
<h2>Top Processes <a href="/processes" class="view-all">View All</a></h2>
<table class="data-table">
<thead>
<tr>
<th>PID</th>
<th>Name</th>
<th>CPU %</th>
<th>Memory (MB)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for process in processes
<tr>
<td>@process.pid</td>
<td>@process.name</td>
<td>@process.cpu%</td>
<td>@process.memory</td>
<td>
<a href="/processes/@process.pid" class="btn btn-small">Details</a>
<button onclick="killProcess(@process.pid)" class="btn btn-small btn-danger">Kill</button>
</td>
</tr>
@end
</tbody>
</table>
</div>
<div class="jobs-section">
<h2>Recent Jobs <a href="/jobs" class="view-all">View All</a></h2>
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Topic</th>
<th>Status</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for job in jobs
<tr class="status-@job.status">
<td>@job.job_id</td>
<td>@job.topic</td>
<td>@job.status</td>
<td>@job.duration</td>
<td>
<a href="/jobs/@job.job_id" class="btn btn-small">Details</a>
</td>
</tr>
@end
</tbody>
</table>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
function killProcess(pid) {
if (confirm('Are you sure you want to kill process ' + pid + '?')) {
fetch(`/api/processes/${pid}/kill`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Process killed successfully');
location.reload();
} else {
alert('Failed to kill process');
}
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,114 @@
<html>
<head>
<title>Hero Agent UI - Job Details</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>Job Details</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes">Processes</a>
<a href="/jobs" class="active">Jobs</a>
<a href="/openrpc">OpenRPC</a>
</nav>
</header>
<main>
<div class="job-details-container">
<div class="job-header status-@job.status">
<h2>Job #@job.job_id: @job.topic</h2>
<div class="job-status">
<span class="status-badge status-@job.status">@job.status</span>
</div>
</div>
<div class="detail-section">
<div class="detail-item">
<span class="label">Circle ID:</span>
<span class="value">@job.circle_id</span>
</div>
<div class="detail-item">
<span class="label">Session Key:</span>
<span class="value">@job.session_key</span>
</div>
<div class="detail-item">
<span class="label">Parameters Type:</span>
<span class="value">@job.params_type</span>
</div>
<div class="detail-item">
<span class="label">Scheduled Time:</span>
<span class="value">@job.time_scheduled</span>
</div>
<div class="detail-item">
<span class="label">Start Time:</span>
<span class="value">@job.time_start</span>
</div>
<div class="detail-item">
<span class="label">End Time:</span>
<span class="value">@job.time_end</span>
</div>
<div class="detail-item">
<span class="label">Duration:</span>
<span class="value">@job.duration</span>
</div>
@if job.has_error
<div class="detail-item error-item">
<span class="label">Error:</span>
<span class="value error-value">@job.error</span>
</div>
@end
</div>
<div class="job-timeline">
<h3>Job Timeline</h3>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-icon scheduled"></div>
<div class="timeline-content">
<h4>Scheduled</h4>
<p>@job.time_scheduled</p>
</div>
</div>
@if !job.time_start.str().is_blank()
<div class="timeline-item">
<div class="timeline-icon started"></div>
<div class="timeline-content">
<h4>Started</h4>
<p>@job.time_start</p>
</div>
</div>
@end
@if !job.time_end.str().is_blank()
<div class="timeline-item">
<div class="timeline-icon @if job.has_error { 'error' } else { 'completed' }"></div>
<div class="timeline-content">
<h4>@if job.has_error { 'Failed' } else { 'Completed' }</h4>
<p>@job.time_end</p>
@if job.has_error
<p class="error-message">@job.error</p>
@end
</div>
</div>
@end
</div>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,117 @@
<html>
<head>
<title>Hero Agent UI - Jobs</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>Job Management</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes">Processes</a>
<a href="/jobs" class="active">Jobs</a>
<a href="/openrpc">OpenRPC</a>
</nav>
</header>
<main>
<div class="jobs-container">
<div class="filter-controls">
<input type="text" id="job-search" placeholder="Search jobs..." onkeyup="filterJobs()">
<div class="filter-buttons">
<button class="filter-btn active" data-status="all" onclick="filterByStatus('all')">All</button>
<button class="filter-btn" data-status="new" onclick="filterByStatus('new')">New</button>
<button class="filter-btn" data-status="active" onclick="filterByStatus('active')">Active</button>
<button class="filter-btn" data-status="done" onclick="filterByStatus('done')">Done</button>
<button class="filter-btn" data-status="error" onclick="filterByStatus('error')">Error</button>
</div>
</div>
<table class="data-table" id="jobs-table">
<thead>
<tr>
<th>ID</th>
<th>Circle</th>
<th>Topic</th>
<th>Status</th>
<th>Scheduled</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for job in jobs
<tr class="status-@job.status" data-status="@job.status">
<td>@job.job_id</td>
<td>@job.circle_id</td>
<td>@job.topic</td>
<td>@job.status</td>
<td>@job.time_scheduled</td>
<td>@job.duration</td>
<td>
<a href="/jobs/@job.job_id" class="btn btn-small">Details</a>
</td>
</tr>
@end
</tbody>
</table>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
function filterJobs() {
const input = document.getElementById('job-search');
const filter = input.value.toUpperCase();
const table = document.getElementById('jobs-table');
const rows = table.getElementsByTagName('tr');
const activeStatus = document.querySelector('.filter-btn.active').dataset.status;
for (let i = 1; i < rows.length; i++) {
const topicCell = rows[i].getElementsByTagName('td')[2];
const circleCell = rows[i].getElementsByTagName('td')[1];
const statusMatch = activeStatus === 'all' || rows[i].dataset.status === activeStatus;
if (topicCell && circleCell) {
const topicValue = topicCell.textContent || topicCell.innerText;
const circleValue = circleCell.textContent || circleCell.innerText;
const textMatch = topicValue.toUpperCase().indexOf(filter) > -1 ||
circleValue.toUpperCase().indexOf(filter) > -1;
if (textMatch && statusMatch) {
rows[i].style.display = '';
} else {
rows[i].style.display = 'none';
}
}
}
}
function filterByStatus(status) {
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`.filter-btn[data-status="${status}"]`).classList.add('active');
// Filter table
const table = document.getElementById('jobs-table');
const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) {
if (status === 'all') {
rows[i].style.display = '';
} else if (rows[i].dataset.status === status) {
rows[i].style.display = '';
} else {
rows[i].style.display = 'none';
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<html>
<head>
<title>Hero Agent UI - OpenRPC</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>OpenRPC Specifications</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes">Processes</a>
<a href="/jobs">Jobs</a>
<a href="/openrpc" class="active">OpenRPC</a>
</nav>
</header>
<main>
<div class="openrpc-container">
<div class="specs-list">
<h2>Available Specifications</h2>
<div class="specs-grid">
@for spec in specs
<div class="spec-card">
<h3>@spec.title</h3>
<p class="spec-version">Version: @spec.version</p>
<p class="spec-description">@spec.description</p>
<p class="spec-methods"><strong>@spec.methods.len</strong> methods available</p>
<a href="/openrpc/@spec.title" class="btn">View Details</a>
</div>
@end
</div>
</div>
<div class="openrpc-info">
<h2>About OpenRPC</h2>
<p>
OpenRPC is a standard for defining JSON-RPC 2.0 APIs. It provides a machine-readable
specification format that allows for automatic documentation generation, client SDK
generation, and server implementation validation.
</p>
<p>
The Hero Agent system uses OpenRPC to define and document its APIs, making it easier
to interact with the system programmatically.
</p>
<h3>Key Benefits</h3>
<ul>
<li>Standardized API definitions</li>
<li>Automatic documentation generation</li>
<li>Client SDK generation</li>
<li>Server implementation validation</li>
<li>Improved developer experience</li>
</ul>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,121 @@
<html>
<head>
<title>Hero Agent UI - OpenRPC Spec</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>OpenRPC Specification</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes">Processes</a>
<a href="/jobs">Jobs</a>
<a href="/openrpc" class="active">OpenRPC</a>
</nav>
</header>
<main>
<div class="openrpc-spec-container">
<div class="spec-header">
<h2>@spec.title</h2>
<div class="spec-meta">
<span class="spec-version">Version: @spec.version</span>
</div>
</div>
<div class="spec-description">
<p>@spec.description</p>
</div>
<div class="methods-section">
<h3>Available Methods</h3>
<div class="methods-list">
@for method in methods
<div class="method-card">
<h4 class="method-name">@method.name</h4>
<p class="method-summary">@method.summary</p>
<p class="method-description">@method.description</p>
<button class="btn" onclick="showMethodExecutor('@method.name')">Execute Method</button>
</div>
@end
</div>
</div>
<div id="method-executor" class="method-executor hidden">
<h3>Method Executor</h3>
<div class="executor-content">
<div class="executor-header">
<h4 id="executor-method-name">Method Name</h4>
<button class="close-btn" onclick="hideMethodExecutor()">×</button>
</div>
<div class="executor-body">
<div class="params-section">
<h5>Parameters</h5>
<textarea id="params-editor" placeholder="Enter JSON parameters here..."></textarea>
</div>
<div class="executor-actions">
<button class="btn btn-primary" onclick="executeMethod()">Execute</button>
</div>
<div class="result-section">
<h5>Result</h5>
<pre id="result-display">Results will appear here...</pre>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
let currentMethod = '';
function showMethodExecutor(methodName) {
currentMethod = methodName;
document.getElementById('executor-method-name').textContent = methodName;
document.getElementById('method-executor').classList.remove('hidden');
document.getElementById('params-editor').value = '{\n \n}';
document.getElementById('result-display').textContent = 'Results will appear here...';
}
function hideMethodExecutor() {
document.getElementById('method-executor').classList.add('hidden');
}
function executeMethod() {
const params = document.getElementById('params-editor').value;
const specName = '@spec.title';
try {
// Validate JSON
JSON.parse(params);
// Execute the RPC call
fetch(`/api/openrpc/${specName}/${currentMethod}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: params
})
.then(response => response.json())
.then(data => {
document.getElementById('result-display').textContent = JSON.stringify(data, null, 2);
})
.catch(error => {
document.getElementById('result-display').textContent = `Error: ${error.message}`;
});
} catch (e) {
document.getElementById('result-display').textContent = `Invalid JSON: ${e.message}`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<html>
<head>
<title>Hero Agent UI - Process Details</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>Process Details</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes" class="active">Processes</a>
<a href="/jobs">Jobs</a>
<a href="/openrpc">OpenRPC</a>
</nav>
</header>
<main>
<div class="process-details-container">
<div class="process-header">
<h2>@process.name</h2>
<div class="process-actions">
<button onclick="killProcess('@process.pid')" class="btn btn-danger">Kill Process</button>
</div>
</div>
<div class="detail-section">
<div class="detail-item">
<span class="label">PID:</span>
<span class="value">@process.pid</span>
</div>
<div class="detail-item">
<span class="label">CPU Usage:</span>
<span class="value">@process.cpu%</span>
<div class="detail-bar">
<div class="detail-fill" style="width: @process.cpu%;"></div>
</div>
</div>
<div class="detail-item">
<span class="label">Memory Usage:</span>
<span class="value">@process.memory MB</span>
</div>
</div>
<div class="process-info">
<h3>Process Information</h3>
<div class="info-grid">
<div class="info-card">
<h4>CPU Usage Over Time</h4>
<div class="chart-placeholder">
<!-- In a real implementation, this would be a chart -->
<div class="placeholder-text">CPU usage chart would be displayed here</div>
</div>
</div>
<div class="info-card">
<h4>Memory Usage Over Time</h4>
<div class="chart-placeholder">
<!-- In a real implementation, this would be a chart -->
<div class="placeholder-text">Memory usage chart would be displayed here</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
function killProcess(pid) {
if (confirm('Are you sure you want to kill process ' + pid + '?')) {
fetch(`/api/processes/${pid}/kill`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Process killed successfully');
window.location.href = '/processes';
} else {
alert('Failed to kill process');
}
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<html>
<head>
<title>Hero Agent UI - Processes</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>System Processes</h1>
<nav>
<a href="/">Dashboard</a>
<a href="/processes" class="active">Processes</a>
<a href="/jobs">Jobs</a>
<a href="/openrpc">OpenRPC</a>
</nav>
</header>
<main>
<div class="processes-container">
<div class="filter-controls">
<input type="text" id="process-search" placeholder="Search processes..." onkeyup="filterProcesses()">
<div class="sort-controls">
<label>Sort by:</label>
<select id="sort-field" onchange="sortProcesses()">
<option value="pid">PID</option>
<option value="name">Name</option>
<option value="cpu">CPU Usage</option>
<option value="memory">Memory Usage</option>
</select>
<button id="sort-direction" onclick="toggleSortDirection()"></button>
</div>
</div>
<table class="data-table" id="processes-table">
<thead>
<tr>
<th>PID</th>
<th>Name</th>
<th>CPU %</th>
<th>Memory (MB)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for process in processes
<tr>
<td>@process.pid</td>
<td>@process.name</td>
<td>@process.cpu</td>
<td>@process.memory</td>
<td>
<a href="/processes/@process.pid" class="btn btn-small">Details</a>
<button onclick="killProcess('@process.pid')" class="btn btn-small btn-danger">Kill</button>
</td>
</tr>
@end
</tbody>
</table>
</div>
</main>
<footer>
<p>&copy; 2025 Hero Agent System</p>
</footer>
<script src="/static/js/main.js"></script>
<script>
function killProcess(pid) {
if (confirm('Are you sure you want to kill process ' + pid + '?')) {
fetch(`/api/processes/${pid}/kill`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Process killed successfully');
location.reload();
} else {
alert('Failed to kill process');
}
});
}
}
function filterProcesses() {
const input = document.getElementById('process-search');
const filter = input.value.toUpperCase();
const table = document.getElementById('processes-table');
const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) {
const nameCell = rows[i].getElementsByTagName('td')[1];
if (nameCell) {
const nameValue = nameCell.textContent || nameCell.innerText;
if (nameValue.toUpperCase().indexOf(filter) > -1) {
rows[i].style.display = '';
} else {
rows[i].style.display = 'none';
}
}
}
}
let sortAscending = true;
function toggleSortDirection() {
sortAscending = !sortAscending;
const button = document.getElementById('sort-direction');
button.textContent = sortAscending ? '↑' : '↓';
sortProcesses();
}
function sortProcesses() {
const table = document.getElementById('processes-table');
const rows = Array.from(table.getElementsByTagName('tr')).slice(1);
const sortField = document.getElementById('sort-field').value;
let columnIndex;
let isNumeric = false;
switch (sortField) {
case 'pid':
columnIndex = 0;
isNumeric = true;
break;
case 'name':
columnIndex = 1;
break;
case 'cpu':
columnIndex = 2;
isNumeric = true;
break;
case 'memory':
columnIndex = 3;
isNumeric = true;
break;
default:
columnIndex = 0;
isNumeric = true;
}
rows.sort((a, b) => {
const aValue = a.getElementsByTagName('td')[columnIndex].textContent;
const bValue = b.getElementsByTagName('td')[columnIndex].textContent;
if (isNumeric) {
return sortAscending
? parseFloat(aValue) - parseFloat(bValue)
: parseFloat(bValue) - parseFloat(aValue);
} else {
return sortAscending
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}
});
const tbody = table.getElementsByTagName('tbody')[0];
rows.forEach(row => tbody.appendChild(row));
}
</script>
</body>
</html>