import { useCallback, useEffect, useMemo, useState } from "react"; import { Plus, Server, Wifi, WifiOff, Globe, RefreshCw, X } from "lucide-react"; import "./NodesView.css"; import { useNodes } from "../hooks/useNodes"; import { useProjects } from "../hooks/useProjects"; import { useNodeSettingsSync, computeSyncState } from "../hooks/useNodeSettingsSync"; import type { NodeInfo, NodeUpdateInput } from "../api"; import { NodeCard } from "./NodeCard"; import { MeshTopology } from "./MeshTopology"; import { AddNodeModal, type AddNodeInput } from "./AddNodeModal"; import { NodeDetailModal } from "./NodeDetailModal"; import type { ToastType } from "../hooks/useToast"; interface NodesViewProps { addToast: (message: string, type?: ToastType) => void; onClose: () => void; } export function NodesView({ addToast, onClose }: NodesViewProps) { const { nodes, loading, error, refresh, register, update, unregister, healthCheck } = useNodes(); const { projects } = useProjects(); const { syncStatusMap, pushSettings, pullSettings, syncAuth, trackNode, getAuthSyncState, getAuthProviders } = useNodeSettingsSync(); const [addModalOpen, setAddModalOpen] = useState(false); const [selectedNode, setSelectedNode] = useState(null); // Track remote nodes for sync status polling useEffect(() => { const remoteNodes = nodes.filter((node) => node.type === "remote"); for (const node of remoteNodes) { trackNode(node.id); } }, [nodes, trackNode]); useEffect(() => { if (!selectedNode) return; const latest = nodes.find((node) => node.id === selectedNode.id) ?? null; setSelectedNode(latest); }, [nodes, selectedNode]); const stats = useMemo(() => { const total = nodes.length; const online = nodes.filter((node) => node.status === "online").length; const offline = nodes.filter((node) => node.status === "offline" || node.status === "error").length; const remote = nodes.filter((node) => node.type === "remote").length; const synced = nodes.filter( (node) => node.type === "remote" && syncStatusMap[node.id] && computeSyncState(syncStatusMap[node.id]).syncState === "synced" ).length; return { total, online, offline, remote, synced }; }, [nodes, syncStatusMap]); const handleRegister = useCallback(async (input: AddNodeInput) => { await register(input); }, [register]); const handleRefresh = useCallback(async () => { try { await refresh(); } catch { addToast("Failed to refresh nodes", "error"); } }, [addToast, refresh]); const handleHealthCheck = useCallback(async (id: string) => { try { await healthCheck(id); addToast("Node health check complete", "success"); } catch (err) { const message = err instanceof Error ? err.message : "Health check failed"; addToast(message, "error"); } }, [addToast, healthCheck]); const handleUnregister = useCallback(async (id: string) => { try { await unregister(id); addToast("Node removed", "success"); if (selectedNode?.id === id) { setSelectedNode(null); } } catch (err) { const message = err instanceof Error ? err.message : "Failed to remove node"; addToast(message, "error"); } }, [addToast, selectedNode?.id, unregister]); const handleUpdate = useCallback(async (id: string, updates: NodeUpdateInput) => { await update(id, updates); }, [update]); return (

Nodes

{nodes.length} registered
Total {stats.total}
Online {stats.online}
Offline {stats.offline}
Remote {stats.remote}
Synced {stats.synced}
{error &&
{error}
} {/* Mesh Topology Visualization */} {!loading && nodes.length > 0 && (

Mesh Topology

)} {loading ? (
{Array.from({ length: 4 }).map((_, index) => (
))}
) : nodes.length === 0 ? (

No nodes are registered yet.

) : (
{nodes.map((node) => { const nodeSyncStatus = node.type === "remote" && syncStatusMap[node.id] ? computeSyncState(syncStatusMap[node.id]) : undefined; return ( { void handleHealthCheck(id); }} onEdit={(selected) => setSelectedNode(selected)} onRemove={(id) => { void handleUnregister(id); }} isLoading={loading} syncStatus={nodeSyncStatus} authSyncState={node.type === "remote" ? getAuthSyncState(node.id) : undefined} authSyncProviders={node.type === "remote" ? getAuthProviders(node.id) : undefined} /> ); })}
)} setAddModalOpen(false)} onSubmit={handleRegister} addToast={addToast} /> setSelectedNode(null)} node={selectedNode} projects={projects} onUpdate={handleUpdate} onHealthCheck={handleHealthCheck} addToast={addToast} syncStatus={selectedNode?.type === "remote" && selectedNode && syncStatusMap[selectedNode.id] ? computeSyncState(syncStatusMap[selectedNode.id]) : undefined} onPushSettings={pushSettings} onPullSettings={pullSettings} onSyncAuth={syncAuth} />
); }