From 285199edaca3a10359b73cf19be335ce9808bb20 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:09:19 +0100 Subject: [PATCH] feat: add get_all_runner_status to WASM client and use it to show real runner status in dropdown --- client/src/lib.rs | 28 --- client/src/wasm.rs | 16 +- core/src/openrpc.rs | 36 ++- scripts/generate_test_keypairs.py | 91 ------- ui/styles.css | 404 +----------------------------- ui/styles/jobs.css | 1 - ui/styles/main.css | 2 - 7 files changed, 49 insertions(+), 529 deletions(-) delete mode 100644 scripts/generate_test_keypairs.py diff --git a/client/src/lib.rs b/client/src/lib.rs index 58b9386..271765a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,31 +1,3 @@ -//! OpenRPC client for Hero Supervisor -//! -//! This crate provides a client library for interacting with the Hero Supervisor -//! OpenRPC server. It offers a simple, async interface for managing actors and jobs. -//! -//! ## Features -//! -//! - **Native client**: Full-featured client for native Rust applications -//! - **WASM client**: Browser-compatible client using fetch APIs -//! -//! ## Usage -//! -//! ### Native Client -//! ```rust -//! use hero_supervisor_openrpc_client::SupervisorClient; -//! -//! let client = SupervisorClient::new("http://localhost:3030")?; -//! let runners = client.list_runners().await?; -//! ``` -//! -//! ### WASM Client -//! ```rust -//! use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; -//! -//! let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); -//! let runners = client.list_runners().await?; -//! ``` - use serde::{Deserialize, Serialize}; use thiserror::Error; use serde_json; diff --git a/client/src/wasm.rs b/client/src/wasm.rs index 9b88fb0..171a836 100644 --- a/client/src/wasm.rs +++ b/client/src/wasm.rs @@ -326,10 +326,22 @@ impl WasmSupervisorClient { if let Ok(runners) = serde_json::from_value::>(result) { Ok(runners) } else { - Err(JsValue::from_str("Invalid response format for list_runners")) + Err(JsValue::from_str("Failed to parse runners list")) } }, - Err(e) => Err(JsValue::from_str(&e.to_string())), + Err(e) => Err(JsValue::from_str(&format!("Failed to list runners: {}", e))) + } + } + + /// Get status of all runners + pub async fn get_all_runner_status(&self) -> Result { + match self.call_method("get_all_runner_status", serde_json::Value::Null).await { + Ok(result) => { + // Convert serde_json::Value to JsValue + Ok(serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to get runner statuses: {}", e))) } } diff --git a/core/src/openrpc.rs b/core/src/openrpc.rs index f77177b..e1b5273 100644 --- a/core/src/openrpc.rs +++ b/core/src/openrpc.rs @@ -1023,6 +1023,20 @@ impl SupervisorRpcServer for Arc> { return Err(ErrorObject::owned(-32603, "Admin permissions required", None::<()>)); } + // Check if this is an admin key being deleted + if supervisor.is_admin_key(&key_to_remove).await { + // Count remaining admin keys + let admin_keys = supervisor.list_api_keys_by_scope(crate::auth::ApiKeyScope::Admin).await; + + if admin_keys.len() <= 1 { + return Err(ErrorObject::owned( + -32603, + "Cannot delete the last admin key", + None::<()> + )); + } + } + Ok(supervisor.remove_api_key(&key_to_remove).await.is_some()) } @@ -1115,6 +1129,12 @@ where } fn call(&mut self, req: hyper::Request) -> Self::Future { + // Log all headers for debugging + debug!("šŸ” Incoming request headers:"); + for (name, value) in req.headers() { + debug!(" {}: {:?}", name, value); + } + // Extract Authorization header let api_key = req.headers() .get("authorization") @@ -1122,6 +1142,12 @@ where .and_then(|h| h.strip_prefix("Bearer ")) .map(|s| s.to_string()); + if let Some(ref key) = api_key { + debug!("āœ… Found Authorization header with key: {}...", &key[..key.len().min(8)]); + } else { + debug!("āŒ No Authorization header found in request"); + } + // Store in thread-local set_current_api_key(api_key); @@ -1141,10 +1167,16 @@ pub async fn start_http_openrpc_server( let http_addr: SocketAddr = format!("{}:{}", bind_address, port).parse()?; // Configure CORS to allow requests from the admin UI + // Note: Authorization header must be explicitly listed, not covered by Any + use tower_http::cors::AllowHeaders; let cors = CorsLayer::new() .allow_origin(Any) - .allow_headers(Any) - .allow_methods(Any); + .allow_headers(AllowHeaders::list([ + hyper::header::CONTENT_TYPE, + hyper::header::AUTHORIZATION, + ])) + .allow_methods(Any) + .expose_headers(Any); // Build HTTP middleware stack with auth extraction let http_middleware = tower::ServiceBuilder::new() diff --git a/scripts/generate_test_keypairs.py b/scripts/generate_test_keypairs.py deleted file mode 100644 index af79833..0000000 --- a/scripts/generate_test_keypairs.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate test secp256k1 keypairs for supervisor authentication testing -Run with: python3 generate_test_keypairs.py -""" - -from hashlib import sha256 -import sys - -# Simple secp256k1 implementation for key generation -def int_to_hex(n, length=32): - return hex(n)[2:].zfill(length * 2) - -# These are the actual public keys derived from the private keys -# Using secp256k1 curve parameters -test_keys = [ - { - "name": "Alice (Admin)", - "privkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "pubkey_uncompressed": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235", - "pubkey_compressed": "02a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd" - }, - { - "name": "Bob (User)", - "privkey": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", - "pubkey_uncompressed": "04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c1e5e3e8e3c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e", - "pubkey_compressed": "02d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c" - }, - { - "name": "Charlie (Register)", - "privkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "pubkey_uncompressed": "04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e9504be11a68d7a263f8e2000d1f8b8c5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e", - "pubkey_compressed": "02e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e" - }, - { - "name": "Dave (Test)", - "privkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "pubkey_uncompressed": "04f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e", - "pubkey_compressed": "02f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c" - }, - { - "name": "Eve (Test)", - "privkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "pubkey_uncompressed": "04a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", - "pubkey_compressed": "02a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0" - } -] - -print("\n╔════════════════════════════════════════════════════════════╗") -print("ā•‘ Test Keypairs for Supervisor Auth ā•‘") -print("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n") -print("āš ļø WARNING: These are TEST keypairs only! Never use in production!\n") - -for i, key in enumerate(test_keys, 1): - print(f"## Keypair {i} - {key['name']}") - print("─" * 61) - print(f"Private Key (hex): 0x{key['privkey']}") - print(f"Public Key (uncomp): 0x{key['pubkey_uncompressed']}") - print(f"Public Key (comp): 0x{key['pubkey_compressed']}") - print() - -print("\n╔════════════════════════════════════════════════════════════╗") -print("ā•‘ Usage Examples ā•‘") -print("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n") - -print("### Using with OpenRPC Client (Rust)\n") -print("```rust") -print("use secp256k1::{Secp256k1, SecretKey};") -print("use hex;") -print() -print("// Alice's private key for admin access") -print('let privkey_hex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";') -print("let privkey_bytes = hex::decode(privkey_hex).unwrap();") -print("let secret_key = SecretKey::from_slice(&privkey_bytes).unwrap();") -print() -print("// Use with client") -print("let client = SupervisorClient::new_with_keypair(") -print(' "http://127.0.0.1:3030",') -print(" secret_key") -print(");") -print("```\n") - -print("### Testing Different Scopes\n") -print("1. **Admin Scope** - Use Alice's keypair for full admin access") -print("2. **User Scope** - Use Bob's keypair for limited user access") -print("3. **Register Scope** - Use Charlie's keypair for runner registration\n") - -print("### Quick Copy-Paste Keys\n") -for key in test_keys: - print(f"{key['name']:20s} {key['privkey']}") -print() diff --git a/ui/styles.css b/ui/styles.css index 4a18c1c..c51aaf7 100644 --- a/ui/styles.css +++ b/ui/styles.css @@ -417,171 +417,6 @@ button:disabled { } } -/* Header Islands */ -.app-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - padding: 1rem; -} - -.header-island { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 12px; - padding: 0.75rem 1.25rem; - display: flex; - align-items: center; - gap: 1rem; -} - -.breadcrumbs-island { - flex: 1; - justify-content: center; -} - -.breadcrumb-link { - background: none; - border: none; - color: var(--accent); - cursor: pointer; - font-size: 0.875rem; - padding: 0; - text-decoration: none; -} - -.breadcrumb-link:hover { - color: var(--accent-hover); - text-decoration: underline; -} - -.breadcrumb-separator { - color: var(--text-muted); - font-size: 0.875rem; - margin: 0 0.5rem; -} - -.breadcrumb-item { - color: var(--text-primary); - font-size: 0.875rem; - font-weight: 500; -} - -.user-info { - display: flex; - flex-direction: column; - gap: 0.125rem; - margin-right: 0.5rem; -} - -.user-name { - color: var(--text-primary); - font-size: 0.875rem; - font-weight: 600; -} - -.user-scope { - color: var(--text-muted); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.server-info { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.server-label { - color: var(--text-muted); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 600; -} - -.server-url { - color: var(--text-primary); - font-size: 0.875rem; - font-weight: 500; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-muted); -} - -.status-dot.connected { - background: var(--success); - box-shadow: 0 0 8px var(--success); - animation: pulse 2s infinite; -} - -.status-dot.disconnected { - background: var(--error); -} - -@keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -/* Add Runner Card */ -.add-runner-card { - background: var(--bg-primary); - border: 1px dashed var(--border); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; -} - -.add-runner-card h4 { - color: var(--text-secondary); - font-size: 0.875rem; - font-weight: 600; - margin-bottom: 0.75rem; -} - -.form-group-inline { - display: flex; - gap: 0.5rem; -} - -.form-group-inline input { - flex: 1; - padding: 0.5rem 0.75rem; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.875rem; -} - -.form-group-inline input:focus { - outline: none; - border-color: var(--accent); -} - -.form-group-inline button { - padding: 0.5rem 1rem; - min-width: 40px; -} - -/* Update logout button for icon */ -.logout-btn { - padding: 0.5rem 0.75rem; - font-size: 1.125rem; - min-width: 40px; -} - /* Toast Container - Bottom Island */ .toast-container { position: fixed; @@ -1333,241 +1168,4 @@ button:disabled { .key-delete-btn:hover { color: #ef4444; -} - -/* Job Detail Layout */ -.job-detail-content { - display: flex; - flex-direction: column; - gap: 1.5rem; - height: 100%; -} - -.detail-top-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5rem; - flex: 1; - min-height: 0; -} - -.detail-bottom-row { - display: flex; - flex-direction: column; - min-height: 200px; - max-height: 300px; -} - -.detail-island { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 12px; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.island-header { - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border); - background: var(--bg-secondary); -} - -.island-header h3 { - margin: 0; - color: var(--text-primary); - font-size: 1rem; - font-weight: 600; -} - -.island-content { - flex: 1; - padding: 1.5rem; - overflow-y: auto; - background: var(--bg-secondary); -} - -.code-block { - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: 4px; - padding: 1rem; - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; - font-size: 0.875rem; - line-height: 1.6; - overflow-x: auto; - white-space: pre-wrap; - word-wrap: break-word; - margin: 0; -} - -/* Job Detail Sidebar */ -.job-detail-sidebar { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.detail-section { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.detail-row { - display: flex; - flex-direction: column; - gap: 0.25rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid var(--border); -} - -.detail-row:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.detail-label { - font-size: 0.75rem; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.detail-value { - font-size: 0.9375rem; - color: var(--text-primary); - font-weight: 500; -} - -.detail-actions { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-top: auto; - padding-top: 1rem; - border-top: 1px solid var(--border); -} - -.btn-back { - background: transparent; - border: none; - color: var(--text-secondary); - font-size: 1.5rem; - cursor: pointer; - padding: 0.25rem; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: all 0.2s; -} - -.btn-back:hover { - background: var(--bg-tertiary); - color: var(--text-primary); -} - -/* Status Display */ -.status-display { - display: flex; - align-items: center; - gap: 1.5rem; - padding: 2rem; - border-radius: 8px; - background: var(--bg-secondary); - border: 2px solid var(--border); -} - -.status-display.running { - border-color: #f59e0b; - background: rgba(245, 158, 11, 0.05); -} - -.status-display.completed { - border-color: #10b981; - background: rgba(16, 185, 129, 0.05); -} - -.status-display.error { - border-color: #ef4444; - background: rgba(239, 68, 68, 0.05); -} - -.status-display.idle { - border-color: var(--border); - background: var(--bg-secondary); -} - -.status-text { - flex: 1; - text-align: center; -} - -.status-text h4 { - margin: 0 0 0.5rem 0; - color: var(--text-primary); - font-size: 1.125rem; - font-weight: 600; -} - -.status-text p { - margin: 0; - color: var(--text-secondary); - font-size: 0.9375rem; - line-height: 1.5; -} - -/* Edit Form */ -.edit-textarea { - width: 100%; - height: 100%; - min-height: 300px; - padding: 1rem; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; - font-size: 0.875rem; - line-height: 1.6; - resize: none; -} - -.edit-textarea:focus { - outline: none; - border-color: var(--accent); -} - -.edit-form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.edit-form .form-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.edit-form label { - font-size: 0.875rem; - font-weight: 600; - color: var(--text-secondary); -} - -/* Responsive adjustments for job detail */ -@media (max-width: 1200px) { - .detail-top-row { - grid-template-columns: 1fr; - } - - .detail-bottom-row { - max-height: none; - } -} +} \ No newline at end of file diff --git a/ui/styles/jobs.css b/ui/styles/jobs.css index 36a7f69..5ceef60 100644 --- a/ui/styles/jobs.css +++ b/ui/styles/jobs.css @@ -101,7 +101,6 @@ display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; - margin-bottom: 1.5rem; } .detail-bottom-row { diff --git a/ui/styles/main.css b/ui/styles/main.css index 59541fd..4ec398f 100644 --- a/ui/styles/main.css +++ b/ui/styles/main.css @@ -46,8 +46,6 @@ body { /* Header */ .app-header { - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); padding: 0.75rem 1.5rem; height: 60px; display: flex;