import { useEffect, useRef, useCallback, useState } from 'react'; import type { WSDashboardInit, WSAgentOffline, WSStatsUpdate, WSCommandResult, WSAgentLog, } from '../types/ws'; import type { Agent, Share, FleetAlert, PoolStatus, AIActivityEntry, WSMessage } from '../types'; interface UseWebSocketReturn { isConnected: boolean; agents: Agent[]; recentShares: Share[]; fleetAlerts: FleetAlert[]; poolStatus: PoolStatus[]; aiActivity: AIActivityEntry[]; agentLogs: Record; latestMessage: WSMessage | null; } export function useWebSocket(): UseWebSocketReturn { const wsRef = useRef(null); const reconnectTimer = useRef | null>(null); const unmounted = useRef(false); const [isConnected, setIsConnected] = useState(false); const [agents, setAgents] = useState([]); const [recentShares, setRecentShares] = useState([]); const [fleetAlerts, setFleetAlerts] = useState([]); const [poolStatus, setPoolStatus] = useState([]); const [aiActivity, setAiActivity] = useState([]); const [agentLogs, setAgentLogs] = useState>({}); const [latestMessage, setLatestMessage] = useState(null); const connect = useCallback(() => { if (unmounted.current) return; if (reconnectTimer.current) { clearTimeout(reconnectTimer.current); reconnectTimer.current = null; } const existing = wsRef.current; if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) { existing.close(); } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/dashboard`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { if (!unmounted.current) setIsConnected(true); }; ws.onclose = () => { if (unmounted.current) return; setIsConnected(false); if (reconnectTimer.current) clearTimeout(reconnectTimer.current); reconnectTimer.current = setTimeout(connect, 3000); }; ws.onerror = () => { ws.close(); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data) as WSMessage; setLatestMessage(msg); switch (msg.type) { case 'init': { const data = msg.payload as WSDashboardInit; if (data.agents) setAgents(data.agents); break; } case 'agent_online': { const agent = msg.payload as Agent; setAgents((prev) => { const idx = prev.findIndex((a) => a.id === agent.id); if (idx >= 0) { const updated = [...prev]; updated[idx] = { ...updated[idx], ...agent }; return updated; } return [...prev, agent]; }); break; } case 'agent_offline': { const { agent_id } = msg.payload as WSAgentOffline; setAgents((prev) => prev.map((a) => a.id === agent_id ? { ...a, status: 'offline' as const } : a ) ); break; } case 'stats_update': { const update = msg.payload as WSStatsUpdate; setAgents((prev) => prev.map((a) => a.id === update.agent_id ? { ...a, hashrate_15s: update.hashrate_15s, hashrate_1m: update.hashrate_1m, hashrate_15m: update.hashrate_15m, cpu_usage_pct: update.cpu_usage_pct, memory_usage_pct: update.memory_usage_pct ?? a.memory_usage_pct, uptime_seconds: update.uptime_seconds ?? a.uptime_seconds, shares_total: update.shares_submitted ?? a.shares_total, shares_good: update.shares_accepted ?? a.shares_good, shares_bad: Math.max( 0, (update.shares_submitted ?? a.shares_total) - (update.shares_accepted ?? a.shares_good) ), status: 'online' as const, } : a ) ); break; } case 'new_share': { const share = msg.payload as Share; setRecentShares((prev) => [share, ...prev].slice(0, 50)); break; } case 'fleet_alert': { const alert = msg.payload as FleetAlert; setFleetAlerts((prev) => [alert, ...prev].slice(0, 20)); break; } case 'pool_status': { const pools = msg.payload as PoolStatus[]; if (Array.isArray(pools)) setPoolStatus(pools); break; } case 'ai_activity': { const entry = msg.payload as AIActivityEntry; setAiActivity((prev) => { const idx = prev.findIndex((a) => a.agent_id === entry.agent_id); if (idx >= 0) { const next = [...prev]; next[idx] = entry; return next; } return [...prev, entry]; }); break; } case 'command_result': { const p = msg.payload as WSCommandResult; const agent_id = p.agent_id; if (agent_id && p.action === 'get_log' && p.success && p.message) { setAgentLogs((prev) => ({ ...prev, [agent_id]: p.message! })); } break; } case 'agent_log': { const { agent_id, content } = msg.payload as WSAgentLog; if (agent_id) { setAgentLogs((prev) => ({ ...prev, [agent_id]: content })); } break; } } } catch (err) { console.error('Failed to parse WebSocket message:', err); } }; }, []); useEffect(() => { unmounted.current = false; connect(); return () => { unmounted.current = true; if (reconnectTimer.current) { clearTimeout(reconnectTimer.current); } const ws = wsRef.current; if (ws) { ws.onclose = null; ws.close(); } }; }, [connect]); return { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity, agentLogs, latestMessage }; }