import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import { apiFetch } from '@/lib/api'; export interface Node { id: number; name: string; type: 'local' | 'remote'; compose_dir: string; is_default: boolean; status: 'online' | 'offline' | 'unknown'; created_at: number; api_url?: string; api_token?: string; } interface NodeContextType { nodes: Node[]; activeNode: Node | null; setActiveNode: (node: Node) => void; refreshNodes: () => Promise; isLoading: boolean; } const NodeContext = createContext(undefined); export function NodeProvider({ children }: { children: React.ReactNode }) { const [nodes, setNodes] = useState([]); const [activeNode, setActiveNodeState] = useState(null); const [isLoading, setIsLoading] = useState(true); // Ref lets refreshNodes read current activeNode without being a dep (breaks infinite loop) const activeNodeRef = useRef(null); activeNodeRef.current = activeNode; const refreshNodes = useCallback(async () => { try { const res = await apiFetch('/nodes'); if (res.ok) { const data = await res.json(); setNodes(data); const currentActive = activeNodeRef.current; if (!currentActive) { const defaultNode = data.find((n: Node) => n.is_default); if (defaultNode) { setActiveNodeState(defaultNode); } else if (data.length > 0) { setActiveNodeState(data[0]); } } else { const updatedActive = data.find((n: Node) => n.id === currentActive.id); if (updatedActive) { setActiveNodeState(updatedActive); } else { const defaultNode = data.find((n: Node) => n.is_default); if (defaultNode) { setActiveNodeState(defaultNode); } else if (data.length > 0) { setActiveNodeState(data[0]); } else { setActiveNodeState(null); } } } } } catch (error) { console.error('Failed to fetch nodes:', error); } finally { setIsLoading(false); } }, []); // stable - reads activeNode via ref, not closure capture const setActiveNode = useCallback((node: Node) => { setActiveNodeState(node); localStorage.setItem('sencho-active-node', String(node.id)); }, []); useEffect(() => { refreshNodes(); const handleNodeNotFound = () => { console.warn('[NodeContext] Active node is unreachable or deleted. Forcing sync...'); refreshNodes(); }; window.addEventListener('node-not-found', handleNodeNotFound); return () => window.removeEventListener('node-not-found', handleNodeNotFound); }, [refreshNodes]); return ( {children} ); } export function useNodes() { const context = useContext(NodeContext); if (!context) { throw new Error('useNodes must be used within a NodeProvider'); } return context; }