import { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import type { User, DroneRegistration, SubProcessStat } from '../lib/api'; import { droneApi } from '../lib/api'; import { socketClient } from '../lib/socket'; import SubProcessTable from '../components/SubProcessTable'; import DroneMonitor from '../components/DroneMonitor'; import LogRenderer from '../components/LogRenderer'; import type { LogRendererEntry } from '../components/LogRenderer'; interface DroneManagerProps { user: User | null; } interface ToastState { message: string; type: 'success' | 'error'; } let logIdCounter = 0; export default function DroneManager({ user }: DroneManagerProps) { const navigate = useNavigate(); const location = useLocation(); const [allDrones, setAllDrones] = useState([]); const [selectedDrone, setSelectedDrone] = useState(null); const [loading, setLoading] = useState(true); const [terminating, setTerminating] = useState(null); const [toast, setToast] = useState(null); // SubProcess state const [processStats, setProcessStats] = useState([]); const [selectedPid, setSelectedPid] = useState(null); // Live log state const [logEntries, setLogEntries] = useState([]); const [autoScroll, setAutoScroll] = useState(true); const logContainerRef = useRef(null); // Track cleanup const pollIntervalRef = useRef | null>(null); const subscribedDroneRef = useRef(null); const droneChangeGeneration = useRef(0); useEffect(() => { if (!user) { navigate('/sign-in'); return; } loadDrones(); }, [user]); // Socket lifecycle — subscribe, poll, listen when selectedDrone changes useEffect(() => { const gen = ++droneChangeGeneration.current; // Cleanup previous subscription if (subscribedDroneRef.current) { socketClient.unsubscribeDrone(subscribedDroneRef.current); subscribedDroneRef.current = null; } if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } if (!selectedDrone || selectedDrone.status === 'offline') { setProcessStats([]); setLogEntries([]); return; } const regId = selectedDrone._id; subscribedDroneRef.current = regId; setProcessStats([]); setLogEntries([]); // Subscribe to live drone events socketClient.subscribeDrone(regId); // Listen for log events const handleLog = (data: { timestamp: string; level: string; component: string; message: string; metadata?: unknown }) => { if (droneChangeGeneration.current !== gen) return; logIdCounter++; setLogEntries((prev) => { const entry: LogRendererEntry = { id: `log-${logIdCounter}`, timestamp: new Date(data.timestamp), level: data.level, component: data.component, message: data.message, metadata: data.metadata, }; const next = [...prev, entry]; if (next.length > 200) { return next.slice(next.length - 200); } return next; }); }; socketClient.on('drone:log', handleLog); // Poll process stats every 1s const poll = async () => { if (droneChangeGeneration.current !== gen) return; const result = await socketClient.requestProcessStats(regId); if (result.success && result.processes && droneChangeGeneration.current === gen) { setProcessStats(result.processes as SubProcessStat[]); } }; poll(); pollIntervalRef.current = setInterval(poll, 1000); return () => { // Mark this effect cycle as stale so async callbacks are no-ops droneChangeGeneration.current++; clearInterval(pollIntervalRef.current!); pollIntervalRef.current = null; socketClient.off('drone:log', handleLog); socketClient.unsubscribeDrone(regId); subscribedDroneRef.current = null; }; }, [selectedDrone]); // Auto-scroll log to bottom when new entries arrive useEffect(() => { if (autoScroll && logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [logEntries, autoScroll]); const handleScroll = () => { if (logContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current; const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; setAutoScroll(isNearBottom); } }; const loadDrones = async () => { try { const data = await droneApi.getForManager(); setAllDrones(data); // Auto-select from route state if provided const state = location.state as { droneId?: string } | null; if (state?.droneId) { const match = data.find((d) => d._id === state.droneId); if (match) { setSelectedDrone(match); } // Clear route state to avoid re-selecting on re-render window.history.replaceState({}, ''); } } catch (err) { console.error('Failed to load drones', err); showToast('Failed to load drones', 'error'); } finally { setLoading(false); } }; const showToast = (message: string, type: 'success' | 'error') => { setToast({ message, type }); setTimeout(() => setToast(null), 5000); }; const handleSelectDrone = (drone: DroneRegistration) => { setSelectedDrone(drone); setSelectedPid(null); }; const handleTerminate = async () => { if (!selectedDrone) return; if (selectedDrone.status === 'busy') { const confirmed = confirm('Are you sure you want to terminate the drone? Any work or operations currently in progress may be lost.'); if (!confirmed) return; } setTerminating(selectedDrone._id); try { const result = await droneApi.terminate(selectedDrone._id); if (result.message) { showToast(result.message, result.success ? 'success' : 'error'); } else { showToast('Drone terminated successfully', 'success'); } await loadDrones(); if (selectedDrone.status !== 'offline') { setSelectedDrone(null); } } catch (err) { console.error('Failed to terminate drone', err); showToast('Failed to terminate drone', 'error'); } finally { setTerminating(null); } }; const onlineDrones = allDrones.filter(d => d.status === 'available' || d.status === 'busy'); const offlineDrones = allDrones.filter(d => d.status === 'offline' || d.status === 'starting'); if (!user) { return (

Please sign in to view drones.

); } return (
{toast && (
{toast.message}
)}
{selectedDrone ? ( <>

Drone Details

{selectedDrone.status !== 'offline' && ( )}
Hostname
{selectedDrone.hostname}
Status
{selectedDrone.status}
Workspace
{selectedDrone.workspaceDir}
Registered
{new Date(selectedDrone.createdAt).toLocaleString()}

Drone Log {selectedDrone.status !== 'offline' && '(Live)'}

) : (

Select a drone to view details

Choose from the online or offline drone lists

)}
); }