diff --git a/docs/features/agent_node_mesh.md b/docs/features/agent_node_mesh.md new file mode 100644 index 0000000..853e56d --- /dev/null +++ b/docs/features/agent_node_mesh.md @@ -0,0 +1,122 @@ +# Feature Reference: Agent Node Mesh + +This document serves as the comprehensive reference for the **Agent Node Mesh** UI in the Cortex platform. It describes the UI structure, design philosophy, user journeys, and technical implementation details to ensure functional stability during future iterations. + +--- + +## 1. UI Overview & Layout Structure + +The Agent Node Mesh page (`NodesPage.js`) is the central control plane for managing distributed execution environments. It follows a vertical stack layout with collapsible functional modules. + +### A. Dashboard Header (Top) +- **Position**: Fixed at the top of the main content area. +- **Title**: `Agent Node Mesh` with a rocket icon (🚀). +- **Description**: Contextual message based on user role (Admin vs. User). +- **Global Actions**: + - **Refresh List**: Triggers a full REST fetch of node registry and group access. + - **Register Node (Admin Only)**: Opens a modal to create new node slugs and generate invite tokens. + +### B. Node Management Card (Repeatable Component) +Each registered node is represented by a high-density management card. + +#### 1. Identity & Live Pulse (Top-Left) +- **Pulsing Indicator**: A real-time status light. + - `bg-green-500`: Node is active and heartbeating via gRPC. + - `bg-gray-400`: Node is offline or stale. +- **Status Text**: Dynamic label (`online`, `busy`, `idle`, `offline`) derived from gRPC mesh status. +- **Node Name**: Primary display name. +- **Node ID**: Secondary monospaced identifier (e.g., `test-prod-node`). + +#### 2. Live Health Metrics (Integrated in Top Row) +- **CPU Meter**: Gradient bar (Indigo-Blue) showing total host load. **Hover** displays core count and exact percentage. +- **RAM Meter**: Gradient bar (Pink-Rose) showing memory utilization. **Hover** displays GB used vs total. +- **Dynamic Visibility**: Meters only appear when the node is **online**. If offline, a stable space of `140px` is reserved to prevent UI shifting. +- **Philosophy**: Instant "at-a-glance" health check without needing to open deep menus. + +#### 3. State Control Toggle (Right Middle) +- **Active/Disabled Switch**: A tactile, rounded toggle. + - **Active (Indigo)**: Node is allowed to accept tasks and sync files. + - **Disabled (Red)**: Node restricted from synchronization and command execution. + +#### 4. Action Utility Bar (Right) +- **Terminal Button (Amber/Indigo)**: Expands the PTY-based console. +- **File Navigator Button (Amber)**: Expands the directory explorer. +- **Settings & Details Button (Gray)**: Expands the administrative configuration panel. +- **Deregister Button (Red Trash)**: Permanently removes node from registry (Admin only). + +--- + +## 2. Expanded Feature Panes + +### A. Interactive Console (`NodeTerminal.js`) +- **Philosophy**: Persistent PTY (Pseudo-Terminal) session. +- **Features**: + - Real-time character-by-character streaming (ANSI support). + - **Latency Monitor**: Displays RTT in milliseconds. + - **Zoom/Fullscreen**: Toggle for complex shell tasks. + - **Debug Mode**: Toggles visibility of background task events (start/stop/snapshot). + - **Clear**: Wipes the xterm grid. + +### B. File System Navigator (`FileSystemNavigator.js`) +- **Philosophy**: Lazy-loaded directory tree for massive file systems. +- **Features**: + - **Directory Expansion**: Clicking a folder fetches its immediate children via gRPC `LIST`. + - **File View**: Opens a modal with a monospaced code viewer (prevents binary file viewing). + - **Sync Indicators**: Amber dots indicate if a file is currently synced with the server's Ghost Mirror. + - **Operations**: Create File/Folder, Delete, and Breadcrumb navigation. + +### C. Admin Settings Pane (Inline) +- **Identity Details**: Displays registered description. +- **Skill Configuration**: Toggles for `Shell`, `Browser`, and `Sync` logic. +- **Group Access Management**: Map specific user groups to this node with 'use' or 'root' permissions. +- **Download Bundle**: Generates the `agent_config.yaml` and installation package. + +--- + +## 3. Execution Live Bus (Bottom Overlay) + +- **Position**: Bottom of the page, visible when events occur. +- **Content**: A streaming timeline of all mesh events (`task_start`, `task_complete`, `sync_progress`). +- **Philosophy**: Providing a "Global Pulse" of what the AI is doing across the entire fleet of nodes. + +--- + +## 4. User Journey & Steps + +### Path 1: Node Registration (Admin) +1. Click **Register Node** in header. +2. Enter Slug (e.g. `gpu-worker-1`) and Display Name. +3. Expanded **Settings** on the new node card. +4. Click **Download Configuration**. +5. Run the installation script on the physical machine using the generated YAML. + +### Path 2: Live Debugging (User/Admin) +1. Locate the target node in the mesh list. +2. Verify **Live Pulse** is green. +3. Click the **Terminal** icon. +4. Execute `ls -la` or `top` to inspect environment. +5. Click **File Navigator** to locate artifacts or logs. + +--- + +## 5. Source Code Mapping + +| Component | UI Purpose | Source Path | +| :--- | :--- | :--- | +| `NodesPage.js` | Main Orchestrator View & Mesh Logic | [NodesPage.js](file:///app/ui/client-app/src/pages/NodesPage.js) | +| `NodeHealthMetrics` | Real-time CPU/RAM Bars | [NodesPage.js (L140)](file:///app/ui/client-app/src/pages/NodesPage.js#L140) | +| `NodeTerminal.js` | PTY-bound Xterm.js terminal | [NodeTerminal.js](file:///app/ui/client-app/src/components/NodeTerminal.js) | +| `FileSystemNavigator.js` | File/Folder Explorer | [FileSystemNavigator.js](file:///app/ui/client-app/src/components/FileSystemNavigator.js) | +| `NodeRegistryService` | Backend Node Tracking | [node_registry.py](file:///app/ai-hub/app/core/services/node_registry.py) | +| `TaskAssistant` | gRPC Dispatcher Brain | [assistant.py](file:///app/ai-hub/app/core/grpc/services/assistant.py) | + +--- + +## 6. Guidelines for Future Changes + +> [!IMPORTANT] +> **Before modifying any existing UI elements on the Agent Node Mesh page:** +> 1. Read this reference document to understand the current feature set. +> 2. If adding a new element (e.g., "GPU Monitor"), update this document with its position, style, and purpose **first**. +> 3. Ensure new components do not break the "Expanded" state logic of Terminal/Files. +> 4. Use `@xterm/addon-fit` for all terminal-like additions to maintain responsive UI. diff --git a/ui/client-app/src/pages/NodesPage.js b/ui/client-app/src/pages/NodesPage.js index cf1d679..6c2f919 100644 --- a/ui/client-app/src/pages/NodesPage.js +++ b/ui/client-app/src/pages/NodesPage.js @@ -50,11 +50,16 @@ // WebSocket Connection for Live Mesh Status useEffect(() => { + if (!user?.id) return; + + // CRITICAL FIX: getNodeStreamUrl() without args uses the global user-based stream. + // Passing user.id was incorrectly interpreted as a nodeId. const wsUrl = getNodeStreamUrl(); const ws = new WebSocket(wsUrl); - ws.onmessage = (event) => { - const msg = JSON.parse(event.data); + ws.onopen = () => console.log("[📡] Mesh Monitor Connected"); + ws.onmessage = (e) => { + const msg = JSON.parse(e.data); if (msg.event === 'initial_snapshot') { const statusMap = {}; msg.data.nodes.forEach(n => { @@ -62,18 +67,29 @@ }); setMeshStatus(statusMap); } else if (msg.event === 'mesh_heartbeat') { - const statusMap = { ...meshStatus }; - msg.data.nodes.forEach(n => { - statusMap[n.node_id] = { status: n.status, stats: n.stats }; + setMeshStatus(prev => { + const statusMap = { ...prev }; + msg.data.nodes.forEach(n => { + statusMap[n.node_id] = { status: n.status, stats: n.stats }; + }); + return statusMap; }); - setMeshStatus(statusMap); - } else if (['task_start', 'task_complete', 'task_error', 'info'].includes(msg.event)) { + } else if (msg.event === 'heartbeat') { + // Individual node heartbeat + setMeshStatus(prev => ({ + ...prev, + [msg.node_id]: { ...prev[msg.node_id], stats: msg.data, status: 'online' } + })); + } else { setRecentEvents(prev => [msg, ...prev].slice(0, 50)); } }; + ws.onclose = () => console.log("[📡] Mesh Monitor Disconnected"); + ws.onerror = (err) => console.error("[📡] Mesh Monitor Error:", err); + return () => ws.close(); - }, [user?.id, meshStatus]); + }, [user?.id]); const handleCreateNode = async (e) => { e.preventDefault(); @@ -126,8 +142,11 @@ const NodeHealthMetrics = ({ nodeId, stats, compact = false }) => { const live = meshStatus[nodeId]; - const isOnline = live?.status === 'online' || live?.status === 'idle' || live?.status === 'busy'; - if (!isOnline && compact) return null; + // If we have no live status, we are definitely not showing live meters in compact mode + if (!live && compact) return
; // Reserve space + + const isOnline = live?.status === 'online' || live?.status === 'idle' || live?.status === 'busy' || stats?.cpu_usage_percent > 0; + if (!isOnline && compact) return
; const cpu = stats?.cpu_usage_percent || 0; const mem = stats?.memory_usage_percent || 0; @@ -136,23 +155,33 @@ const memTotal = (stats?.memory_total_gb || 0).toFixed(1); return ( -
-
+
+
CPU - {cpu.toFixed(0)}% +
+ {cpu.toFixed(0)}% + {compact && cpuCount > 0 && ( + ({cpuCount}c) + )} +
-
-
+
+
-
+
RAM - {mem.toFixed(0)}% +
+ {mem.toFixed(0)}% + {compact && memTotal > 0 && ( + ({memUsed}G) + )} +
-
-
+
+
@@ -180,7 +209,7 @@ className="p-2 text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" title="Refresh List" > - + @@ -215,17 +244,17 @@ {nodes.map(node => (
{/* Top Row: Basic Info & Actions */} -
+
-
+
-
-
- +
+
+ {meshStatus[node.node_id]?.status || node.last_status || 'offline'}
-

{node.display_name}

+

{node.display_name}