diff --git a/examples/example.v b/examples/example.v new file mode 100644 index 0000000..720ffba --- /dev/null +++ b/examples/example.v @@ -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) diff --git a/herolib b/herolib new file mode 120000 index 0000000..9f9031a --- /dev/null +++ b/herolib @@ -0,0 +1 @@ +../../../github/freeflowuniverse/herolib \ No newline at end of file diff --git a/src/agentui/agentui.v b/src/agentui/agentui.v new file mode 100644 index 0000000..7107f79 --- /dev/null +++ b/src/agentui/agentui.v @@ -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) +} \ No newline at end of file diff --git a/src/agentui/controllers/job_ctrl.v b/src/agentui/controllers/job_ctrl.v new file mode 100644 index 0000000..0030f79 --- /dev/null +++ b/src/agentui/controllers/job_ctrl.v @@ -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 +} \ No newline at end of file diff --git a/src/agentui/controllers/process_ctrl.v b/src/agentui/controllers/process_ctrl.v new file mode 100644 index 0000000..2288852 --- /dev/null +++ b/src/agentui/controllers/process_ctrl.v @@ -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 +} diff --git a/src/agentui/controllers/rpc_ctrl.v b/src/agentui/controllers/rpc_ctrl.v new file mode 100644 index 0000000..fe3ffad --- /dev/null +++ b/src/agentui/controllers/rpc_ctrl.v @@ -0,0 +1,36 @@ +module controllers +// 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) ?OpenRPCSpec { + 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"}}' +} \ No newline at end of file diff --git a/src/agentui/controllers/system_controller.v b/src/agentui/controllers/system_controller.v new file mode 100644 index 0000000..0e52c3e --- /dev/null +++ b/src/agentui/controllers/system_controller.v @@ -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() +} diff --git a/src/agentui/models/jobs.v b/src/agentui/models/jobs.v new file mode 100644 index 0000000..01aa659 --- /dev/null +++ b/src/agentui/models/jobs.v @@ -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 + } + ] +} diff --git a/src/agentui/models/openrpc.v b/src/agentui/models/openrpc.v new file mode 100644 index 0000000..44bca6e --- /dev/null +++ b/src/agentui/models/openrpc.v @@ -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' + } + ] + } + ] +} diff --git a/src/agentui/models/process.v b/src/agentui/models/process.v new file mode 100644 index 0000000..eeb486c --- /dev/null +++ b/src/agentui/models/process.v @@ -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 + } + ] +} diff --git a/src/agentui/models/stats.v b/src/agentui/models/stats.v new file mode 100644 index 0000000..18a3a07 --- /dev/null +++ b/src/agentui/models/stats.v @@ -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 + } +} \ No newline at end of file diff --git a/src/agentui/static/css/style.css b/src/agentui/static/css/style.css new file mode 100644 index 0000000..912ab44 --- /dev/null +++ b/src/agentui/static/css/style.css @@ -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; +} \ No newline at end of file diff --git a/src/agentui/static/js/main.js b/src/agentui/static/js/main.js new file mode 100644 index 0000000..4992f99 --- /dev/null +++ b/src/agentui/static/js/main.js @@ -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); + }); +} \ No newline at end of file diff --git a/src/agentui/templates/agent_details.html b/src/agentui/templates/agent_details.html new file mode 100644 index 0000000..67125b4 --- /dev/null +++ b/src/agentui/templates/agent_details.html @@ -0,0 +1,100 @@ + + + Agent Details - Hero Agent UI + + + +
+

Agent Details

+ +
+ +
+
+

@agent.name

+ +
+
+ ID: + @agent.id +
+ +
+ Type: + @agent.type_ +
+ +
+ Status: + @agent.status +
+ +
+ Created: + @agent.created_at +
+ +
+ Last Active: + @agent.last_active +
+
+ +
+ + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/agentui/templates/dashboard.html b/src/agentui/templates/dashboard.html new file mode 100644 index 0000000..9148fcb --- /dev/null +++ b/src/agentui/templates/dashboard.html @@ -0,0 +1,59 @@ + + + Dashboard - Hero Agent UI + + + +
+

System Dashboard

+ +
+ +
+
+
+

CPU Usage

+
@stats.cpu_usage%
+
+
+
+
+ +
+

Memory Usage

+
@stats.memory_usage%
+
+
+
+
+ +
+

Disk Usage

+
@stats.disk_usage%
+
+
+
+
+ +
+

Uptime

+
@stats.uptime
+
+ +
+

Agent Count

+
@stats.agent_count
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/agentui/templates/index.html b/src/agentui/templates/index.html new file mode 100644 index 0000000..98213d5 --- /dev/null +++ b/src/agentui/templates/index.html @@ -0,0 +1,134 @@ + + + Hero Agent UI - Dashboard + + + +
+

Hero Agent Dashboard

+ +
+ +
+
+
+

System Statistics

+
+
+

CPU Usage

+
@stats.cpu_usage%
+
+
+
+
+ +
+

Memory Usage

+
@stats.memory_usage%
+
+
+
+
+ +
+

Disk Usage

+
@stats.disk_usage%
+
+
+
+
+ +
+

Uptime

+
@stats.uptime
+
+
+
+ +
+

Top Processes View All

+ + + + + + + + + + + + @for process in processes + + + + + + + + @end + +
PIDNameCPU %Memory (MB)Actions
@process.pid@process.name@process.cpu%@process.memory + Details + +
+
+ +
+

Recent Jobs View All

+ + + + + + + + + + + + @for job in jobs + + + + + + + + @end + +
IDTopicStatusDurationActions
@job.job_id@job.topic@job.status@job.duration + Details +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/agentui/templates/job_details.html b/src/agentui/templates/job_details.html new file mode 100644 index 0000000..19985ac --- /dev/null +++ b/src/agentui/templates/job_details.html @@ -0,0 +1,114 @@ + + + Hero Agent UI - Job Details + + + +
+

Job Details

+ +
+ +
+
+
+

Job #@job.job_id: @job.topic

+
+ @job.status +
+
+ +
+
+ Circle ID: + @job.circle_id +
+ +
+ Session Key: + @job.session_key +
+ +
+ Parameters Type: + @job.params_type +
+ +
+ Scheduled Time: + @job.time_scheduled +
+ +
+ Start Time: + @job.time_start +
+ +
+ End Time: + @job.time_end +
+ +
+ Duration: + @job.duration +
+ + @if job.has_error +
+ Error: + @job.error +
+ @end +
+ +
+

Job Timeline

+
+
+
+
+

Scheduled

+

@job.time_scheduled

+
+
+ + @if !job.time_start.str().is_blank() +
+
+
+

Started

+

@job.time_start

+
+
+ @end + + @if !job.time_end.str().is_blank() +
+
+
+

@if job.has_error { 'Failed' } else { 'Completed' }

+

@job.time_end

+ @if job.has_error +

@job.error

+ @end +
+
+ @end +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/agentui/templates/jobs.html b/src/agentui/templates/jobs.html new file mode 100644 index 0000000..0cce795 --- /dev/null +++ b/src/agentui/templates/jobs.html @@ -0,0 +1,117 @@ + + + Hero Agent UI - Jobs + + + +
+

Job Management

+ +
+ +
+
+
+ +
+ + + + + +
+
+ + + + + + + + + + + + + + + @for job in jobs + + + + + + + + + + @end + +
IDCircleTopicStatusScheduledDurationActions
@job.job_id@job.circle_id@job.topic@job.status@job.time_scheduled@job.duration + Details +
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/agentui/templates/openrpc.html b/src/agentui/templates/openrpc.html new file mode 100644 index 0000000..07f5319 --- /dev/null +++ b/src/agentui/templates/openrpc.html @@ -0,0 +1,63 @@ + + + Hero Agent UI - OpenRPC + + + +
+

OpenRPC Specifications

+ +
+ +
+
+
+

Available Specifications

+
+ @for spec in specs +
+

@spec.title

+

Version: @spec.version

+

@spec.description

+

@spec.methods.len methods available

+ View Details +
+ @end +
+
+ +
+

About OpenRPC

+

+ 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. +

+

+ The Hero Agent system uses OpenRPC to define and document its APIs, making it easier + to interact with the system programmatically. +

+

Key Benefits

+
    +
  • Standardized API definitions
  • +
  • Automatic documentation generation
  • +
  • Client SDK generation
  • +
  • Server implementation validation
  • +
  • Improved developer experience
  • +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/agentui/templates/openrpc_spec.html b/src/agentui/templates/openrpc_spec.html new file mode 100644 index 0000000..7a2b756 --- /dev/null +++ b/src/agentui/templates/openrpc_spec.html @@ -0,0 +1,121 @@ + + + Hero Agent UI - OpenRPC Spec + + + +
+

OpenRPC Specification

+ +
+ +
+
+
+

@spec.title

+
+ Version: @spec.version +
+
+ +
+

@spec.description

+
+ +
+

Available Methods

+
+ @for method in methods +
+

@method.name

+

@method.summary

+

@method.description

+ +
+ @end +
+
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/src/agentui/templates/process_details.html b/src/agentui/templates/process_details.html new file mode 100644 index 0000000..39a8c8f --- /dev/null +++ b/src/agentui/templates/process_details.html @@ -0,0 +1,91 @@ + + + Hero Agent UI - Process Details + + + +
+

Process Details

+ +
+ +
+
+
+

@process.name

+
+ +
+
+ +
+
+ PID: + @process.pid +
+ +
+ CPU Usage: + @process.cpu% +
+
+
+
+ +
+ Memory Usage: + @process.memory MB +
+
+ +
+

Process Information

+
+
+

CPU Usage Over Time

+
+ +
CPU usage chart would be displayed here
+
+
+ +
+

Memory Usage Over Time

+
+ +
Memory usage chart would be displayed here
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/agentui/templates/processes.html b/src/agentui/templates/processes.html new file mode 100644 index 0000000..ee72b9e --- /dev/null +++ b/src/agentui/templates/processes.html @@ -0,0 +1,159 @@ + + + Hero Agent UI - Processes + + + +
+

System Processes

+ +
+ +
+
+
+ +
+ + + +
+
+ + + + + + + + + + + + + @for process in processes + + + + + + + + @end + +
PIDNameCPU %Memory (MB)Actions
@process.pid@process.name@process.cpu@process.memory + Details + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/clients/zinit/README.md b/src/clients/zinit/README.md new file mode 100644 index 0000000..2aee592 --- /dev/null +++ b/src/clients/zinit/README.md @@ -0,0 +1,406 @@ +# Zinit Client Module + +A well-documented V (Vlang) client library for interacting with the Zinit service manager via JSON-RPC over Unix socket. + +## Overview + +This module provides a complete client implementation for the Zinit JSON-RPC API, allowing you to manage services, monitor system resources, and control the Zinit service manager programmatically. + +## Features + +- **Service Management**: Start, stop, monitor, and forget services +- **Service Configuration**: Create, delete, and retrieve service configurations +- **System Control**: Shutdown, reboot, and HTTP server management +- **Monitoring**: Get service status, statistics, and resource usage +- **Logging**: Stream current logs and subscribe to log updates +- **Error Handling**: Comprehensive error handling with detailed error messages +- **Type Safety**: Strongly typed structs for all API responses +- **Connection Management**: Automatic connection handling with proper cleanup + +## Installation + +Simply import the module in your V project: + +```v +import zinit +``` + +## Quick Start + +```v +import zinit + +fn main() { + // Create a client with default socket path (/tmp/zinit.sock) + mut client := zinit.new_default_client() + + // Or specify a custom socket path + // mut client := zinit.new_client('/custom/path/to/zinit.sock') + + defer { + client.disconnect() + } + + // List all services + services := client.service_list() or { + eprintln('Error: ${err}') + return + } + + for name, state in services { + println('${name}: ${state}') + } +} +``` + +## API Reference + +### Client Creation + +#### `new_client(socket_path string) &Client` +Creates a new zinit client with a custom socket path. + +#### `new_default_client() &Client` +Creates a new zinit client with the default socket path (`/tmp/zinit.sock`). + +### Connection Management + +#### `connect() !` +Establishes a connection to the zinit Unix socket. Called automatically by API methods. + +#### `disconnect()` +Closes the connection to the zinit Unix socket. Should be called when done with the client. + +### Service Management + +#### `service_list() !map[string]string` +Lists all services managed by Zinit. + +**Returns**: A map of service names to their current states. + +```v +services := client.service_list()! +for name, state in services { + println('${name}: ${state}') +} +``` + +#### `service_status(name string) !ServiceStatus` +Shows detailed status information for a specific service. + +**Parameters**: +- `name`: The name of the service + +**Returns**: `ServiceStatus` struct with detailed information. + +```v +status := client.service_status('redis')! +println('Service: ${status.name}') +println('State: ${status.state}') +println('PID: ${status.pid}') +``` + +#### `service_start(name string) !` +Starts a service. + +**Parameters**: +- `name`: The name of the service to start + +#### `service_stop(name string) !` +Stops a service. + +**Parameters**: +- `name`: The name of the service to stop + +#### `service_monitor(name string) !` +Starts monitoring a service. The service configuration is loaded from the config directory. + +**Parameters**: +- `name`: The name of the service to monitor + +#### `service_forget(name string) !` +Stops monitoring a service. You can only forget a stopped service. + +**Parameters**: +- `name`: The name of the service to forget + +#### `service_kill(name string, signal string) !` +Sends a signal to a running service. + +**Parameters**: +- `name`: The name of the service to send the signal to +- `signal`: The signal to send (e.g., "SIGTERM", "SIGKILL") + +### Service Configuration + +#### `service_create(name string, config ServiceConfig) !string` +Creates a new service configuration file. + +**Parameters**: +- `name`: The name of the service to create +- `config`: The service configuration + +**Returns**: Result message. + +```v +config := zinit.ServiceConfig{ + exec: '/usr/bin/redis-server' + oneshot: false + log: 'stdout' + env: { + 'REDIS_PORT': '6379' + } + shutdown_timeout: 30 +} + +result := client.service_create('redis', config)! +println('Service created: ${result}') +``` + +#### `service_delete(name string) !string` +Deletes a service configuration file. + +**Parameters**: +- `name`: The name of the service to delete + +**Returns**: Result message. + +#### `service_get(name string) !ServiceConfig` +Gets a service configuration file. + +**Parameters**: +- `name`: The name of the service to get + +**Returns**: `ServiceConfig` struct with the service configuration. + +### Service Statistics + +#### `service_stats(name string) !ServiceStats` +Gets memory and CPU usage statistics for a service. + +**Parameters**: +- `name`: The name of the service to get stats for + +**Returns**: `ServiceStats` struct with usage information. + +```v +stats := client.service_stats('redis')! +println('Memory Usage: ${stats.memory_usage / 1024 / 1024} MB') +println('CPU Usage: ${stats.cpu_usage}%') +``` + +### System Operations + +#### `system_shutdown() !` +Stops all services and powers off the system. + +⚠️ **Warning**: This will actually shut down the system! + +#### `system_reboot() !` +Stops all services and reboots the system. + +⚠️ **Warning**: This will actually reboot the system! + +#### `system_start_http_server(address string) !string` +Starts an HTTP/RPC server at the specified address. + +**Parameters**: +- `address`: The network address to bind the server to (e.g., "127.0.0.1:8080") + +**Returns**: Result message. + +#### `system_stop_http_server() !` +Stops the HTTP/RPC server if running. + +### Logging + +#### `stream_current_logs(name ?string) ![]string` +Gets current logs from zinit and monitored services. + +**Parameters**: +- `name`: Optional service name filter. If provided, only logs from this service will be returned. + +**Returns**: Array of log strings. + +```v +// Get all logs +all_logs := client.stream_current_logs(none)! + +// Get logs for a specific service +redis_logs := client.stream_current_logs('redis')! +``` + +#### `stream_subscribe_logs(name ?string) !string` +Subscribes to log messages generated by zinit and monitored services. + +**Parameters**: +- `name`: Optional service name filter. + +**Returns**: A single log message. + +**Note**: For continuous streaming, call this method repeatedly. + +### API Discovery + +#### `rpc_discover() !map[string]interface{}` +Returns the OpenRPC specification for the API. + +**Returns**: The complete OpenRPC specification as a map. + +## Data Types + +### ServiceConfig +Represents the configuration for a zinit service. + +```v +struct ServiceConfig { +pub mut: + exec string // Command to run + oneshot bool // Whether the service should be restarted + after []string // Services that must be running before this one starts + log string // How to handle service output (null, ring, stdout) + env map[string]string // Environment variables for the service + shutdown_timeout int // Maximum time to wait for service to stop during shutdown +} +``` + +### ServiceStatus +Represents the detailed status information for a service. + +```v +struct ServiceStatus { +pub mut: + name string // Service name + pid int // Process ID of the running service (if running) + state string // Current state of the service (Running, Success, Error, etc.) + target string // Target state of the service (Up, Down) + after map[string]string // Dependencies of the service and their states +} +``` + +### ServiceStats +Represents memory and CPU usage statistics for a service. + +```v +struct ServiceStats { +pub mut: + name string // Service name + pid int // Process ID of the service + memory_usage i64 // Memory usage in bytes + cpu_usage f64 // CPU usage as a percentage (0-100) + children []ChildStats // Stats for child processes +} +``` + +### ChildStats +Represents statistics for a child process. + +```v +struct ChildStats { +pub mut: + pid int // Process ID of the child process + memory_usage i64 // Memory usage in bytes + cpu_usage f64 // CPU usage as a percentage (0-100) +} +``` + +## Error Handling + +The client provides comprehensive error handling through V's error system. All API methods that can fail return a result type (`!`). + +### ZinitError +Custom error type for zinit-specific errors. + +```v +struct ZinitError { +pub mut: + code int // Error code + message string // Error message + data string // Additional error data +} +``` + +Common error codes: +- `-32000`: Service not found +- `-32001`: Service already monitored +- `-32002`: Service is up +- `-32003`: Service is down +- `-32004`: Invalid signal +- `-32005`: Config error +- `-32006`: Shutting down +- `-32007`: Service already exists +- `-32008`: Service file error + +## Examples + +See `example.v` for comprehensive usage examples covering all API methods. + +### Basic Service Management + +```v +import zinit + +fn manage_service() ! { + mut client := zinit.new_default_client() + defer { client.disconnect() } + + // Create a service + config := zinit.ServiceConfig{ + exec: '/usr/bin/nginx' + oneshot: false + log: 'stdout' + after: ['network'] + } + + client.service_create('nginx', config)! + client.service_monitor('nginx')! + client.service_start('nginx')! + + // Check status + status := client.service_status('nginx')! + println('Nginx is ${status.state}') + + // Get statistics + stats := client.service_stats('nginx')! + println('Memory: ${stats.memory_usage / 1024 / 1024} MB') +} +``` + +### Log Monitoring + +```v +import zinit +import time + +fn monitor_logs() ! { + mut client := zinit.new_default_client() + defer { client.disconnect() } + + // Get current logs + logs := client.stream_current_logs(none)! + for log in logs { + println(log) + } + + // Subscribe to new logs (simplified example) + for i in 0..10 { + log_entry := client.stream_subscribe_logs(none) or { continue } + println('New log: ${log_entry}') + time.sleep(1 * time.second) + } +} +``` + +## Thread Safety + +The client is not thread-safe. If you need to use it from multiple threads, you should create separate client instances or implement your own synchronization. + +## Requirements + +- V compiler +- Unix-like operating system (for Unix socket support) +- Running Zinit service manager + +## License + +This module follows the same license as the parent project. diff --git a/src/clients/zinit/client.v b/src/clients/zinit/client.v new file mode 100644 index 0000000..7ca54e3 --- /dev/null +++ b/src/clients/zinit/client.v @@ -0,0 +1,211 @@ +module zinit + +import net.unix +import json +import time + +// service_status shows detailed status information for a specific service +// name: the name of the service +pub fn (mut c Client) service_status(name string) !ServiceStatus { + params := [name] + response := c.send_request('service_status', params)! + + result_map := response.result as map[string]interface{} + + mut after_map := map[string]string{} + if after_raw := result_map['after'] { + if after_obj := after_raw as map[string]interface{} { + for key, value in after_obj { + after_map[key] = value.str() + } + } + } + + return ServiceStatus{ + name: result_map['name'] or { '' }.str() + pid: result_map['pid'] or { 0 }.int() + state: result_map['state'] or { '' }.str() + target: result_map['target'] or { '' }.str() + after: after_map + } +} + +// service_start starts a service +// name: the name of the service to start +pub fn (mut c Client) service_start(name string) ! { + params := [name] + c.send_request('service_start', params)! +} + +// service_stop stops a service +// name: the name of the service to stop +pub fn (mut c Client) service_stop(name string) ! { + params := [name] + c.send_request('service_stop', params)! +} + +// service_monitor starts monitoring a service +// The service configuration is loaded from the config directory +// name: the name of the service to monitor +pub fn (mut c Client) service_monitor(name string) ! { + params := [name] + c.send_request('service_monitor', params)! +} + +// service_forget stops monitoring a service +// You can only forget a stopped service +// name: the name of the service to forget +pub fn (mut c Client) service_forget(name string) ! { + params := [name] + c.send_request('service_forget', params)! +} + +// service_kill sends a signal to a running service +// name: the name of the service to send the signal to +// signal: the signal to send (e.g., SIGTERM, SIGKILL) +pub fn (mut c Client) service_kill(name string, signal string) ! { + params := [name, signal] + c.send_request('service_kill', params)! +} + +// service_create creates a new service configuration file +// name: the name of the service to create +// config: the service configuration +pub fn (mut c Client) service_create(name string, config ServiceConfig) !string { + params := [name, config] + response := c.send_request('service_create', params)! + return response.result.str() +} + +// service_delete deletes a service configuration file +// name: the name of the service to delete +pub fn (mut c Client) service_delete(name string) !string { + params := [name] + response := c.send_request('service_delete', params)! + return response.result.str() +} + +// service_get gets a service configuration file +// name: the name of the service to get +pub fn (mut c Client) service_get(name string) !ServiceConfig { + params := [name] + response := c.send_request('service_get', params)! + + result_map := response.result as map[string]interface{} + + mut after_list := []string{} + if after_raw := result_map['after'] { + if after_array := after_raw as []interface{} { + for item in after_array { + after_list << item.str() + } + } + } + + mut env_map := map[string]string{} + if env_raw := result_map['env'] { + if env_obj := env_raw as map[string]interface{} { + for key, value in env_obj { + env_map[key] = value.str() + } + } + } + + return ServiceConfig{ + exec: result_map['exec'] or { '' }.str() + oneshot: result_map['oneshot'] or { false }.bool() + after: after_list + log: result_map['log'] or { '' }.str() + env: env_map + shutdown_timeout: result_map['shutdown_timeout'] or { 0 }.int() + } +} + +// service_stats gets memory and CPU usage statistics for a service +// name: the name of the service to get stats for +pub fn (mut c Client) service_stats(name string) !ServiceStats { + params := [name] + response := c.send_request('service_stats', params)! + + result_map := response.result as map[string]interface{} + + mut children_list := []ChildStats{} + if children_raw := result_map['children'] { + if children_array := children_raw as []interface{} { + for child_raw in children_array { + if child_map := child_raw as map[string]interface{} { + children_list << ChildStats{ + pid: child_map['pid'] or { 0 }.int() + memory_usage: child_map['memory_usage'] or { i64(0) }.i64() + cpu_usage: child_map['cpu_usage'] or { 0.0 }.f64() + } + } + } + } + } + + return ServiceStats{ + name: result_map['name'] or { '' }.str() + pid: result_map['pid'] or { 0 }.int() + memory_usage: result_map['memory_usage'] or { i64(0) }.i64() + cpu_usage: result_map['cpu_usage'] or { 0.0 }.f64() + children: children_list + } +} + +// system_shutdown stops all services and powers off the system +pub fn (mut c Client) system_shutdown() ! { + c.send_request('system_shutdown', []interface{})! +} + +// system_reboot stops all services and reboots the system +pub fn (mut c Client) system_reboot() ! { + c.send_request('system_reboot', []interface{})! +} + +// system_start_http_server starts an HTTP/RPC server at the specified address +// address: the network address to bind the server to (e.g., '127.0.0.1:8080') +pub fn (mut c Client) system_start_http_server(address string) !string { + params := [address] + response := c.send_request('system_start_http_server', params)! + return response.result.str() +} + +// system_stop_http_server stops the HTTP/RPC server if running +pub fn (mut c Client) system_stop_http_server() ! { + c.send_request('system_stop_http_server', []interface{})! +} + +// stream_current_logs gets current logs from zinit and monitored services +// name: optional service name filter. If provided, only logs from this service will be returned +pub fn (mut c Client) stream_current_logs(name ?string) ![]string { + mut params := []interface{} + if service_name := name { + params << service_name + } + + response := c.send_request('stream_currentLogs', params)! + + if logs_array := response.result as []interface{} { + mut logs := []string{} + for log_entry in logs_array { + logs << log_entry.str() + } + return logs + } + + return []string{} +} + +// stream_subscribe_logs subscribes to log messages generated by zinit and monitored services +// name: optional service name filter. If provided, only logs from this service will be returned +// Note: This method returns a single log message. For continuous streaming, call this method repeatedly +pub fn (mut c Client) stream_subscribe_logs(name ?string) !string { + mut params := []interface{} + if service_name := name { + params << service_name + } + + response := c.send_request('stream_subscribeLogs', params)! + return response.result.str() +} diff --git a/src/clients/zinit/openrpc.json b/src/clients/zinit/openrpc.json new file mode 100644 index 0000000..65cc7af --- /dev/null +++ b/src/clients/zinit/openrpc.json @@ -0,0 +1,873 @@ +{ + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API", + "description": "JSON-RPC 2.0 API for controlling and querying Zinit services", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "name": "Unix Socket", + "url": "unix:///tmp/zinit.sock" + } + ], + "methods": [ + { + "name": "rpc_discover", + "description": "Returns the OpenRPC specification for the API", + "params": [], + "result": { + "name": "OpenRPCSpec", + "description": "The OpenRPC specification", + "schema": { + "type": "object" + } + }, + "examples": [ + { + "name": "Get API specification", + "params": [], + "result": { + "name": "OpenRPCSpecResult", + "value": { + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API" + } + } + } + } + ] + }, + { + "name": "service_list", + "description": "Lists all services managed by Zinit", + "params": [], + "result": { + "name": "ServiceList", + "description": "A map of service names to their current states", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Service state (Running, Success, Error, etc.)" + } + } + }, + "examples": [ + { + "name": "List all services", + "params": [], + "result": { + "name": "ServiceListResult", + "value": { + "service1": "Running", + "service2": "Success", + "service3": "Error" + } + } + } + ] + }, + { + "name": "service_status", + "description": "Shows detailed status information for a specific service", + "params": [ + { + "name": "name", + "description": "The name of the service", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStatus", + "description": "Detailed status information for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the running service (if running)" + }, + "state": { + "type": "string", + "description": "Current state of the service (Running, Success, Error, etc.)" + }, + "target": { + "type": "string", + "description": "Target state of the service (Up, Down)" + }, + "after": { + "type": "object", + "description": "Dependencies of the service and their states", + "additionalProperties": { + "type": "string", + "description": "State of the dependency" + } + } + } + } + }, + "examples": [ + { + "name": "Get status of redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatusResult", + "value": { + "name": "redis", + "pid": 1234, + "state": "Running", + "target": "Up", + "after": { + "dependency1": "Success", + "dependency2": "Running" + } + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_start", + "description": "Starts a service", + "params": [ + { + "name": "name", + "description": "The name of the service to start", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartResult", + "description": "Result of the start operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Start redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StartResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_stop", + "description": "Stops a service", + "params": [ + { + "name": "name", + "description": "The name of the service to stop", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StopResult", + "description": "Result of the stop operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StopResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "service_monitor", + "description": "Starts monitoring a service. The service configuration is loaded from the config directory.", + "params": [ + { + "name": "name", + "description": "The name of the service to monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "MonitorResult", + "description": "Result of the monitor operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Monitor redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "MonitorResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32001, + "message": "Service already monitored", + "data": "service \"redis\" already monitored" + }, + { + "code": -32005, + "message": "Config error", + "data": "failed to load service configuration" + } + ] + }, + { + "name": "service_forget", + "description": "Stops monitoring a service. You can only forget a stopped service.", + "params": [ + { + "name": "name", + "description": "The name of the service to forget", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ForgetResult", + "description": "Result of the forget operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Forget redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ForgetResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32002, + "message": "Service is up", + "data": "service \"redis\" is up" + } + ] + }, + { + "name": "service_kill", + "description": "Sends a signal to a running service", + "params": [ + { + "name": "name", + "description": "The name of the service to send the signal to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "signal", + "description": "The signal to send (e.g., SIGTERM, SIGKILL)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "KillResult", + "description": "Result of the kill operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Send SIGTERM to redis service", + "params": [ + { + "name": "name", + "value": "redis" + }, + { + "name": "signal", + "value": "SIGTERM" + } + ], + "result": { + "name": "KillResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + }, + { + "code": -32004, + "message": "Invalid signal", + "data": "invalid signal: INVALID" + } + ] + }, + { + "name": "system_shutdown", + "description": "Stops all services and powers off the system", + "params": [], + "result": { + "name": "ShutdownResult", + "description": "Result of the shutdown operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Shutdown the system", + "params": [], + "result": { + "name": "ShutdownResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "system_reboot", + "description": "Stops all services and reboots the system", + "params": [], + "result": { + "name": "RebootResult", + "description": "Result of the reboot operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Reboot the system", + "params": [], + "result": { + "name": "RebootResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "service_create", + "description": "Creates a new service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to create", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "content", + "description": "The service configuration content", + "required": true, + "schema": { + "type": "object", + "properties": { + "exec": { + "type": "string", + "description": "Command to run" + }, + "oneshot": { + "type": "boolean", + "description": "Whether the service should be restarted" + }, + "after": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Services that must be running before this one starts" + }, + "log": { + "type": "string", + "enum": ["null", "ring", "stdout"], + "description": "How to handle service output" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables for the service" + }, + "shutdown_timeout": { + "type": "integer", + "description": "Maximum time to wait for service to stop during shutdown" + } + } + } + } + ], + "result": { + "name": "CreateServiceResult", + "description": "Result of the create operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32007, + "message": "Service already exists", + "data": "Service 'name' already exists" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to create service file" + } + ] + }, + { + "name": "service_delete", + "description": "Deletes a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to delete", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "DeleteServiceResult", + "description": "Result of the delete operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to delete service file" + } + ] + }, + { + "name": "service_get", + "description": "Gets a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to get", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "GetServiceResult", + "description": "The service configuration", + "schema": { + "type": "object" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to read service file" + } + ] + }, + { + "name": "service_stats", + "description": "Get memory and CPU usage statistics for a service", + "params": [ + { + "name": "name", + "description": "The name of the service to get stats for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStats", + "description": "Memory and CPU usage statistics for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the service" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + }, + "children": { + "type": "array", + "description": "Stats for child processes", + "items": { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "description": "Process ID of the child process" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + } + } + } + } + } + } + }, + "examples": [ + { + "name": "Get stats for redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatsResult", + "value": { + "name": "redis", + "pid": 1234, + "memory_usage": 10485760, + "cpu_usage": 2.5, + "children": [ + { + "pid": 1235, + "memory_usage": 5242880, + "cpu_usage": 1.2 + } + ] + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "system_start_http_server", + "description": "Start an HTTP/RPC server at the specified address", + "params": [ + { + "name": "address", + "description": "The network address to bind the server to (e.g., '127.0.0.1:8080')", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartHttpServerResult", + "description": "Result of the start HTTP server operation", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Start HTTP server on localhost:8080", + "params": [ + { + "name": "address", + "value": "127.0.0.1:8080" + } + ], + "result": { + "name": "StartHttpServerResult", + "value": "HTTP server started at 127.0.0.1:8080" + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Invalid address", + "data": "Invalid network address format" + } + ] + }, + { + "name": "system_stop_http_server", + "description": "Stop the HTTP/RPC server if running", + "params": [], + "result": { + "name": "StopHttpServerResult", + "description": "Result of the stop HTTP server operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop the HTTP server", + "params": [], + "result": { + "name": "StopHttpServerResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Server not running", + "data": "No HTTP server is currently running" + } + ] + }, + { + "name": "stream_currentLogs", + "description": "Get current logs from zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogsResult", + "description": "Array of log strings", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": [ + { + "name": "Get all logs", + "params": [], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:01 nginx: Starting service" + ] + } + }, + { + "name": "Get logs for a specific service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:02 redis: Service started" + ] + } + } + ] + }, + { + "name": "stream_subscribeLogs", + "description": "Subscribe to log messages generated by zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogSubscription", + "description": "A subscription to log messages", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Subscribe to all logs", + "params": [], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + }, + { + "name": "Subscribe to filtered logs", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/clients/zinit/openrpc.v b/src/clients/zinit/openrpc.v new file mode 100644 index 0000000..0172eb0 --- /dev/null +++ b/src/clients/zinit/openrpc.v @@ -0,0 +1,161 @@ +module zinit + +import freeflowuniverse.herolib.schemas.jsonrpc +import net.unix +import time +import json + +// UnixSocketTransport implements the jsonrpc.IRPCTransportClient interface for Unix domain sockets +struct UnixSocketTransport { +mut: + socket_path string +} + +// new_unix_socket_transport creates a new Unix socket transport +fn new_unix_socket_transport(socket_path string) &UnixSocketTransport { + return &UnixSocketTransport{ + socket_path: socket_path + } +} + +// send implements the jsonrpc.IRPCTransportClient interface +fn (mut t UnixSocketTransport) send(request string, params jsonrpc.SendParams) !string { + // Create a Unix domain socket client + mut socket := unix.connect_stream(t.socket_path)! + defer { socket.close() or {} } + + // Set timeout if specified + if params.timeout > 0 { + socket.set_read_timeout(params.timeout * time.second) + socket.set_write_timeout(params.timeout * time.second) + } + + // Send the request + socket.write_string(request + '\n')! + + // Read the response + mut response := '' + mut buf := []u8{len: 4096} + + for { + bytes_read := socket.read(mut buf)! + if bytes_read <= 0 { + break + } + response += buf[..bytes_read].bytestr() + + // Check if we've received a complete JSON response + if response.ends_with('}') { + break + } + } + + return response +} + +// Client provides a client interface to the zinit JSON-RPC API over Unix socket +@[heap] +pub struct Client { +mut: + socket_path string + rpc_client &jsonrpc.Client + request_id int +} + +// new_client creates a new zinit client instance +// socket_path: path to the Unix socket (default: /tmp/zinit.sock) +pub fn new_client(socket_path string) &Client { + mut transport := new_unix_socket_transport(socket_path) + mut rpc_client := jsonrpc.new_client(transport) + + return &Client{ + socket_path: socket_path + rpc_client: rpc_client + request_id: 0 + } +} + +// new_default_client creates a new zinit client with default socket path +pub fn new_default_client() &Client { + return new_client('/tmp/zinit.sock') +} + +// rpc_discover returns the OpenRPC specification for the API +pub fn (mut c Client) rpc_discover() !map[string]json.Any { + send_params := jsonrpc.SendParams{ + timeout: 30 + retry: 1 + } + + request := jsonrpc.new_request_generic('rpc_discover', []string{}) + result := c.rpc_client.send[[]string, map[string]json.Any](request, send_params)! + + return result +} + +// service_list lists all services managed by Zinit +// Returns a map of service names to their current states +pub fn (mut c Client) service_list() !map[string]string { + send_params := jsonrpc.SendParams{ + timeout: 30 + retry: 1 + } + + request := jsonrpc.new_request_generic('service_list', []string{}) + result := c.rpc_client.send[[]string, map[string]string](request, send_params)! + + return result +} + +// service_status shows detailed status information for a specific service +// name: the name of the service +pub fn (mut c Client) service_status(name string) !ServiceStatus { + send_params := jsonrpc.SendParams{ + timeout: 30 + retry: 1 + } + + request := jsonrpc.new_request_generic('service_status', [name]) + result_map := c.rpc_client.send[[string], map[string]json.Any](request, send_params)! + + mut after_map := map[string]string{} + if after_raw := result_map['after'] { + if after_raw is map[string]json.Any { + for key, value in after_raw { + after_map[key] = value.str() + } + } + } + + return ServiceStatus{ + name: result_map['name'].str() + pid: result_map['pid'].int() + state: result_map['state'].str() + target: result_map['target'].str() + after: after_map + } +} + +// service_start starts a service +// name: the name of the service to start +pub fn (mut c Client) service_start(name string) ! { + send_params := jsonrpc.SendParams{ + timeout: 30 + retry: 1 + } + + request := jsonrpc.new_request_generic('service_start', [name]) + c.rpc_client.send[[string], json.Any](request, send_params)! +} + +// service_stop stops a service +// name: the name of the service to stop +pub fn (mut c Client) service_stop(name string) ! { + send_params := jsonrpc.SendParams{ + timeout: 30 + retry: 1 + } + + request := jsonrpc.new_request_generic('service_stop', [name]) + c.rpc_client.send[[string], json.Any](request, send_params)! +} diff --git a/src/clients/zinit/test.v b/src/clients/zinit/test.v new file mode 100644 index 0000000..299e9d3 --- /dev/null +++ b/src/clients/zinit/test.v @@ -0,0 +1,204 @@ +module zinit + +import time + +// Basic tests for the zinit client module +// Note: These tests require a running zinit instance at /tmp/zinit.sock + +// test_client_creation tests basic client creation +fn test_client_creation() { + // Test default client creation + client1 := new_default_client() + assert client1.socket_path == '/tmp/zinit.sock' + + // Test custom client creation + client2 := new_client('/custom/path/zinit.sock') + assert client2.socket_path == '/custom/path/zinit.sock' + + println('✓ Client creation tests passed') +} + +// test_service_config tests ServiceConfig struct +fn test_service_config() { + config := ServiceConfig{ + exec: '/usr/bin/test' + oneshot: true + after: ['dependency1', 'dependency2'] + log: 'stdout' + env: { + 'TEST_VAR': 'test_value' + 'PATH': '/usr/bin:/bin' + } + shutdown_timeout: 30 + } + + assert config.exec == '/usr/bin/test' + assert config.oneshot == true + assert config.after.len == 2 + assert config.after[0] == 'dependency1' + assert config.env['TEST_VAR'] == 'test_value' + assert config.shutdown_timeout == 30 + + println('✓ ServiceConfig tests passed') +} + +// test_service_status tests ServiceStatus struct +fn test_service_status() { + mut after_map := map[string]string{} + after_map['dep1'] = 'Running' + after_map['dep2'] = 'Success' + + status := ServiceStatus{ + name: 'test_service' + pid: 1234 + state: 'Running' + target: 'Up' + after: after_map + } + + assert status.name == 'test_service' + assert status.pid == 1234 + assert status.state == 'Running' + assert status.target == 'Up' + assert status.after['dep1'] == 'Running' + + println('✓ ServiceStatus tests passed') +} + +// test_service_stats tests ServiceStats and ChildStats structs +fn test_service_stats() { + child1 := ChildStats{ + pid: 1235 + memory_usage: 1024 * 1024 // 1MB + cpu_usage: 2.5 + } + + child2 := ChildStats{ + pid: 1236 + memory_usage: 2 * 1024 * 1024 // 2MB + cpu_usage: 1.2 + } + + stats := ServiceStats{ + name: 'test_service' + pid: 1234 + memory_usage: 10 * 1024 * 1024 // 10MB + cpu_usage: 5.0 + children: [child1, child2] + } + + assert stats.name == 'test_service' + assert stats.pid == 1234 + assert stats.memory_usage == 10 * 1024 * 1024 + assert stats.cpu_usage == 5.0 + assert stats.children.len == 2 + assert stats.children[0].pid == 1235 + assert stats.children[1].memory_usage == 2 * 1024 * 1024 + + println('✓ ServiceStats tests passed') +} + +// test_zinit_error tests ZinitError struct and error message +fn test_zinit_error() { + error := ZinitError{ + code: -32000 + message: 'Service not found' + data: 'service name "unknown" unknown' + } + + assert error.code == -32000 + assert error.message == 'Service not found' + assert error.data == 'service name "unknown" unknown' + + error_msg := error.msg() + assert error_msg.contains('Zinit Error -32000') + assert error_msg.contains('Service not found') + assert error_msg.contains('service name "unknown" unknown') + + println('✓ ZinitError tests passed') +} + +// test_connection_handling tests connection management (without actual connection) +fn test_connection_handling() { + mut client := new_default_client() + + // Initially no connection + assert client.conn == none + + // Test disconnect on non-connected client (should not panic) + client.disconnect() + assert client.conn == none + + println('✓ Connection handling tests passed') +} + +// integration_test_basic performs basic integration tests if zinit is available +fn integration_test_basic() { + mut client := new_default_client() + defer { + client.disconnect() + } + + println('Running integration tests (requires running zinit)...') + + // Test RPC discovery + spec := client.rpc_discover() or { + println('⚠ Integration test skipped: zinit not available (${err})') + return + } + + println('✓ RPC discovery successful') + + // Test service list + services := client.service_list() or { + println('✗ Service list failed: ${err}') + return + } + + println('✓ Service list successful (${services.len} services)') + + // If there are services, test getting status of the first one + if services.len > 0 { + service_name := services.keys()[0] + status := client.service_status(service_name) or { + println('⚠ Could not get status for ${service_name}: ${err}') + return + } + println('✓ Service status for ${service_name}: ${status.state}') + } + + // Test getting current logs + logs := client.stream_current_logs(none) or { + println('⚠ Could not get logs: ${err}') + return + } + + println('✓ Log streaming successful (${logs.len} log entries)') + + println('✓ All integration tests passed') +} + +// run_all_tests runs all test functions +pub fn run_all_tests() { + println('Running Zinit Client Tests...\n') + + // Unit tests + test_client_creation() + test_service_config() + test_service_status() + test_service_stats() + test_zinit_error() + test_connection_handling() + + println('\n--- Unit Tests Complete ---\n') + + // Integration tests (optional, requires running zinit) + integration_test_basic() + + println('\n--- All Tests Complete ---') +} + +// main function for running tests directly +fn main() { + run_all_tests() +} diff --git a/src/clients/zinit/types.v b/src/clients/zinit/types.v new file mode 100644 index 0000000..62adf03 --- /dev/null +++ b/src/clients/zinit/types.v @@ -0,0 +1,53 @@ +module zinit + +// ServiceConfig represents the configuration for a zinit service +pub struct ServiceConfig { +pub mut: + exec string // Command to run + oneshot bool // Whether the service should be restarted + after []string // Services that must be running before this one starts + log string // How to handle service output (null, ring, stdout) + env map[string]string // Environment variables for the service + shutdown_timeout int // Maximum time to wait for service to stop during shutdown +} + +// ServiceStatus represents the detailed status information for a service +pub struct ServiceStatus { +pub mut: + name string // Service name + pid int // Process ID of the running service (if running) + state string // Current state of the service (Running, Success, Error, etc.) + target string // Target state of the service (Up, Down) + after map[string]string // Dependencies of the service and their states +} + +// ServiceStats represents memory and CPU usage statistics for a service +pub struct ServiceStats { +pub mut: + name string // Service name + pid int // Process ID of the service + memory_usage i64 // Memory usage in bytes + cpu_usage f64 // CPU usage as a percentage (0-100) + children []ChildStats // Stats for child processes +} + +// ChildStats represents statistics for a child process +pub struct ChildStats { +pub mut: + pid int // Process ID of the child process + memory_usage i64 // Memory usage in bytes + cpu_usage f64 // CPU usage as a percentage (0-100) +} + +// ZinitError represents an error returned by the zinit API +pub struct ZinitError { +pub mut: + code int // Error code + message string // Error message + data string // Additional error data +} + +// Error implements the error interface for ZinitError +pub fn (e ZinitError) msg() string { + return 'Zinit Error ${e.code}: ${e.message} - ${e.data}' +} diff --git a/src/clients/zinit/zinit.v b/src/clients/zinit/zinit.v new file mode 100644 index 0000000..26c7957 --- /dev/null +++ b/src/clients/zinit/zinit.v @@ -0,0 +1,145 @@ +module zinit + +// Zinit Client Module +// +// This module provides a comprehensive V (Vlang) client library for interacting +// with the Zinit service manager via JSON-RPC over Unix socket. +// +// The module includes: +// - Complete type definitions for all API structures +// - Full client implementation with all OpenRPC methods +// - Comprehensive error handling +// - Connection management +// - Well-documented API with examples +// +// Usage: +// import zinit +// +// mut client := zinit.new_default_client() +// defer { client.disconnect() } +// +// services := client.service_list()! +// for name, state in services { +// println('${name}: ${state}') +// } +// +// For detailed documentation, see README.md +// For usage examples, see example.v +// For tests, see test.v + +// Module version information +pub const ( + version = '1.0.0' + author = 'Hero Code' + license = 'MIT' +) + +// Default socket path for zinit +pub const default_socket_path = '/tmp/zinit.sock' + +// Common service states +pub const ( + state_running = 'Running' + state_success = 'Success' + state_error = 'Error' + state_stopped = 'Stopped' + state_failed = 'Failed' +) + +// Common service targets +pub const ( + target_up = 'Up' + target_down = 'Down' +) + +// Common log types +pub const ( + log_null = 'null' + log_ring = 'ring' + log_stdout = 'stdout' +) + +// Common signals +pub const ( + signal_term = 'SIGTERM' + signal_kill = 'SIGKILL' + signal_hup = 'SIGHUP' + signal_usr1 = 'SIGUSR1' + signal_usr2 = 'SIGUSR2' +) + +// JSON-RPC error codes as defined in the OpenRPC specification +pub const ( + error_service_not_found = -32000 + error_service_already_monitored = -32001 + error_service_is_up = -32002 + error_service_is_down = -32003 + error_invalid_signal = -32004 + error_config_error = -32005 + error_shutting_down = -32006 + error_service_already_exists = -32007 + error_service_file_error = -32008 +) + +// Helper function to create a basic service configuration +pub fn new_service_config(exec string) ServiceConfig { + return ServiceConfig{ + exec: exec + oneshot: false + log: log_stdout + env: map[string]string{} + shutdown_timeout: 30 + } +} + +// Helper function to create a oneshot service configuration +pub fn new_oneshot_service_config(exec string) ServiceConfig { + return ServiceConfig{ + exec: exec + oneshot: true + log: log_stdout + env: map[string]string{} + shutdown_timeout: 30 + } +} + +// Helper function to check if an error is a specific zinit error code +pub fn is_zinit_error_code(err IError, code int) bool { + if zinit_err := err as ZinitError { + return zinit_err.code == code + } + return false +} + +// Helper function to check if service is not found error +pub fn is_service_not_found_error(err IError) bool { + return is_zinit_error_code(err, error_service_not_found) +} + +// Helper function to check if service is already monitored error +pub fn is_service_already_monitored_error(err IError) bool { + return is_zinit_error_code(err, error_service_already_monitored) +} + +// Helper function to check if service is down error +pub fn is_service_down_error(err IError) bool { + return is_zinit_error_code(err, error_service_is_down) +} + +// Helper function to format memory usage in human-readable format +pub fn format_memory_usage(bytes i64) string { + if bytes < 1024 { + return '${bytes} B' + } else if bytes < 1024 * 1024 { + return '${bytes / 1024} KB' + } else if bytes < 1024 * 1024 * 1024 { + return '${bytes / 1024 / 1024} MB' + } else { + return '${bytes / 1024 / 1024 / 1024} GB' + } +} + +// Helper function to format CPU usage +pub fn format_cpu_usage(cpu_percent f64) string { + return '${cpu_percent:.1f}%' +}