import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { api } from '../api/client'; import { useWebSocket } from '../hooks/useWebSocket'; import type { Agent, HashrateSample, ServerInfo } from '../types'; import LatencyBadge from '../components/Fleet/LatencyBadge'; import HashrateChart from '../components/Charts/HashrateChart'; import { resolveChartSeries } from '../help/chartSampleData'; 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 { downloadScreenshotFromBase64, sanitizeScreenshotBase64 } from '../help/screenshotDownload'; import { groupsForAgent } from '../help/fleetGroups'; import { useFleetGroups } from '../hooks/useFleetGroups'; import CreateGroupModal from '../components/Fleet/CreateGroupModal'; import FleetGroupsStrip from '../components/Fleet/FleetGroupsStrip'; import '../components/Fleet/FleetToolbar.css'; import './Pages.css'; function QuickDeployPanel({ serverInfo }: { serverInfo: ServerInfo | null }) { const [copied, setCopied] = useState(null); const base = serverInfo?.suggested_url?.replace(/\/$/, '') ?? window.location.origin; const copy = (text: string, key: string) => { navigator.clipboard.writeText(text).then(() => { setCopied(key); setTimeout(() => setCopied(null), 2000); }); }; const ps1 = `iex (irm '${base}/install.ps1')`; const sh = `curl -sL ${base}/install.sh | bash`; const dlWin = `${base}/get?os=windows`; const dlLin = `${base}/get?os=linux`; const dlMac = `${base}/get?os=darwin`; const Row = ({ label, cmd, id }: { label: string; cmd: string; id: string }) => (
{label} {cmd}
); return (
One-liner Quick Deploy

Run any of these commands on a remote machine — the agent downloads itself and connects back automatically. No files to transfer manually.

Install & run (auto-launches)
Direct download only (saves file)
); } export default function AgentsPage() { const { agents: liveAgents, isConnected, agentLogs, commandResults } = 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 [serverInfo, setServerInfo] = useState(null); const [loadError, setLoadError] = useState(''); const [logContent, setLogContent] = useState(''); const [logLoading, setLogLoading] = useState(false); const [logDownloading, setLogDownloading] = useState(false); const [notesDraft, setNotesDraft] = useState(''); const [tagsDraft, setTagsDraft] = useState(''); const [metaSaving, setMetaSaving] = useState(false); const [metaMsg, setMetaMsg] = useState(''); const isConnectedRef = useRef(isConnected); isConnectedRef.current = isConnected; const screenshotWatchId = useRef(null); const screenshotSeqRef = useRef(0); const [showGroupModal, setShowGroupModal] = useState(false); const { groups, addGroup, removeGroup } = useFleetGroups(); const onlineAgentIds = useMemo( () => new Set(agents.filter((a) => a.status === 'online').map((a) => a.id)), [agents] ); useEffect(() => { if (!commandResults?.length || !screenshotWatchId.current) return; const watch = screenshotWatchId.current; for (const r of commandResults) { if (r._seq <= screenshotSeqRef.current) continue; if (r.agent_id !== watch || r.action !== 'screenshot') continue; screenshotSeqRef.current = r._seq; screenshotWatchId.current = null; const label = agents.find((a) => a.id === watch)?.name ?? watch.slice(0, 8); if (r.success && r.message) { const ok = downloadScreenshotFromBase64(sanitizeScreenshotBase64(r.message), label); if (!ok) alert(`Screenshot from ${label} failed — empty or invalid image.`); } else { alert(`Screenshot failed on ${label}: ${r.message ?? 'unknown error'}`); } break; } }, [commandResults, agents]); useEffect(() => { let cancelled = false; api.listAgents() .then((data) => { if (!cancelled && !isConnectedRef.current) setAgents(data); }) .catch((err) => { if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Failed to load agents'); }) .finally(() => { if (!cancelled) setLoading(false); }); api.getServerInfo().then(setServerInfo).catch(() => {}); return () => { cancelled = true; }; }, []); useEffect(() => { if (!selectedAgent) return; setNotesDraft(selectedAgent.notes || ''); setTagsDraft((selectedAgent.tags || []).join(', ')); }, [selectedAgent?.id]); useEffect(() => { if (!isConnected) return; setAgents(liveAgents); if (!selectedAgent) return; const updated = liveAgents.find((a) => a.id === selectedAgent.id); if (updated) { setSelectedAgent(updated); } else { setSelectedAgent(null); setLogContent(''); } }, [liveAgents, isConnected, selectedAgent?.id]); useEffect(() => { if (selectedAgent && agentLogs[selectedAgent.id]) { setLogContent(agentLogs[selectedAgent.id]); } }, [selectedAgent?.id, agentLogs]); // Sort: online first, then by last_seen desc, then alphabetical const sortedAgents = useMemo(() => [...agents].sort((a, b) => { if (a.status === 'online' && b.status !== 'online') return -1; if (a.status !== 'online' && b.status === 'online') return 1; const ta = a.last_seen ? new Date(a.last_seen).getTime() : 0; const tb = b.last_seen ? new Date(b.last_seen).getTime() : 0; if (tb !== ta) return tb - ta; return a.name.localeCompare(b.name); }), [agents]); const filteredAgents = useMemo( () => filterFleetAgents(sortedAgents, filters), [sortedAgents, 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); } // Auto-fetch sysinfo so the terminal is pre-populated immediately if (agent.status === 'online') { setTimeout(() => { api.sendAgentCommand(agent.id, 'sysinfo').catch(() => {}); }, 300); } }; 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 handleDeleteAgent = async (agentId: string) => { if (!window.confirm('Remove this machine from the fleet roster? This cannot be undone.')) return; try { await api.deleteAgent(agentId); setAgents((prev) => prev.filter((a) => a.id !== agentId)); if (selectedAgent?.id === agentId) setSelectedAgent(null); setSelectedIds((prev) => { const next = new Set(prev); next.delete(agentId); return next; }); } catch (err) { alert(err instanceof Error ? err.message : 'Delete failed'); } }; const handleUninstallAndDelete = async (agent: Agent) => { const label = agent.status === 'online' ? `Uninstall the miner from "${agent.name}" and remove it from the roster?` : `"${agent.name}" is offline — it cannot be remotely uninstalled. Remove from roster only?`; if (!window.confirm(label)) return; if (agent.status === 'online') { try { await api.sendAgentCommand(agent.id, 'uninstall', {}); } catch { // Non-fatal — proceed to delete the record regardless } } try { await api.deleteAgent(agent.id); setAgents((prev) => prev.filter((a) => a.id !== agent.id)); if (selectedAgent?.id === agent.id) setSelectedAgent(null); setSelectedIds((prev) => { const next = new Set(prev); next.delete(agent.id); return next; }); } catch (err) { alert(err instanceof Error ? err.message : 'Delete failed'); } }; const handleBulkAction = async (action: string) => { const ids = [...selectedIds]; if (ids.length === 0) return; if (action === 'delete') { if (!window.confirm(`Permanently remove ${ids.length} machine(s) from the fleet roster?`)) return; setBulkBusy(true); try { await api.bulkDeleteAgents(ids); setAgents((prev) => prev.filter((a) => !ids.includes(a.id))); if (selectedAgent && ids.includes(selectedAgent.id)) setSelectedAgent(null); setSelectedIds(new Set()); } catch (err) { alert(err instanceof Error ? err.message : 'Bulk delete failed'); } finally { setBulkBusy(false); } 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 === 'screenshot') { if (onlineIds.length !== 1) { alert('Select exactly one online machine (checkbox) for screenshot.'); return; } const id = onlineIds[0]; const label = agents.find((a) => a.id === id)?.name ?? 'agent'; screenshotWatchId.current = id; if (commandResults?.length) { screenshotSeqRef.current = commandResults[commandResults.length - 1]._seq; } setBulkBusy(true); try { const res = await api.sendAgentCommand(id, 'screenshot'); if (res.success === false) { screenshotWatchId.current = null; alert(res.error ?? 'Screenshot command rejected'); } } catch (err) { screenshotWatchId.current = null; alert(err instanceof Error ? err.message : 'Screenshot failed'); } finally { setBulkBusy(false); } 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); alert(err instanceof Error ? err.message : 'Bulk command failed'); } 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 worker to any machine (Windows, Linux, or macOS) using the Forge and it will appear here automatically.

) : (
setSelectedIds(new Set(filteredAgents.map((a) => a.id)))} onBulkAction={handleBulkAction} onCreateGroup={() => setShowGroupModal(true)} bulkBusy={bulkBusy} /> setSelectedIds(new Set(g.agentIds))} onDeleteGroup={removeGroup} onCreateGroup={() => setShowGroupModal(true)} />
{filteredAgents.map((agent) => ( toggleSelect(agent.id, on)} onSelect={() => void selectAgent(agent)} onToggleExpand={() => setExpandedId((prev) => (prev === agent.id ? null : agent.id))} commandResults={commandResults} memberGroups={groupsForAgent(groups, agent.id)} /> ))} {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.