import { useState, useEffect, useMemo, useCallback } from 'react'; import { api } from '../api/client'; import { useWebSocket } from '../hooks/useWebSocket'; import type { Agent, HashrateSample } from '../types'; import HashrateChart from '../components/Charts/HashrateChart'; import NeonCard from '../components/NeonCard/NeonCard'; import AgentRemoteActions from '../components/Fleet/AgentRemoteActions'; import AgentListItem from '../components/Fleet/AgentListItem'; import FleetToolbar from '../components/Fleet/FleetToolbar'; import { DEFAULT_FLEET_FILTERS, filterFleetAgents, agentIsIdleMiner, formatHashrate, formatUptime, } from '../help/fleetFilters'; import type { FleetFilterState } from '../help/fleetFilters'; import '../components/Fleet/FleetPanels.css'; import '../components/Fleet/FleetToolbar.css'; import '../components/Fleet/AgentRemoteActions.css'; import './Pages.css'; export default function AgentsPage() { const { agents: liveAgents, isConnected, agentLogs, latestMessage } = useWebSocket(); const [agents, setAgents] = useState([]); const [selectedAgent, setSelectedAgent] = useState(null); const [expandedId, setExpandedId] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const [filters, setFilters] = useState(DEFAULT_FLEET_FILTERS); const [bulkBusy, setBulkBusy] = useState(false); const [hashrateHistory, setHashrateHistory] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [logContent, setLogContent] = useState(''); const [logLoading, setLogLoading] = useState(false); const [notesDraft, setNotesDraft] = useState(''); const [tagsDraft, setTagsDraft] = useState(''); const [metaSaving, setMetaSaving] = useState(false); const [metaMsg, setMetaMsg] = useState(''); useEffect(() => { api.listAgents() .then(setAgents) .catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load agents')) .finally(() => setLoading(false)); }, []); useEffect(() => { if (!isConnected) return; setAgents(liveAgents); if (!selectedAgent) return; const updated = liveAgents.find((a) => a.id === selectedAgent.id); if (updated) { setSelectedAgent(updated); setNotesDraft(updated.notes || ''); setTagsDraft((updated.tags || []).join(', ')); } else { setSelectedAgent(null); setLogContent(''); } }, [liveAgents, isConnected, selectedAgent?.id]); useEffect(() => { if (selectedAgent && agentLogs[selectedAgent.id]) { setLogContent(agentLogs[selectedAgent.id]); } }, [selectedAgent?.id, agentLogs]); const filteredAgents = useMemo( () => filterFleetAgents(agents, filters), [agents, filters] ); const refreshLog = async (refresh = false) => { if (!selectedAgent) return; setLogLoading(true); try { const res = await api.getAgentLog(selectedAgent.id, refresh); setLogContent(res.content || ''); } catch (err) { setLogContent(err instanceof Error ? err.message : 'Failed to load log'); } finally { setLogLoading(false); } }; const selectAgent = async (agent: Agent) => { setSelectedAgent(agent); setNotesDraft(agent.notes || ''); setTagsDraft((agent.tags || []).join(', ')); setMetaMsg(''); setLogContent(''); try { const history = await api.getAgentStats(agent.id, 60); setHashrateHistory(history); } catch (err) { console.error(err); } }; const saveMeta = async () => { if (!selectedAgent) return; setMetaSaving(true); setMetaMsg(''); const tags = tagsDraft.split(',').map((t) => t.trim()).filter(Boolean); try { const res = await api.updateAgentMeta(selectedAgent.id, notesDraft, tags); const updated = res.agent; setAgents((prev) => prev.map((a) => (a.id === updated.id ? { ...a, ...updated } : a))); setSelectedAgent((prev) => (prev?.id === updated.id ? { ...prev, ...updated } : prev)); setMetaMsg('Saved'); setTimeout(() => setMetaMsg(''), 2000); } catch (err) { setMetaMsg(err instanceof Error ? err.message : 'Save failed'); } finally { setMetaSaving(false); } }; const toggleSelect = useCallback((id: string, on: boolean) => { setSelectedIds((prev) => { const next = new Set(prev); if (on) next.add(id); else next.delete(id); return next; }); }, []); const handleBulkAction = async (action: string) => { const ids = [...selectedIds]; if (ids.length === 0) return; let targetIds = ids; if (action === 'restart_idle') { targetIds = agents.filter((a) => ids.includes(a.id) && agentIsIdleMiner(a)).map((a) => a.id); if (targetIds.length === 0) { alert('No selected online agents with idle hashrate (< 100 H/s).'); return; } action = 'restart'; } const onlineIds = targetIds.filter((id) => agents.find((a) => a.id === id)?.status === 'online'); if (onlineIds.length === 0) { alert('No online agents in selection.'); return; } if (action === 'stop' && !window.confirm(`Stop miner on ${onlineIds.length} agent(s)?`)) return; setBulkBusy(true); try { await api.sendBulkCommand(onlineIds, action); } catch (err) { console.error(err); } finally { setBulkBusy(false); } }; return (

FLEET REGISTRY

Fleet Roster

Compact list — click a row to expand quick actions or inspect full telemetry on the right.

{filteredAgents.length}/{agents.length} NODES
{loadError && (

{loadError}

)} {loading ? (

Scanning network...

) : agents.length === 0 ? (

No agents registered

Deploy a miner to a Windows machine and it will appear here automatically.

) : (
{filteredAgents.map((agent) => ( toggleSelect(agent.id, on)} onSelect={() => void selectAgent(agent)} onToggleExpand={() => setExpandedId((prev) => (prev === agent.id ? null : agent.id))} latestWsMessage={latestMessage} /> ))} {filteredAgents.length === 0 && (

No agents match filters.

)}
{selectedAgent && (

{selectedAgent.name}

{(selectedAgent.tags?.length ?? 0) > 0 && (
{selectedAgent.tags!.map((t) => ( {t} ))}
)}

Notes & Tags

Labels like "Living room PC" or "Rack B" — stored on the server, shown on list cards.