diff --git a/frontend/src/App.js b/frontend/src/App.js
index d3243f7..5bbcb88 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -7,7 +7,7 @@
import LoginPage from "./pages/LoginPage";
import SettingsPage from "./pages/SettingsPage";
import ProfilePage from "./pages/ProfilePage";
-import NodesPage from "./pages/NodesPage";
+import { NodesPage } from "./features/nodes";
import SkillsPage from "./pages/SkillsPage";
import { getUserStatus, logout, getUserProfile } from "./services/apiService";
diff --git a/frontend/src/components/NodeTerminal.js b/frontend/src/components/NodeTerminal.js
deleted file mode 100644
index 2ecf8c7..0000000
--- a/frontend/src/components/NodeTerminal.js
+++ /dev/null
@@ -1,229 +0,0 @@
-import React, { useState, useEffect, useRef, useMemo } from 'react';
-import { getNodeStreamUrl } from '../services/apiService';
-import { Terminal } from '@xterm/xterm';
-import { FitAddon } from '@xterm/addon-fit';
-import '@xterm/xterm/css/xterm.css';
-
-const NodeTerminal = ({ nodeId }) => {
- const [isZoomed, setIsZoomed] = useState(false);
- const [debugMode, setDebugMode] = useState(false);
- const [latency, setLatency] = useState(null);
-
- const terminalRef = useRef(null);
- const wsRef = useRef(null);
- const xtermRef = useRef(null);
- const fitAddonRef = useRef(null);
-
- const sessionId = useMemo(() => `terminal-${nodeId}-${Math.random().toString(36).substr(2, 9)}`, [nodeId]);
-
- // 1. Core Terminal Engine Initialization
- useEffect(() => {
- const xterm = new Terminal({
- cursorBlink: true,
- theme: {
- background: '#030712', // Matches Tailwind bg-gray-950
- foreground: '#10b981', // Matches text-emerald-500 loosely
- cursor: '#10b981',
- cursorAccent: '#030712',
- },
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
- fontSize: 13,
- rightClickSelectsWord: true,
- scrollback: 1000,
- convertEol: true,
- scrollOnOutput: true,
- });
-
- const fitAddon = new FitAddon();
- xterm.loadAddon(fitAddon);
-
- xterm.open(terminalRef.current);
- fitAddon.fit();
-
- xtermRef.current = xterm;
- fitAddonRef.current = fitAddon;
-
- // Auto-refit terminal rows/cols when user resizes Chrome window
- const observer = new ResizeObserver(() => fitAddon.fit());
- if (terminalRef.current) {
- observer.observe(terminalRef.current);
- }
-
- return () => {
- observer.disconnect();
- xterm.dispose();
- };
- }, []);
-
- // 2. Headless WS Comm Link & Keystroke Forwarding
- useEffect(() => {
- const ws = new WebSocket(getNodeStreamUrl(nodeId));
- wsRef.current = ws;
-
- ws.onopen = () => {
- xtermRef.current?.writeln('\x1b[32m\x1b[1m[CORTEX MESH] Stream Established — Persistent PTY Active\x1b[0m\r');
- // Force initial resize sync
- fitAddonRef.current?.fit();
- };
-
- ws.onmessage = (event) => {
- try {
- const msg = JSON.parse(event.data);
-
- // --- 1. Periodic Latency Monitor (Pong Response) ---
- if (msg.event === 'pong' && msg.client_ts) {
- const rtt = Date.now() - msg.client_ts;
- setLatency(rtt);
- }
-
- // --- 2. Skill Events (Terminal stdout) ---
- else if (msg.event === 'skill_event' && msg.data?.type === 'output') {
- // Crucial: Just blindly pipe pure PTY ANSI stdout to Xterm directly! No formatting!
- xtermRef.current?.write(msg.data.data);
- }
- else if (msg.event === 'task_error') {
- const errStr = msg.data?.stderr || JSON.stringify(msg.data);
- xtermRef.current?.writeln(`\r\n\x1b[31m[ERROR] ${errStr}\x1b[0m\r`);
- }
- else if (debugMode) {
- // Debug events visible natively inline in the terminal text
- if (msg.event === 'task_start') {
- xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_start id=${msg.task_id || '?'}\x1b[0m\r`);
- } else if (msg.event === 'task_complete') {
- xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_complete status=${msg.data?.status ?? '?'} id=${msg.task_id || '?'}\x1b[0m\r`);
- } else if (msg.event === 'snapshot') {
- xtermRef.current?.writeln(`\x1b[33m[DEBUG] snapshot status=${msg.data?.status || '?'}\x1b[0m\r`);
- }
- }
- } catch (e) {
- // Ignore raw failures
- }
- };
-
- ws.onerror = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket connection error]\x1b[0m\r');
- ws.onclose = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket disconnected]\x1b[0m\r');
-
- // BIND XTERM DATA TO SOCKET
- const disposableData = xtermRef.current?.onData((data) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify({
- action: "dispatch",
- command: JSON.stringify({ tty: data }),
- session_id: sessionId
- }));
- }
- });
-
- const disposableResize = xtermRef.current?.onResize(({ cols, rows }) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify({
- action: "dispatch",
- command: JSON.stringify({ action: "resize", cols, rows }),
- session_id: sessionId
- }));
- }
- });
-
- // --- PERIODIC LATENCY PING (Pulse) ---
- const pingInterval = setInterval(() => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify({
- action: "ping",
- ts: Date.now()
- }));
- }
- }, 3000);
-
- return () => {
- disposableData?.dispose();
- disposableResize?.dispose();
- clearInterval(pingInterval);
- ws.close();
- };
- }, [nodeId, sessionId, debugMode]);
-
- // Force XTerm internal Grid fit on zoom toggle
- useEffect(() => {
- const timeoutId = setTimeout(() => {
- fitAddonRef.current?.fit();
- }, 150);
- return () => clearTimeout(timeoutId);
- }, [isZoomed]);
-
- const handleClear = () => {
- xtermRef.current?.clear();
- };
-
- return (
-
- {isZoomed &&
setIsZoomed(false)}>
}
-
- {/* Title Bar */}
-
-
-
- Interactive Console
- — {nodeId}
- {latency !== null && (
-
- {latency}ms
-
- )}
-
-
- {/* Debug Mode Toggle */}
-
setDebugMode(d => !d)}
- title={debugMode ? 'Debug mode ON — click to disable' : 'Debug mode OFF — click to enable'}
- className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[9px] font-black uppercase tracking-widest transition-all border ${debugMode
- ? 'bg-amber-500/20 border-amber-500/50 text-amber-400 shadow shadow-amber-500/20'
- : 'bg-gray-800 border-gray-700 text-gray-600 hover:text-gray-400 hover:border-gray-600'
- }`}
- >
-
-
-
- {debugMode ? 'Debug ON' : 'Debug'}
-
-
- {/* Clear */}
-
-
-
-
-
-
- {/* Zoom */}
-
setIsZoomed(!isZoomed)}
- title={isZoomed ? 'Exit fullscreen' : 'Fullscreen'}
- className="text-gray-600 hover:text-white transition-colors p-1"
- >
- {isZoomed ? (
-
- ) : (
-
- )}
-
-
-
-
- {/* XTerm Host Area */}
- {/* Note: 'h-auto' combined with 'flex-1' guarantees XTerm occupies exactly the rest of the flex parent container */}
-
-
- );
-};
-
-export default NodeTerminal;
diff --git a/frontend/src/features/nodes/components/NodeTerminal.js b/frontend/src/features/nodes/components/NodeTerminal.js
new file mode 100644
index 0000000..0d5d6b9
--- /dev/null
+++ b/frontend/src/features/nodes/components/NodeTerminal.js
@@ -0,0 +1,229 @@
+import React, { useState, useEffect, useRef, useMemo } from 'react';
+import { getNodeStreamUrl } from '../../../services/apiService';
+import { Terminal } from '@xterm/xterm';
+import { FitAddon } from '@xterm/addon-fit';
+import '@xterm/xterm/css/xterm.css';
+
+const NodeTerminal = ({ nodeId }) => {
+ const [isZoomed, setIsZoomed] = useState(false);
+ const [debugMode, setDebugMode] = useState(false);
+ const [latency, setLatency] = useState(null);
+
+ const terminalRef = useRef(null);
+ const wsRef = useRef(null);
+ const xtermRef = useRef(null);
+ const fitAddonRef = useRef(null);
+
+ const sessionId = useMemo(() => `terminal-${nodeId}-${Math.random().toString(36).substr(2, 9)}`, [nodeId]);
+
+ // 1. Core Terminal Engine Initialization
+ useEffect(() => {
+ const xterm = new Terminal({
+ cursorBlink: true,
+ theme: {
+ background: '#030712', // Matches Tailwind bg-gray-950
+ foreground: '#10b981', // Matches text-emerald-500 loosely
+ cursor: '#10b981',
+ cursorAccent: '#030712',
+ },
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
+ fontSize: 13,
+ rightClickSelectsWord: true,
+ scrollback: 1000,
+ convertEol: true,
+ scrollOnOutput: true,
+ });
+
+ const fitAddon = new FitAddon();
+ xterm.loadAddon(fitAddon);
+
+ xterm.open(terminalRef.current);
+ fitAddon.fit();
+
+ xtermRef.current = xterm;
+ fitAddonRef.current = fitAddon;
+
+ // Auto-refit terminal rows/cols when user resizes Chrome window
+ const observer = new ResizeObserver(() => fitAddon.fit());
+ if (terminalRef.current) {
+ observer.observe(terminalRef.current);
+ }
+
+ return () => {
+ observer.disconnect();
+ xterm.dispose();
+ };
+ }, []);
+
+ // 2. Headless WS Comm Link & Keystroke Forwarding
+ useEffect(() => {
+ const ws = new WebSocket(getNodeStreamUrl(nodeId));
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ xtermRef.current?.writeln('\x1b[32m\x1b[1m[CORTEX MESH] Stream Established — Persistent PTY Active\x1b[0m\r');
+ // Force initial resize sync
+ fitAddonRef.current?.fit();
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const msg = JSON.parse(event.data);
+
+ // --- 1. Periodic Latency Monitor (Pong Response) ---
+ if (msg.event === 'pong' && msg.client_ts) {
+ const rtt = Date.now() - msg.client_ts;
+ setLatency(rtt);
+ }
+
+ // --- 2. Skill Events (Terminal stdout) ---
+ else if (msg.event === 'skill_event' && msg.data?.type === 'output') {
+ // Crucial: Just blindly pipe pure PTY ANSI stdout to Xterm directly! No formatting!
+ xtermRef.current?.write(msg.data.data);
+ }
+ else if (msg.event === 'task_error') {
+ const errStr = msg.data?.stderr || JSON.stringify(msg.data);
+ xtermRef.current?.writeln(`\r\n\x1b[31m[ERROR] ${errStr}\x1b[0m\r`);
+ }
+ else if (debugMode) {
+ // Debug events visible natively inline in the terminal text
+ if (msg.event === 'task_start') {
+ xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_start id=${msg.task_id || '?'}\x1b[0m\r`);
+ } else if (msg.event === 'task_complete') {
+ xtermRef.current?.writeln(`\r\n\x1b[33m[DEBUG] task_complete status=${msg.data?.status ?? '?'} id=${msg.task_id || '?'}\x1b[0m\r`);
+ } else if (msg.event === 'snapshot') {
+ xtermRef.current?.writeln(`\x1b[33m[DEBUG] snapshot status=${msg.data?.status || '?'}\x1b[0m\r`);
+ }
+ }
+ } catch (e) {
+ // Ignore raw failures
+ }
+ };
+
+ ws.onerror = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket connection error]\x1b[0m\r');
+ ws.onclose = () => xtermRef.current?.writeln('\r\n\x1b[31m[WebSocket disconnected]\x1b[0m\r');
+
+ // BIND XTERM DATA TO SOCKET
+ const disposableData = xtermRef.current?.onData((data) => {
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify({
+ action: "dispatch",
+ command: JSON.stringify({ tty: data }),
+ session_id: sessionId
+ }));
+ }
+ });
+
+ const disposableResize = xtermRef.current?.onResize(({ cols, rows }) => {
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify({
+ action: "dispatch",
+ command: JSON.stringify({ action: "resize", cols, rows }),
+ session_id: sessionId
+ }));
+ }
+ });
+
+ // --- PERIODIC LATENCY PING (Pulse) ---
+ const pingInterval = setInterval(() => {
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify({
+ action: "ping",
+ ts: Date.now()
+ }));
+ }
+ }, 3000);
+
+ return () => {
+ disposableData?.dispose();
+ disposableResize?.dispose();
+ clearInterval(pingInterval);
+ ws.close();
+ };
+ }, [nodeId, sessionId, debugMode]);
+
+ // Force XTerm internal Grid fit on zoom toggle
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ fitAddonRef.current?.fit();
+ }, 150);
+ return () => clearTimeout(timeoutId);
+ }, [isZoomed]);
+
+ const handleClear = () => {
+ xtermRef.current?.clear();
+ };
+
+ return (
+
+ {isZoomed &&
setIsZoomed(false)}>
}
+
+ {/* Title Bar */}
+
+
+
+ Interactive Console
+ — {nodeId}
+ {latency !== null && (
+
+ {latency}ms
+
+ )}
+
+
+ {/* Debug Mode Toggle */}
+
setDebugMode(d => !d)}
+ title={debugMode ? 'Debug mode ON — click to disable' : 'Debug mode OFF — click to enable'}
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[9px] font-black uppercase tracking-widest transition-all border ${debugMode
+ ? 'bg-amber-500/20 border-amber-500/50 text-amber-400 shadow shadow-amber-500/20'
+ : 'bg-gray-800 border-gray-700 text-gray-600 hover:text-gray-400 hover:border-gray-600'
+ }`}
+ >
+
+
+
+ {debugMode ? 'Debug ON' : 'Debug'}
+
+
+ {/* Clear */}
+
+
+
+
+
+
+ {/* Zoom */}
+
setIsZoomed(!isZoomed)}
+ title={isZoomed ? 'Exit fullscreen' : 'Fullscreen'}
+ className="text-gray-600 hover:text-white transition-colors p-1"
+ >
+ {isZoomed ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* XTerm Host Area */}
+ {/* Note: 'h-auto' combined with 'flex-1' guarantees XTerm occupies exactly the rest of the flex parent container */}
+
+
+ );
+};
+
+export default NodeTerminal;
diff --git a/frontend/src/features/nodes/index.js b/frontend/src/features/nodes/index.js
new file mode 100644
index 0000000..a1f3746
--- /dev/null
+++ b/frontend/src/features/nodes/index.js
@@ -0,0 +1,4 @@
+// Feature entry point for the Nodes feature.
+// Exposes the main page (and any future nodes-related exports) via a single import.
+
+export { default as NodesPage } from "./pages/NodesPage";
diff --git a/frontend/src/features/nodes/pages/NodesPage.js b/frontend/src/features/nodes/pages/NodesPage.js
new file mode 100644
index 0000000..d7df934
--- /dev/null
+++ b/frontend/src/features/nodes/pages/NodesPage.js
@@ -0,0 +1,894 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ getAdminNodes, adminCreateNode, adminUpdateNode, adminDeleteNode,
+ adminDownloadNodeBundle, getUserAccessibleNodes,
+ getAdminGroups, getNodeStreamUrl
+} from '../../../services/apiService';
+import NodeTerminal from "../components/NodeTerminal";
+import FileSystemNavigator from "../../../components/FileSystemNavigator";
+
+const NodesPage = ({ user }) => {
+ const [nodes, setNodes] = useState([]);
+ const [groups, setGroups] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [nodeToDelete, setNodeToDelete] = useState(null);
+ const [newNode, setNewNode] = useState({ node_id: '', display_name: '', description: '', skill_config: { shell: { enabled: true }, sync: { enabled: true } } });
+ const [expandedTerminals, setExpandedTerminals] = useState({}); // node_id -> boolean
+ const [expandedNodes, setExpandedNodes] = useState({}); // node_id -> boolean
+ const [expandedFiles, setExpandedFiles] = useState({}); // node_id -> boolean
+ const [editingNodeId, setEditingNodeId] = useState(null);
+ const [editForm, setEditForm] = useState({
+ display_name: '',
+ description: '',
+ skill_config: {
+ shell: {
+ enabled: true,
+ sandbox: { mode: 'STRICT', allowed_commands: [], denied_commands: [], sensitive_commands: [] }
+ }
+ }
+ });
+
+ // WebSocket state for live updates
+ const [meshStatus, setMeshStatus] = useState({}); // node_id -> { status, stats }
+ const [recentEvents, setRecentEvents] = useState([]); // Array of event objects
+
+ const isAdmin = user?.role === 'admin';
+
+ const fetchData = useCallback(async () => {
+ setLoading(true);
+ try {
+ if (isAdmin) {
+ const [nodesData, groupsData] = await Promise.all([getAdminNodes(), getAdminGroups()]);
+ setNodes(nodesData);
+ setGroups(groupsData);
+ } else {
+ const nodesData = await getUserAccessibleNodes();
+ setNodes(nodesData);
+ }
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }, [isAdmin]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ // 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.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 => {
+ statusMap[n.node_id] = { status: n.status, stats: n.stats };
+ });
+ setMeshStatus(statusMap);
+ } else if (msg.event === 'mesh_heartbeat') {
+ setMeshStatus(prev => {
+ const statusMap = { ...prev };
+ msg.data.nodes.forEach(n => {
+ statusMap[n.node_id] = { status: n.status, stats: n.stats };
+ });
+ return statusMap;
+ });
+ } 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]);
+
+ const handleCreateNode = async (e) => {
+ e.preventDefault();
+ try {
+ await adminCreateNode(newNode);
+ setShowCreateModal(false);
+ fetchData();
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ const toggleNodeActive = async (node) => {
+ try {
+ await adminUpdateNode(node.node_id, { is_active: !node.is_active });
+ fetchData();
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ const confirmDeleteNode = async () => {
+ if (!nodeToDelete) return;
+ try {
+ await adminDeleteNode(nodeToDelete);
+ setNodeToDelete(null);
+ fetchData();
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ const startEditing = (node) => {
+ setEditingNodeId(node.node_id);
+ setEditForm({
+ display_name: node.display_name,
+ description: node.description,
+ skill_config: node.skill_config || {}
+ });
+ };
+
+ const handleUpdateNode = async (nodeId) => {
+ try {
+ await adminUpdateNode(nodeId, editForm);
+ setEditingNodeId(null);
+ fetchData();
+ } catch (err) {
+ alert(err.message);
+ }
+ };
+
+ // ─── Node Name Hover Tooltip ─────────────────────────────────────────────
+ const NodeNameWithTooltip = ({ node }) => {
+ const liveCaps = meshStatus[node.node_id]?.caps || {};
+ const caps = { ...(node.capabilities || {}), ...liveCaps };
+
+ const gpu = caps.gpu;
+ const arch = caps.arch === 'aarch64' ? 'arm64' : caps.arch;
+ const os = caps.os;
+ const osRelease = caps.os_release;
+ const hasCaps = caps && Object.keys(caps).length > 0;
+
+ const gpuColor = !gpu || gpu === 'none'
+ ? 'bg-gray-100 dark:bg-gray-700 text-gray-500'
+ : gpu === 'apple-silicon'
+ ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
+ : 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300';
+ const gpuIcon = (!gpu || gpu === 'none') ? '—' : gpu === 'apple-silicon' ? '🍎' : '🟢';
+ const gpuText = !gpu || gpu === 'none' ? 'No GPU' : gpu === 'apple-silicon' ? 'Apple GPU (Metal)' : gpu.split(',')[0].trim();
+
+ const osIcon = os === 'linux' ? '🐧' : os === 'darwin' ? '🍏' : os === 'windows' ? '🪟' : '💻';
+ const archIsArm = arch && (arch.startsWith('arm') || arch === 'arm64');
+
+ return (
+
+ {/* Node Name — the hover trigger */}
+
+ {node.display_name}
+
+
+ {/* Tooltip — appears below the name */}
+
+ {/* Arrow */}
+
+
+ {/* Header */}
+
+
{node.display_name}
+
{node.node_id}
+ {node.description && (
+
{node.description}
+ )}
+
+
+ {hasCaps ? (
+
+ {/* GPU Row */}
+
+ GPU
+
+ {gpuIcon} {gpuText}
+
+
+ {/* OS Row */}
+ {os && (
+
+ OS
+
+ {osIcon} {os === 'darwin' ? 'macOS' : os === 'linux' ? `Linux ${osRelease || ''}`.trim() : os}
+
+
+ )}
+ {/* Arch Row */}
+ {arch && (
+
+ Arch
+
+ {archIsArm ? '🔩' : '🔲'} {arch}
+
+
+ )}
+ {/* Registered Owner */}
+
+ Owner
+ {node.registered_by || 'system'}
+
+
+ ) : (
+
Capabilities available when node connects
+ )}
+
+
+ );
+ };
+ // ─────────────────────────────────────────────────────────────────────────
+
+ const NodeHealthMetrics = ({ node, compact = false }) => {
+ const live = meshStatus[node.node_id];
+ const stats = live?.stats || node.stats;
+
+ // Use live status if available, fallback to initial node status
+ const status = live?.status || node.last_status;
+ const isOnline = status === 'online' || status === 'idle' || status === 'busy' || (stats?.cpu_usage_percent > 0);
+
+ // If we are definitely offline in compact mode, show the spacer
+ if (!isOnline && compact) return
;
+
+ const cpu = stats?.cpu_usage_percent || 0;
+ const mem = stats?.memory_usage_percent || 0;
+ const cpuCount = stats?.cpu_count || 0;
+ const memUsed = (stats?.memory_used_gb || 0).toFixed(1);
+ const memTotal = (stats?.memory_total_gb || 0).toFixed(1);
+
+ // Rich Metrics (M6 additions)
+ const cpuFreq = stats?.cpu_freq_mhz || 0;
+ const loadAvg = stats?.load_avg || [0, 0, 0];
+ const perCore = stats?.cpu_usage_per_core || [];
+ const memAvail = (stats?.memory_available_gb || 0).toFixed(1);
+
+ return (
+
+ {/* CPU Metric */}
+
+
+
CPU
+
+ {cpu.toFixed(0)}%
+ {compact && cpuCount > 0 && (
+ ({cpuCount}c)
+ )}
+
+
+
+
+ {/* Rich Tooltip (CPU) - Popping DOWN to avoid header clipping */}
+
+ {/* Tooltip Arrow (Top) */}
+
+
+ CPU Details
+ {cpu.toFixed(1)}%
+
+
+
+ Cores:
+ {cpuCount} Threads
+
+ {cpuFreq > 0 && (
+
+ Frequency:
+ {(cpuFreq / 1000).toFixed(2)} GHz
+
+ )}
+
+ Load (1/5/15):
+ {loadAvg.map(l => l.toFixed(2)).join(' ')}
+
+
+ {perCore.length > 0 && (
+
+
Per-Core Distribution
+
+ {perCore.map((c, i) => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* RAM Metric */}
+
+
+
RAM
+
+ {mem.toFixed(0)}%
+ {compact && memTotal > 0 && (
+ ({memUsed}G)
+ )}
+
+
+
+
+ {/* Rich Tooltip (RAM) - Popping DOWN to avoid header clipping */}
+
+ {/* Tooltip Arrow (Top) */}
+
+
+ Memory Hub
+ {mem.toFixed(1)}%
+
+
+
+ Used:
+ {memUsed} GB
+
+
+ Available:
+ {memAvail} GB
+
+
+ Total Physical:
+ {memTotal} GB
+
+
+
+
+
+ Used
+ {100 - mem > 10 ? 'Free' : ''}
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ 🚀 Agent Node Mesh
+
+
+ {isAdmin
+ ? "Manage distributed execution nodes and monitor live health."
+ : "Monitor the health and availability of your accessible agent nodes."}
+
+
+
+
+
+
+
+
+ {isAdmin && (
+
setShowCreateModal(true)}
+ className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center shadow-md active:scale-95"
+ >
+
+
+
+ Register Node
+
+ )}
+
+
+
+
+ {/* Main Content */}
+
+ {loading ? (
+
+ ) : error ? (
+
+ Error: {error}
+
+ ) : (
+
+
+ {nodes.map(node => (
+
+ {/* ── Top Row (Mobile-friendly) ─────────────────────────── */}
+
+
+ {/* Row 1: Status pill + Name + (desktop) CPU/RAM */}
+
+ {/* Status dot */}
+
+
+
+ {meshStatus[node.node_id]?.status || node.last_status || 'offline'}
+
+
+
+ {/* Name (tooltip) — takes remaining width */}
+
+
+ {/* Desktop-only CPU/RAM inline */}
+
+
+
+
+
+ {/* Mobile-only: CPU/RAM row below name */}
+
+
+
+
+ {/* Row 2: Action toolbar */}
+
+ {/* Active/Disabled pill — slim on mobile */}
+
toggleNodeActive(node)}
+ className={`text-[9px] font-black uppercase tracking-widest px-2.5 py-1 rounded-full border transition-all duration-200 ${node.is_active
+ ? 'bg-indigo-600 text-white border-indigo-600 shadow-md shadow-indigo-500/20'
+ : 'bg-transparent text-gray-400 border-gray-300 dark:border-gray-600'
+ }`}
+ >
+ {node.is_active ? '● Active' : '○ Disabled'}
+
+
+ {/* Icon action buttons */}
+
+
setExpandedTerminals(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
+ className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedTerminals[node.node_id] ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600' : 'text-gray-400 hover:text-indigo-600 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
+ title="Interactive Terminal"
+ >
+
+
+
setExpandedFiles(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
+ className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedFiles[node.node_id] ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600' : 'text-gray-400 hover:text-amber-600 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
+ title="File Navigator"
+ >
+
+
+
setExpandedNodes(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
+ className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedNodes[node.node_id] ? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800'}`}
+ title="Settings & Details"
+ >
+
+
+
+
+ {isAdmin && (
+ <>
+
+
setNodeToDelete(node.node_id)}
+ className="p-1.5 sm:p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
+ title="Deregister"
+ >
+
+
+ >
+ )}
+
+
+
+
+ {/* Expanded Panels */}
+ {(expandedNodes[node.node_id] || expandedTerminals[node.node_id] || expandedFiles[node.node_id]) && (
+
+ {expandedNodes[node.node_id] && (
+
+
+
+
General Configuration
+ {isAdmin && (
+ editingNodeId !== node.node_id ? (
+
startEditing(node)} className="text-xs font-bold text-indigo-600 hover:underline">Edit Node
+ ) : (
+
+ handleUpdateNode(node.node_id)} className="text-xs font-bold text-green-600 hover:underline">Save Changes
+ setEditingNodeId(null)} className="text-xs font-bold text-gray-500 hover:underline">Cancel
+
+ )
+ )}
+
+
+
+
Node Display Name
+ {editingNodeId === node.node_id ? (
+
setEditForm({ ...editForm, display_name: e.target.value })}
+ />
+ ) : (
+
+ {node.display_name}
+
+ )}
+
+
+
Description
+ {editingNodeId === node.node_id ? (
+
+
+
+
+
Skills & Resources
+
+
+
File Sync Capability
+ {editingNodeId === node.node_id ? (
+
+ setEditForm({
+ ...editForm,
+ skill_config: {
+ ...editForm.skill_config,
+ sync: { ...editForm.skill_config.sync, enabled: e.target.checked }
+ }
+ })}
+ />
+ Enable Sync
+
+ ) : (
+
+ {node.skill_config?.sync?.enabled ? 'Active' : 'Disabled'}
+
+ )}
+
+
+
+
Terminal Access
+ {editingNodeId === node.node_id ? (
+
+ setEditForm({
+ ...editForm,
+ skill_config: {
+ ...editForm.skill_config,
+ shell: { ...editForm.skill_config.shell, enabled: e.target.checked }
+ }
+ })}
+ />
+ Enable Shell
+
+ ) : (
+
+ {node.skill_config?.shell?.enabled ? 'Active' : 'Disabled'}
+
+ )}
+
+
+ {/* SANDBOX POLICY CONFIGURATION — New M6 Feature */}
+ {editingNodeId === node.node_id && editForm.skill_config?.shell?.enabled ? (
+
+
+
+
+ {editForm.skill_config.shell.sandbox?.mode === 'STRICT' ? 'Allowed' : 'Blacklisted'} Commands
+ {
+ const list = e.target.value.split(',').map(s => s.trim()).filter(s => s);
+ const field = editForm.skill_config.shell.sandbox?.mode === 'STRICT' ? 'allowed_commands' : 'denied_commands';
+ setEditForm({
+ ...editForm,
+ skill_config: {
+ ...editForm.skill_config,
+ shell: {
+ ...editForm.skill_config.shell,
+ sandbox: { ...editForm.skill_config.shell.sandbox, [field]: list }
+ }
+ }
+ });
+ }}
+ />
+
+
+ ) : (
+ node.skill_config?.shell?.enabled && (
+
+
+ Sandbox:
+
+ {node.skill_config.shell.sandbox?.mode || 'PASSIVE'}
+
+
+
+ {node.skill_config.shell.sandbox?.mode === 'STRICT'
+ ? `Allow: ${node.skill_config.shell.sandbox?.allowed_commands?.join(', ') || 'None'}`
+ : `Block: ${node.skill_config.shell.sandbox?.denied_commands?.join(', ') || 'None'}`}
+
+
+ )
+ )}
+
+
* Skills are defined via the Node Manifest in the bundle.
+
+
+ {isAdmin && (
+
+
+
adminDownloadNodeBundle(node.node_id)}
+ className="flex-1 bg-white dark:bg-gray-700 border dark:border-gray-600 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg text-xs font-bold hover:bg-gray-50 dark:hover:bg-gray-600 shadow-sm transition-all flex items-center justify-center active:scale-95"
+ >
+
+ Download Bundle (M5)
+
+
+
+
+
+
One-Liner Deployment (Headless)
+
+ curl -sSL '{window.location.origin}/api/v1/nodes/provision/{node.node_id}?token={node.invite_token}' | python3
+
+
{
+ const cmd = `curl -sSL '${window.location.origin}/api/v1/nodes/provision/${node.node_id}?token=${node.invite_token}' | python3`;
+ navigator.clipboard.writeText(cmd);
+ alert("Provisioning command copied!");
+ }}
+ className="absolute top-2 right-2 p-1.5 bg-gray-800 hover:bg-indigo-600 text-gray-400 hover:text-white rounded-lg transition-all opacity-0 group-hover/cmd:opacity-100"
+ title="Copy to clipboard"
+ >
+
+
+
+
Best for terminal-only servers. Installs agent as a persistent service.
+
+
+ )}
+
+
+
+ )}
+
+ {expandedFiles[node.node_id] && (
+
+
+
Node File Explorer
+
Browse & synchronize files
+
+
+
+
+
+ )}
+
+ {expandedTerminals[node.node_id] && (
+
+
+
Node Interactive Shell
+ Direct gRPC Proxy
+
+
+
+ )}
+
+ )}
+
+ ))}
+
+
+ {/* Event Timeline (Execution Live Bus) — Debug/Tracing, collapsed by default */}
+
+
+
+
+ Execution Live Bus
+ Debug
+
+
+
+
+
+
+
+
+ {recentEvents.length === 0 && (
+
Listening for mesh events...
+ )}
+ {recentEvents.map((evt, i) => (
+
+ [{evt.timestamp?.split('T')[1].split('.')[0]}]
+ {evt.node_id?.slice(0, 8)}
+
+ {evt.label || evt.event}:
+ {JSON.stringify(evt.data)}
+
+
+ ))}
+
+
+
+
+ )}
+
+
+ {/* CREATE NODE MODAL */}
+ {showCreateModal && (
+
+
+
+
Register Agent Node
+
setShowCreateModal(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700">
+
+
+
+
+
+
+ )}
+
+ {/* DELETE NODE MODAL */}
+ {nodeToDelete && (
+
+
+
+
+
Deregister Node?
+
+ Are you sure you want to completely deregister node {nodeToDelete} ? This will permanently remove all access grants for this node.
+
+
+ setNodeToDelete(null)}
+ className="px-5 py-2.5 text-sm font-semibold text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-xl transition-colors"
+ >
+ Cancel
+
+
+ Deregister Node
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default NodesPage;
diff --git a/frontend/src/pages/NodesPage.js b/frontend/src/pages/NodesPage.js
deleted file mode 100644
index ba51333..0000000
--- a/frontend/src/pages/NodesPage.js
+++ /dev/null
@@ -1,894 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import {
- getAdminNodes, adminCreateNode, adminUpdateNode, adminDeleteNode,
- adminDownloadNodeBundle, getUserAccessibleNodes,
- getAdminGroups, getNodeStreamUrl
-} from '../services/apiService';
-import NodeTerminal from '../components/NodeTerminal';
-import FileSystemNavigator from '../components/FileSystemNavigator';
-
-const NodesPage = ({ user }) => {
- const [nodes, setNodes] = useState([]);
- const [groups, setGroups] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [showCreateModal, setShowCreateModal] = useState(false);
- const [nodeToDelete, setNodeToDelete] = useState(null);
- const [newNode, setNewNode] = useState({ node_id: '', display_name: '', description: '', skill_config: { shell: { enabled: true }, sync: { enabled: true } } });
- const [expandedTerminals, setExpandedTerminals] = useState({}); // node_id -> boolean
- const [expandedNodes, setExpandedNodes] = useState({}); // node_id -> boolean
- const [expandedFiles, setExpandedFiles] = useState({}); // node_id -> boolean
- const [editingNodeId, setEditingNodeId] = useState(null);
- const [editForm, setEditForm] = useState({
- display_name: '',
- description: '',
- skill_config: {
- shell: {
- enabled: true,
- sandbox: { mode: 'STRICT', allowed_commands: [], denied_commands: [], sensitive_commands: [] }
- }
- }
- });
-
- // WebSocket state for live updates
- const [meshStatus, setMeshStatus] = useState({}); // node_id -> { status, stats }
- const [recentEvents, setRecentEvents] = useState([]); // Array of event objects
-
- const isAdmin = user?.role === 'admin';
-
- const fetchData = useCallback(async () => {
- setLoading(true);
- try {
- if (isAdmin) {
- const [nodesData, groupsData] = await Promise.all([getAdminNodes(), getAdminGroups()]);
- setNodes(nodesData);
- setGroups(groupsData);
- } else {
- const nodesData = await getUserAccessibleNodes();
- setNodes(nodesData);
- }
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }, [isAdmin]);
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
-
- // 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.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 => {
- statusMap[n.node_id] = { status: n.status, stats: n.stats };
- });
- setMeshStatus(statusMap);
- } else if (msg.event === 'mesh_heartbeat') {
- setMeshStatus(prev => {
- const statusMap = { ...prev };
- msg.data.nodes.forEach(n => {
- statusMap[n.node_id] = { status: n.status, stats: n.stats };
- });
- return statusMap;
- });
- } 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]);
-
- const handleCreateNode = async (e) => {
- e.preventDefault();
- try {
- await adminCreateNode(newNode);
- setShowCreateModal(false);
- fetchData();
- } catch (err) {
- alert(err.message);
- }
- };
-
- const toggleNodeActive = async (node) => {
- try {
- await adminUpdateNode(node.node_id, { is_active: !node.is_active });
- fetchData();
- } catch (err) {
- alert(err.message);
- }
- };
-
- const confirmDeleteNode = async () => {
- if (!nodeToDelete) return;
- try {
- await adminDeleteNode(nodeToDelete);
- setNodeToDelete(null);
- fetchData();
- } catch (err) {
- alert(err.message);
- }
- };
-
- const startEditing = (node) => {
- setEditingNodeId(node.node_id);
- setEditForm({
- display_name: node.display_name,
- description: node.description,
- skill_config: node.skill_config || {}
- });
- };
-
- const handleUpdateNode = async (nodeId) => {
- try {
- await adminUpdateNode(nodeId, editForm);
- setEditingNodeId(null);
- fetchData();
- } catch (err) {
- alert(err.message);
- }
- };
-
- // ─── Node Name Hover Tooltip ─────────────────────────────────────────────
- const NodeNameWithTooltip = ({ node }) => {
- const liveCaps = meshStatus[node.node_id]?.caps || {};
- const caps = { ...(node.capabilities || {}), ...liveCaps };
-
- const gpu = caps.gpu;
- const arch = caps.arch === 'aarch64' ? 'arm64' : caps.arch;
- const os = caps.os;
- const osRelease = caps.os_release;
- const hasCaps = caps && Object.keys(caps).length > 0;
-
- const gpuColor = !gpu || gpu === 'none'
- ? 'bg-gray-100 dark:bg-gray-700 text-gray-500'
- : gpu === 'apple-silicon'
- ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
- : 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300';
- const gpuIcon = (!gpu || gpu === 'none') ? '—' : gpu === 'apple-silicon' ? '🍎' : '🟢';
- const gpuText = !gpu || gpu === 'none' ? 'No GPU' : gpu === 'apple-silicon' ? 'Apple GPU (Metal)' : gpu.split(',')[0].trim();
-
- const osIcon = os === 'linux' ? '🐧' : os === 'darwin' ? '🍏' : os === 'windows' ? '🪟' : '💻';
- const archIsArm = arch && (arch.startsWith('arm') || arch === 'arm64');
-
- return (
-
- {/* Node Name — the hover trigger */}
-
- {node.display_name}
-
-
- {/* Tooltip — appears below the name */}
-
- {/* Arrow */}
-
-
- {/* Header */}
-
-
{node.display_name}
-
{node.node_id}
- {node.description && (
-
{node.description}
- )}
-
-
- {hasCaps ? (
-
- {/* GPU Row */}
-
- GPU
-
- {gpuIcon} {gpuText}
-
-
- {/* OS Row */}
- {os && (
-
- OS
-
- {osIcon} {os === 'darwin' ? 'macOS' : os === 'linux' ? `Linux ${osRelease || ''}`.trim() : os}
-
-
- )}
- {/* Arch Row */}
- {arch && (
-
- Arch
-
- {archIsArm ? '🔩' : '🔲'} {arch}
-
-
- )}
- {/* Registered Owner */}
-
- Owner
- {node.registered_by || 'system'}
-
-
- ) : (
-
Capabilities available when node connects
- )}
-
-
- );
- };
- // ─────────────────────────────────────────────────────────────────────────
-
- const NodeHealthMetrics = ({ node, compact = false }) => {
- const live = meshStatus[node.node_id];
- const stats = live?.stats || node.stats;
-
- // Use live status if available, fallback to initial node status
- const status = live?.status || node.last_status;
- const isOnline = status === 'online' || status === 'idle' || status === 'busy' || (stats?.cpu_usage_percent > 0);
-
- // If we are definitely offline in compact mode, show the spacer
- if (!isOnline && compact) return
;
-
- const cpu = stats?.cpu_usage_percent || 0;
- const mem = stats?.memory_usage_percent || 0;
- const cpuCount = stats?.cpu_count || 0;
- const memUsed = (stats?.memory_used_gb || 0).toFixed(1);
- const memTotal = (stats?.memory_total_gb || 0).toFixed(1);
-
- // Rich Metrics (M6 additions)
- const cpuFreq = stats?.cpu_freq_mhz || 0;
- const loadAvg = stats?.load_avg || [0, 0, 0];
- const perCore = stats?.cpu_usage_per_core || [];
- const memAvail = (stats?.memory_available_gb || 0).toFixed(1);
-
- return (
-
- {/* CPU Metric */}
-
-
-
CPU
-
- {cpu.toFixed(0)}%
- {compact && cpuCount > 0 && (
- ({cpuCount}c)
- )}
-
-
-
-
- {/* Rich Tooltip (CPU) - Popping DOWN to avoid header clipping */}
-
- {/* Tooltip Arrow (Top) */}
-
-
- CPU Details
- {cpu.toFixed(1)}%
-
-
-
- Cores:
- {cpuCount} Threads
-
- {cpuFreq > 0 && (
-
- Frequency:
- {(cpuFreq / 1000).toFixed(2)} GHz
-
- )}
-
- Load (1/5/15):
- {loadAvg.map(l => l.toFixed(2)).join(' ')}
-
-
- {perCore.length > 0 && (
-
-
Per-Core Distribution
-
- {perCore.map((c, i) => (
-
- ))}
-
-
- )}
-
-
-
-
- {/* RAM Metric */}
-
-
-
RAM
-
- {mem.toFixed(0)}%
- {compact && memTotal > 0 && (
- ({memUsed}G)
- )}
-
-
-
-
- {/* Rich Tooltip (RAM) - Popping DOWN to avoid header clipping */}
-
- {/* Tooltip Arrow (Top) */}
-
-
- Memory Hub
- {mem.toFixed(1)}%
-
-
-
- Used:
- {memUsed} GB
-
-
- Available:
- {memAvail} GB
-
-
- Total Physical:
- {memTotal} GB
-
-
-
-
-
- Used
- {100 - mem > 10 ? 'Free' : ''}
-
-
-
-
-
-
- );
- };
-
- return (
-
- {/* Header */}
-
-
-
-
- 🚀 Agent Node Mesh
-
-
- {isAdmin
- ? "Manage distributed execution nodes and monitor live health."
- : "Monitor the health and availability of your accessible agent nodes."}
-
-
-
-
-
-
-
-
- {isAdmin && (
-
setShowCreateModal(true)}
- className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all flex items-center shadow-md active:scale-95"
- >
-
-
-
- Register Node
-
- )}
-
-
-
-
- {/* Main Content */}
-
- {loading ? (
-
- ) : error ? (
-
- Error: {error}
-
- ) : (
-
-
- {nodes.map(node => (
-
- {/* ── Top Row (Mobile-friendly) ─────────────────────────── */}
-
-
- {/* Row 1: Status pill + Name + (desktop) CPU/RAM */}
-
- {/* Status dot */}
-
-
-
- {meshStatus[node.node_id]?.status || node.last_status || 'offline'}
-
-
-
- {/* Name (tooltip) — takes remaining width */}
-
-
- {/* Desktop-only CPU/RAM inline */}
-
-
-
-
-
- {/* Mobile-only: CPU/RAM row below name */}
-
-
-
-
- {/* Row 2: Action toolbar */}
-
- {/* Active/Disabled pill — slim on mobile */}
-
toggleNodeActive(node)}
- className={`text-[9px] font-black uppercase tracking-widest px-2.5 py-1 rounded-full border transition-all duration-200 ${node.is_active
- ? 'bg-indigo-600 text-white border-indigo-600 shadow-md shadow-indigo-500/20'
- : 'bg-transparent text-gray-400 border-gray-300 dark:border-gray-600'
- }`}
- >
- {node.is_active ? '● Active' : '○ Disabled'}
-
-
- {/* Icon action buttons */}
-
-
setExpandedTerminals(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
- className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedTerminals[node.node_id] ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600' : 'text-gray-400 hover:text-indigo-600 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
- title="Interactive Terminal"
- >
-
-
-
setExpandedFiles(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
- className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedFiles[node.node_id] ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600' : 'text-gray-400 hover:text-amber-600 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
- title="File Navigator"
- >
-
-
-
setExpandedNodes(prev => ({ ...prev, [node.node_id]: !prev[node.node_id] }))}
- className={`p-1.5 sm:p-2 rounded-lg transition-colors ${expandedNodes[node.node_id] ? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800'}`}
- title="Settings & Details"
- >
-
-
-
-
- {isAdmin && (
- <>
-
-
setNodeToDelete(node.node_id)}
- className="p-1.5 sm:p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
- title="Deregister"
- >
-
-
- >
- )}
-
-
-
-
- {/* Expanded Panels */}
- {(expandedNodes[node.node_id] || expandedTerminals[node.node_id] || expandedFiles[node.node_id]) && (
-
- {expandedNodes[node.node_id] && (
-
-
-
-
General Configuration
- {isAdmin && (
- editingNodeId !== node.node_id ? (
-
startEditing(node)} className="text-xs font-bold text-indigo-600 hover:underline">Edit Node
- ) : (
-
- handleUpdateNode(node.node_id)} className="text-xs font-bold text-green-600 hover:underline">Save Changes
- setEditingNodeId(null)} className="text-xs font-bold text-gray-500 hover:underline">Cancel
-
- )
- )}
-
-
-
-
Node Display Name
- {editingNodeId === node.node_id ? (
-
setEditForm({ ...editForm, display_name: e.target.value })}
- />
- ) : (
-
- {node.display_name}
-
- )}
-
-
-
Description
- {editingNodeId === node.node_id ? (
-
setEditForm({ ...editForm, description: e.target.value })}
- />
- ) : (
-
- {node.description || "No description provided."}
-
- )}
-
-
-
-
-
Skills & Resources
-
-
-
File Sync Capability
- {editingNodeId === node.node_id ? (
-
- setEditForm({
- ...editForm,
- skill_config: {
- ...editForm.skill_config,
- sync: { ...editForm.skill_config.sync, enabled: e.target.checked }
- }
- })}
- />
- Enable Sync
-
- ) : (
-
- {node.skill_config?.sync?.enabled ? 'Active' : 'Disabled'}
-
- )}
-
-
-
-
Terminal Access
- {editingNodeId === node.node_id ? (
-
- setEditForm({
- ...editForm,
- skill_config: {
- ...editForm.skill_config,
- shell: { ...editForm.skill_config.shell, enabled: e.target.checked }
- }
- })}
- />
- Enable Shell
-
- ) : (
-
- {node.skill_config?.shell?.enabled ? 'Active' : 'Disabled'}
-
- )}
-
-
- {/* SANDBOX POLICY CONFIGURATION — New M6 Feature */}
- {editingNodeId === node.node_id && editForm.skill_config?.shell?.enabled ? (
-
-
-
-
- {editForm.skill_config.shell.sandbox?.mode === 'STRICT' ? 'Allowed' : 'Blacklisted'} Commands
- {
- const list = e.target.value.split(',').map(s => s.trim()).filter(s => s);
- const field = editForm.skill_config.shell.sandbox?.mode === 'STRICT' ? 'allowed_commands' : 'denied_commands';
- setEditForm({
- ...editForm,
- skill_config: {
- ...editForm.skill_config,
- shell: {
- ...editForm.skill_config.shell,
- sandbox: { ...editForm.skill_config.shell.sandbox, [field]: list }
- }
- }
- });
- }}
- />
-
-
- ) : (
- node.skill_config?.shell?.enabled && (
-
-
- Sandbox:
-
- {node.skill_config.shell.sandbox?.mode || 'PASSIVE'}
-
-
-
- {node.skill_config.shell.sandbox?.mode === 'STRICT'
- ? `Allow: ${node.skill_config.shell.sandbox?.allowed_commands?.join(', ') || 'None'}`
- : `Block: ${node.skill_config.shell.sandbox?.denied_commands?.join(', ') || 'None'}`}
-
-
- )
- )}
-
-
* Skills are defined via the Node Manifest in the bundle.
-
-
- {isAdmin && (
-
-
-
adminDownloadNodeBundle(node.node_id)}
- className="flex-1 bg-white dark:bg-gray-700 border dark:border-gray-600 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg text-xs font-bold hover:bg-gray-50 dark:hover:bg-gray-600 shadow-sm transition-all flex items-center justify-center active:scale-95"
- >
-
- Download Bundle (M5)
-
-
-
-
-
-
One-Liner Deployment (Headless)
-
- curl -sSL '{window.location.origin}/api/v1/nodes/provision/{node.node_id}?token={node.invite_token}' | python3
-
-
{
- const cmd = `curl -sSL '${window.location.origin}/api/v1/nodes/provision/${node.node_id}?token=${node.invite_token}' | python3`;
- navigator.clipboard.writeText(cmd);
- alert("Provisioning command copied!");
- }}
- className="absolute top-2 right-2 p-1.5 bg-gray-800 hover:bg-indigo-600 text-gray-400 hover:text-white rounded-lg transition-all opacity-0 group-hover/cmd:opacity-100"
- title="Copy to clipboard"
- >
-
-
-
-
Best for terminal-only servers. Installs agent as a persistent service.
-
-
- )}
-
-
-
- )}
-
- {expandedFiles[node.node_id] && (
-
-
-
Node File Explorer
-
Browse & synchronize files
-
-
-
-
-
- )}
-
- {expandedTerminals[node.node_id] && (
-
-
-
Node Interactive Shell
- Direct gRPC Proxy
-
-
-
- )}
-
- )}
-
- ))}
-
-
- {/* Event Timeline (Execution Live Bus) — Debug/Tracing, collapsed by default */}
-
-
-
-
- Execution Live Bus
- Debug
-
-
-
-
-
-
-
-
- {recentEvents.length === 0 && (
-
Listening for mesh events...
- )}
- {recentEvents.map((evt, i) => (
-
- [{evt.timestamp?.split('T')[1].split('.')[0]}]
- {evt.node_id?.slice(0, 8)}
-
- {evt.label || evt.event}:
- {JSON.stringify(evt.data)}
-
-
- ))}
-
-
-
-
- )}
-
-
- {/* CREATE NODE MODAL */}
- {showCreateModal && (
-
-
-
-
Register Agent Node
-
setShowCreateModal(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700">
-
-
-
-
-
- Node identifier (Slug)
- setNewNode({ ...newNode, node_id: e.target.value })}
- />
-
-
- Display Name
- setNewNode({ ...newNode, display_name: e.target.value })}
- />
-
-
- Description
- setNewNode({ ...newNode, description: e.target.value })}
- />
-
-
-
- setShowCreateModal(false)} className="px-5 py-2 text-sm font-semibold text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors">Cancel
-
- Register Node
-
-
-
-
-
- )}
-
- {/* DELETE NODE MODAL */}
- {nodeToDelete && (
-
-
-
-
-
Deregister Node?
-
- Are you sure you want to completely deregister node {nodeToDelete} ? This will permanently remove all access grants for this node.
-
-
- setNodeToDelete(null)}
- className="px-5 py-2.5 text-sm font-semibold text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-xl transition-colors"
- >
- Cancel
-
-
- Deregister Node
-
-
-
-
-
- )}
-
- );
-};
-
-export default NodesPage;