Newer
Older
cortex-hub / ui / client-app / src / pages / NodesPage.js
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 }, browser: { 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: {} });

    // 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 (
            <div className="relative group/name flex-1 min-w-0">
                {/* Node Name — the hover trigger */}
                <h3 className="text-base font-bold text-gray-900 dark:text-white truncate cursor-default underline decoration-dotted decoration-gray-400 dark:decoration-gray-600 underline-offset-2">
                    {node.display_name}
                </h3>

                {/* Tooltip — appears below the name */}
                <div className="absolute top-full left-0 mt-2 w-72 bg-white dark:bg-gray-900 border dark:border-gray-700 rounded-2xl shadow-2xl p-4 opacity-0 group-hover/name:opacity-100 pointer-events-none transition-all duration-200 scale-95 group-hover/name:scale-100 z-[200]">
                    {/* Arrow */}
                    <div className="absolute -top-1.5 left-6 w-3 h-3 bg-white dark:bg-gray-900 border-l border-t dark:border-gray-700 rotate-45" />

                    {/* Header */}
                    <div className="mb-3 pb-2 border-b dark:border-gray-700">
                        <div className="text-xs font-black text-gray-700 dark:text-gray-200">{node.display_name}</div>
                        <div className="text-[10px] font-mono text-gray-400 mt-0.5">{node.node_id}</div>
                        {node.description && (
                            <div className="text-[10px] text-gray-500 italic mt-1">{node.description}</div>
                        )}
                    </div>

                    {hasCaps ? (
                        <div className="space-y-2">
                            {/* GPU Row */}
                            <div className="flex items-center justify-between">
                                <span className="text-[9px] font-black uppercase tracking-widest text-gray-400">GPU</span>
                                <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${gpuColor}`}>
                                    {gpuIcon} {gpuText}
                                </span>
                            </div>
                            {/* OS Row */}
                            {os && (
                                <div className="flex items-center justify-between">
                                    <span className="text-[9px] font-black uppercase tracking-widest text-gray-400">OS</span>
                                    <span className="text-[10px] font-bold text-amber-600 dark:text-amber-400">
                                        {osIcon} {os === 'darwin' ? 'macOS' : os === 'linux' ? `Linux ${osRelease || ''}`.trim() : os}
                                    </span>
                                </div>
                            )}
                            {/* Arch Row */}
                            {arch && (
                                <div className="flex items-center justify-between">
                                    <span className="text-[9px] font-black uppercase tracking-widest text-gray-400">Arch</span>
                                    <span className={`text-[10px] font-bold ${archIsArm ? 'text-rose-600 dark:text-rose-400' : 'text-teal-600 dark:text-teal-400'}`}>
                                        {archIsArm ? '🔩' : '🔲'} {arch}
                                    </span>
                                </div>
                            )}
                            {/* Registered Owner */}
                            <div className="flex items-center justify-between pt-1 mt-1 border-t dark:border-gray-700">
                                <span className="text-[9px] font-black uppercase tracking-widest text-gray-400">Owner</span>
                                <span className="text-[10px] font-mono text-gray-500 truncate max-w-[150px]">{node.registered_by || 'system'}</span>
                            </div>
                        </div>
                    ) : (
                        <div className="text-[10px] text-gray-400 italic text-center py-2">Capabilities available when node connects</div>
                    )}
                </div>
            </div>
        );
    };
    // ─────────────────────────────────────────────────────────────────────────

    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 <div className="ml-6 min-w-[140px]" />;

        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 (
            <div className={`flex items-center space-x-4 shrink-0 transition-opacity duration-500 ${compact ? 'ml-6' : 'mt-4 pt-4 border-t dark:border-gray-700'}`}>
                {/* CPU Metric */}
                <div className="flex-1 min-w-[70px] group/metric relative p-1.5 -m-1.5 rounded-lg cursor-help">
                    <div className="flex justify-between items-center mb-1">
                        <span className="text-[9px] font-black text-gray-400 uppercase tracking-tighter">CPU</span>
                        <div className="flex items-center space-x-1">
                            <span className="text-[9px] font-mono text-indigo-500 font-bold">{cpu.toFixed(0)}%</span>
                            {compact && cpuCount > 0 && (
                                <span className="text-[8px] text-gray-400 opacity-30 group-hover:opacity-100 transition-opacity">({cpuCount}c)</span>
                            )}
                        </div>
                    </div>
                    <div className="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-1.5 overflow-hidden shadow-inner">
                        <div className="bg-gradient-to-r from-indigo-500 to-blue-400 h-full transition-all duration-1000 shadow-[0_0_5px_rgba(99,102,241,0.5)]" style={{ width: `${Math.max(2, cpu)}%` }}></div>
                    </div>

                    {/* Rich Tooltip (CPU) - Popping DOWN to avoid header clipping */}
                    <div className="absolute top-full left-1/2 -translate-x-1/2 mt-3 w-56 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border dark:border-gray-700 p-3 opacity-0 group-hover/metric:opacity-100 transition-all duration-200 z-[100] transform scale-95 group-hover/metric:scale-100 pointer-events-none">
                        {/* Tooltip Arrow (Top) */}
                        <div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-white dark:bg-gray-800 border-l border-t dark:border-gray-700 rotate-45"></div>
                        <div className="flex items-center justify-between mb-2">
                            <span className="text-[10px] font-black text-gray-400 uppercase">CPU Details</span>
                            <span className="text-[10px] font-mono text-indigo-500 font-bold">{cpu.toFixed(1)}%</span>
                        </div>
                        <div className="space-y-2">
                            <div className="flex justify-between text-[9px]">
                                <span className="text-gray-500 text-[8px] uppercase font-bold">Cores:</span>
                                <span className="font-bold whitespace-nowrap">{cpuCount} Threads</span>
                            </div>
                            {cpuFreq > 0 && (
                                <div className="flex justify-between text-[9px]">
                                    <span className="text-gray-500 text-[8px] uppercase font-bold">Frequency:</span>
                                    <span className="font-bold whitespace-nowrap">{(cpuFreq / 1000).toFixed(2)} GHz</span>
                                </div>
                            )}
                            <div className="flex justify-between text-[9px]">
                                <span className="text-gray-500 text-[8px] uppercase font-bold">Load (1/5/15):</span>
                                <span className="font-bold font-mono text-[8px] whitespace-nowrap">{loadAvg.map(l => l.toFixed(2)).join(' ')}</span>
                            </div>

                            {perCore.length > 0 && (
                                <div className="pt-2 mt-2 border-t dark:border-gray-700">
                                    <div className="text-[8px] font-bold text-gray-400 uppercase mb-2">Per-Core Distribution</div>
                                    <div className="grid grid-cols-4 gap-1.5">
                                        {perCore.map((c, i) => (
                                            <div key={i} className="h-5 bg-gray-100 dark:bg-gray-900 rounded overflow-hidden relative border dark:border-gray-800" title={`Core ${i}: ${c}%`}>
                                                <div className="absolute bottom-0 left-0 right-0 bg-indigo-500/40 transition-all duration-500" style={{ height: `${c}%` }}></div>
                                                <span className="absolute inset-0 flex items-center justify-center text-[7px] font-mono text-gray-400 font-bold">{i}</span>
                                            </div>
                                        ))}
                                    </div>
                                </div>
                            )}
                        </div>
                    </div>
                </div>

                {/* RAM Metric */}
                <div className="flex-1 min-w-[70px] group/metric relative p-1.5 -m-1.5 rounded-lg cursor-help">
                    <div className="flex justify-between items-center mb-1">
                        <span className="text-[9px] font-black text-gray-400 uppercase tracking-tighter">RAM</span>
                        <div className="flex items-center space-x-1">
                            <span className="text-[9px] font-mono text-pink-500 font-bold">{mem.toFixed(0)}%</span>
                            {compact && memTotal > 0 && (
                                <span className="text-[8px] text-gray-400 opacity-30 group-hover:opacity-100 transition-opacity">({memUsed}G)</span>
                            )}
                        </div>
                    </div>
                    <div className="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-1.5 overflow-hidden shadow-inner">
                        <div className="bg-gradient-to-r from-pink-500 to-rose-400 h-full transition-all duration-1000 shadow-[0_0_5px_rgba(236,72,153,0.5)]" style={{ width: `${Math.max(2, mem)}%` }}></div>
                    </div>

                    {/* Rich Tooltip (RAM) - Popping DOWN to avoid header clipping */}
                    <div className="absolute top-full left-1/2 -translate-x-1/2 mt-3 w-56 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border dark:border-gray-700 p-3 opacity-0 group-hover/metric:opacity-100 transition-all duration-200 z-[100] transform scale-95 group-hover/metric:scale-100 pointer-events-none">
                        {/* Tooltip Arrow (Top) */}
                        <div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-white dark:bg-gray-800 border-l border-t dark:border-gray-700 rotate-45"></div>
                        <div className="flex items-center justify-between mb-2">
                            <span className="text-[10px] font-black text-gray-400 uppercase">Memory Hub</span>
                            <span className="text-[10px] font-mono text-pink-500 font-bold">{mem.toFixed(1)}%</span>
                        </div>
                        <div className="space-y-2">
                            <div className="flex justify-between text-[9px]">
                                <span className="text-gray-500 text-[8px] uppercase font-bold">Used:</span>
                                <span className="font-bold whitespace-nowrap">{memUsed} GB</span>
                            </div>
                            <div className="flex justify-between text-[9px]">
                                <span className="text-gray-500 text-[8px] uppercase font-bold">Available:</span>
                                <span className="font-bold whitespace-nowrap">{memAvail} GB</span>
                            </div>
                            <div className="flex justify-between text-[9px]">
                                <span className="text-gray-500 text-[8px] uppercase font-bold">Total Physical:</span>
                                <span className="font-bold whitespace-nowrap">{memTotal} GB</span>
                            </div>

                            <div className="pt-2 mt-2 border-t dark:border-gray-700">
                                <div className="w-full bg-gray-100 dark:bg-gray-900 h-2.5 rounded-full overflow-hidden flex border dark:border-gray-800 shadow-inner">
                                    <div className="bg-pink-500 h-full transition-all duration-1000" style={{ width: `${mem}%` }}></div>
                                    <div className="bg-gray-200 dark:bg-gray-700 h-full" style={{ width: `${100 - mem}%` }}></div>
                                </div>
                                <div className="flex justify-between text-[7px] mt-1.5 text-gray-400 font-black tracking-widest uppercase">
                                    <span>Used</span>
                                    <span>{100 - mem > 10 ? 'Free' : ''}</span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    };

    return (
        <div className="flex flex-col h-full bg-gray-50 dark:bg-gray-900 overflow-hidden text-gray-900 dark:text-gray-100">
            {/* Header */}
            <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-4 sm:px-8 py-4 sm:py-6">
                <div className="flex justify-between items-center">
                    <div>
                        <h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white flex items-center">
                            <span className="mr-2">🚀</span> Agent Node Mesh
                        </h1>
                        <p className="text-gray-500 dark:text-gray-400 text-xs sm:text-sm mt-1">
                            {isAdmin
                                ? "Manage distributed execution nodes and monitor live health."
                                : "Monitor the health and availability of your accessible agent nodes."}
                        </p>
                    </div>
                    <div className="flex space-x-2">
                        <button
                            onClick={fetchData}
                            className="p-2 text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
                            title="Refresh List"
                        >
                            <svg className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
                            </svg>
                        </button>
                        {isAdmin && (
                            <button
                                onClick={() => 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"
                            >
                                <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
                                </svg>
                                Register Node
                            </button>
                        )}
                    </div>
                </div>
            </header>

            {/* Main Content */}
            <main className="flex-1 overflow-auto p-3 sm:p-6 md:p-8">
                {loading ? (
                    <div className="flex items-center justify-center h-64">
                        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500"></div>
                    </div>
                ) : error ? (
                    <div className="bg-red-50 dark:bg-red-900/20 text-red-600 p-4 rounded-xl border border-red-200 dark:border-red-800">
                        Error: {error}
                    </div>
                ) : (
                    <div className="space-y-12">
                        <div className="grid grid-cols-1 gap-6">
                            {nodes.map(node => (
                                <div key={node.node_id} className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border dark:border-gray-700 transition-all duration-300 relative hover:z-50 z-10">
                                    {/* ── Top Row (Mobile-friendly) ─────────────────────────── */}
                                    <div className="p-3 sm:p-5 hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-t-2xl transition-colors group relative">

                                        {/* Row 1: Status pill + Name + (desktop) CPU/RAM */}
                                        <div className="flex items-start gap-2">
                                            {/* Status dot */}
                                            <div className="flex items-center space-x-1.5 shrink-0 mt-1">
                                                <div className={`w-2 h-2 rounded-full ${meshStatus[node.node_id]?.status === 'online' || node.last_status === 'online' ? 'bg-green-500 animate-pulse shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-400 opacity-50'}`}></div>
                                                <span className={`text-[9px] font-black uppercase tracking-tighter ${meshStatus[node.node_id]?.status === 'online' || node.last_status === 'online' ? 'text-green-500' : 'text-gray-500'}`}>
                                                    {meshStatus[node.node_id]?.status || node.last_status || 'offline'}
                                                </span>
                                            </div>

                                            {/* Name (tooltip) — takes remaining width */}
                                            <div className="flex-1 min-w-0">
                                                <NodeNameWithTooltip node={node} />
                                                <div className="text-[9px] font-mono text-gray-400 truncate opacity-60 mt-0.5">ID: {node.node_id}</div>
                                            </div>

                                            {/* Desktop-only CPU/RAM inline */}
                                            <div className="hidden sm:block shrink-0">
                                                <NodeHealthMetrics node={node} compact={true} />
                                            </div>
                                        </div>

                                        {/* Mobile-only: CPU/RAM row below name */}
                                        <div className="sm:hidden mt-2">
                                            <NodeHealthMetrics node={node} compact={true} />
                                        </div>

                                        {/* Row 2: Action toolbar */}
                                        <div className="flex items-center justify-between mt-3 pt-3 border-t dark:border-gray-700">
                                            {/* Active/Disabled pill — slim on mobile */}
                                            <button
                                                onClick={() => 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'}
                                            </button>

                                            {/* Icon action buttons */}
                                            <div className="flex items-center gap-1">
                                                <button
                                                    onClick={() => 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"
                                                >
                                                    <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8v-4m0 4l-4-4m4 4l4-4" /></svg>
                                                </button>
                                                <button
                                                    onClick={() => 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"
                                                >
                                                    <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
                                                </button>
                                                <button
                                                    onClick={() => 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"
                                                >
                                                    <svg className={`w-4 h-4 transition-transform duration-200 ${expandedNodes[node.node_id] ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                                                    </svg>
                                                </button>
                                                {isAdmin && (
                                                    <>
                                                        <div className="w-px h-4 bg-gray-200 dark:bg-gray-700 mx-0.5" />
                                                        <button
                                                            onClick={() => 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"
                                                        >
                                                            <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
                                                        </button>
                                                    </>
                                                )}
                                            </div>
                                        </div>
                                    </div>

                                    {/* Expanded Panels */}
                                    {(expandedNodes[node.node_id] || expandedTerminals[node.node_id] || expandedFiles[node.node_id]) && (
                                        <div className="border-t dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/30 p-5 space-y-6">
                                            {expandedNodes[node.node_id] && (
                                                <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
                                                    <div className="space-y-5">
                                                        <div className="flex justify-between items-center mb-1">
                                                            <h4 className="text-xs font-black uppercase tracking-widest text-gray-500">General Configuration</h4>
                                                            {editingNodeId !== node.node_id ? (
                                                                <button onClick={() => startEditing(node)} className="text-xs font-bold text-indigo-600 hover:underline">Edit Node</button>
                                                            ) : (
                                                                <div className="space-x-4">
                                                                    <button onClick={() => handleUpdateNode(node.node_id)} className="text-xs font-bold text-green-600 hover:underline">Save Changes</button>
                                                                    <button onClick={() => setEditingNodeId(null)} className="text-xs font-bold text-gray-500 hover:underline">Cancel</button>
                                                                </div>
                                                            )}
                                                        </div>
                                                        <div className="space-y-4">
                                                            <div>
                                                                <label className="block text-[10px] font-bold text-gray-400 uppercase mb-1 px-1">Node Display Name</label>
                                                                {editingNodeId === node.node_id ? (
                                                                    <input
                                                                        className="w-full bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 transition-all dark:text-white"
                                                                        value={editForm.display_name}
                                                                        onChange={e => setEditForm({ ...editForm, display_name: e.target.value })}
                                                                    />
                                                                ) : (
                                                                    <div className="text-sm font-medium text-gray-700 dark:text-gray-300 px-3 py-2 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-transparent italic">
                                                                        {node.display_name}
                                                                    </div>
                                                                )}
                                                            </div>
                                                            <div>
                                                                <label className="block text-[10px] font-bold text-gray-400 uppercase mb-1 px-1">Description</label>
                                                                {editingNodeId === node.node_id ? (
                                                                    <textarea
                                                                        className="w-full bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 transition-all dark:text-white"
                                                                        value={editForm.description}
                                                                        onChange={e => setEditForm({ ...editForm, description: e.target.value })}
                                                                    />
                                                                ) : (
                                                                    <div className="text-sm text-gray-600 dark:text-gray-400 px-3 py-2 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-transparent min-h-[5rem]">
                                                                        {node.description || "No description provided."}
                                                                    </div>
                                                                )}
                                                            </div>
                                                        </div>
                                                    </div>
                                                    <div className="space-y-5">
                                                        <h4 className="text-xs font-black uppercase tracking-widest text-gray-500">Skills & Resources</h4>
                                                        <div className="bg-white dark:bg-gray-800/50 rounded-xl border dark:border-gray-700 p-4 space-y-4 shadow-sm">
                                                            <div className="flex items-center justify-between transition-all duration-200 group">
                                                                <span className="text-xs font-medium text-gray-700 dark:text-gray-300">File Sync Capability</span>
                                                                <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${node.skill_config?.sync?.enabled ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400' : 'bg-gray-100 text-gray-500'}`}>
                                                                    {node.skill_config?.sync?.enabled ? 'Active' : 'Disabled'}
                                                                </span>
                                                            </div>
                                                            <div className="flex items-center justify-between transition-all duration-200 group">
                                                                <span className="text-xs font-medium text-gray-700 dark:text-gray-300">Terminal Access</span>
                                                                <span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${node.skill_config?.shell?.enabled ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-400' : 'bg-gray-100 text-gray-500'}`}>
                                                                    {node.skill_config?.shell?.enabled ? 'Active' : 'Disabled'}
                                                                </span>
                                                            </div>
                                                            <p className="text-[10px] text-gray-400 mt-3 italic">* Skills are defined via the Node Manifest in the bundle.</p>
                                                        </div>
                                                        <div className="pt-2 border-t dark:border-gray-700 flex flex-wrap gap-2">
                                                            <button
                                                                onClick={() => 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"
                                                            >
                                                                <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
                                                                Download Bundle (M5)
                                                            </button>
                                                        </div>
                                                    </div>
                                                </div>
                                            )}

                                            {expandedFiles[node.node_id] && (
                                                <div className="pt-5 border-t dark:border-gray-700">
                                                    <div className="flex items-center justify-between mb-4">
                                                        <h4 className="text-xs font-black uppercase tracking-widest text-amber-500">Node File Explorer</h4>
                                                        <p className="text-[10px] text-gray-400 font-mono italic">Browse & synchronize files</p>
                                                    </div>
                                                    <div className="h-[450px]">
                                                        <FileSystemNavigator nodeId={node.node_id} />
                                                    </div>
                                                </div>
                                            )}

                                            {expandedTerminals[node.node_id] && (
                                                <div className="pt-5 border-t dark:border-gray-700">
                                                    <div className="flex justify-between items-center mb-4">
                                                        <h4 className="text-xs font-black uppercase tracking-widest text-indigo-500">Node Interactive Shell</h4>
                                                        <span className="text-[10px] font-mono text-gray-400">Direct gRPC Proxy</span>
                                                    </div>
                                                    <NodeTerminal nodeId={node.node_id} />
                                                </div>
                                            )}
                                        </div>
                                    )}
                                </div>
                            ))}
                        </div>

                        {/* Event Timeline (Execution Live Bus) — Debug/Tracing, collapsed by default */}
                        <details className="group mt-4 mb-8">
                            <summary className="flex items-center justify-between cursor-pointer select-none px-4 py-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700/70 transition-colors">
                                <div className="flex items-center gap-2">
                                    <span className="w-1.5 h-1.5 rounded-full bg-gray-400 dark:bg-gray-500" />
                                    <span className="text-[10px] font-black uppercase tracking-widest text-gray-400 dark:text-gray-500">Execution Live Bus</span>
                                    <span className="text-[9px] px-1.5 py-0.5 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 font-bold">Debug</span>
                                </div>
                                <svg className="w-3.5 h-3.5 text-gray-400 transition-transform group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 9l-7 7-7-7" />
                                </svg>
                            </summary>

                            <div className="bg-white dark:bg-gray-800 rounded-b-2xl border border-t-0 dark:border-gray-700 overflow-hidden flex flex-col h-[280px]">
                                <div className="flex-1 overflow-y-auto p-4 space-y-3 font-mono text-[11px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 bg-gray-50/30 dark:bg-black/20">
                                    {recentEvents.length === 0 && (
                                        <div className="text-gray-400 text-center py-8 italic">Listening for mesh events...</div>
                                    )}
                                    {recentEvents.map((evt, i) => (
                                        <div key={i} className="flex space-x-3 items-center py-1 border-b border-gray-100 dark:border-gray-800 last:border-0 hover:bg-gray-100/50 dark:hover:bg-white/5 transition-colors">
                                            <span className="text-indigo-400 flex-shrink-0 font-bold opacity-70">[{evt.timestamp?.split('T')[1].split('.')[0]}]</span>
                                            <span className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-[9px] text-gray-500 font-bold">{evt.node_id?.slice(0, 8)}</span>
                                            <span className={`flex-grow break-all ${evt.event === 'task_error' ? 'text-red-500' : 'text-gray-700 dark:text-gray-200'}`}>
                                                <span className="font-black uppercase text-[9px] mr-2 opacity-50">{evt.label || evt.event}:</span>
                                                {JSON.stringify(evt.data)}
                                            </span>
                                        </div>
                                    ))}
                                </div>
                            </div>
                        </details>
                    </div>
                )}
            </main>

            {/* CREATE NODE MODAL */}
            {showCreateModal && (
                <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
                    <div className="bg-white dark:bg-gray-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 border dark:border-gray-700">
                        <div className="px-6 py-4 border-b dark:border-gray-700 flex justify-between items-center bg-gray-50 dark:bg-gray-900/40">
                            <h3 className="font-bold text-gray-900 dark:text-white">Register Agent Node</h3>
                            <button onClick={() => 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">
                                <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
                            </button>
                        </div>
                        <form onSubmit={handleCreateNode} className="p-6 space-y-4">
                            <div>
                                <label className="block text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1.5 ml-1">Node identifier (Slug)</label>
                                <input
                                    required
                                    className="w-full bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-700 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-indigo-500 transition-all dark:text-white outline-none"
                                    placeholder="e.g. macbook-m3-local"
                                    value={newNode.node_id}
                                    onChange={e => setNewNode({ ...newNode, node_id: e.target.value })}
                                />
                            </div>
                            <div>
                                <label className="block text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1.5 ml-1">Display Name</label>
                                <input
                                    required
                                    className="w-full bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-700 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-indigo-500 transition-all dark:text-white outline-none"
                                    placeholder="e.g. My Primary MacBook"
                                    value={newNode.display_name}
                                    onChange={e => setNewNode({ ...newNode, display_name: e.target.value })}
                                />
                            </div>
                            <div>
                                <label className="block text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-1.5 ml-1">Description</label>
                                <textarea
                                    className="w-full bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-700 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-indigo-500 transition-all dark:text-white h-24 resize-none outline-none"
                                    placeholder="What is this node used for?"
                                    value={newNode.description}
                                    onChange={e => setNewNode({ ...newNode, description: e.target.value })}
                                />
                            </div>

                            <div className="flex justify-end space-x-2 pt-4">
                                <button type="button" onClick={() => 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</button>
                                <button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-2 rounded-xl text-sm font-bold shadow-lg shadow-indigo-600/20 active:scale-95 transition-all">
                                    Register Node
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
            )}

            {/* DELETE NODE MODAL */}
            {nodeToDelete && (
                <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
                    <div className="bg-white dark:bg-gray-800 rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 border dark:border-gray-700">
                        <div className="p-6 text-center">
                            <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
                                <svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
                                </svg>
                            </div>
                            <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">Deregister Node?</h3>
                            <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
                                Are you sure you want to completely deregister node <span className="font-mono font-bold text-gray-700 dark:text-gray-300">{nodeToDelete}</span>? This will permanently remove all access grants for this node.
                            </p>
                            <div className="flex justify-center space-x-3 mt-2">
                                <button
                                    onClick={() => 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
                                </button>
                                <button
                                    onClick={confirmDeleteNode}
                                    className="px-5 py-2.5 text-sm font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl shadow-lg shadow-red-600/20 active:scale-95 transition-all"
                                >
                                    Deregister Node
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            )}
        </div>
    );
};

export default NodesPage;