import { useWebSocket } from '../hooks/useWebSocket'; import { api } from '../api/client'; import { useState, useEffect, useMemo, type CSSProperties } from 'react'; import { Link } from 'react-router-dom'; import type { Share } from '../types'; import HashrateChart from '../components/Charts/HashrateChart'; import GaugeRing from '../components/Charts/GaugeRing'; import NeonCard from '../components/NeonCard/NeonCard'; import { FleetPipelineStatus, ActivityPulse } from '../components/Visual/VisualComponents'; import { AlertBanner, PoolStatusPanel, AIActivityPanel, EarningsEstimator, FleetHealthCard, ContributionBars, UnderperformerList, OSArchBreakdown, LANGroupView, } from '../components/Fleet/FleetPanels'; import AgentRemoteActions from '../components/Fleet/AgentRemoteActions'; import FleetToolbar from '../components/Fleet/FleetToolbar'; import ErrorBoundary from '../components/ErrorBoundary'; import FleetTopologyMap from '../components/Visual/3D/FleetTopologyMap'; import MatrixStreamOverlay from '../components/Visual/MatrixStreamOverlay'; import { DEFAULT_FLEET_FILTERS, filterFleetAgents, agentIsIdleMiner, formatHashrate, formatUptime, } from '../help/fleetFilters'; import type { FleetFilterState } from '../help/fleetFilters'; import { computeFleetHealth, contributionBars, findUnderperformers, fleetMedianHashrate, groupBySubnet, osArchBreakdown, } from '../help/fleetAnalytics'; import './Pages.css'; export default function DashboardPage() { const { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity } = useWebSocket(); const [shares, setShares] = useState([]); const [restAlerts, setRestAlerts] = useState([]); const [restPools, setRestPools] = useState([]); const [restAI, setRestAI] = useState([]); const [subtitle, setSubtitle] = useState('security is just an emotion'); const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]); const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]); const [memHistory, setMemHistory] = useState<{ time: string; value: number }[]>([]); const [hasBuilds, setHasBuilds] = useState(false); const [filters, setFilters] = useState(DEFAULT_FLEET_FILTERS); const [selectedIds, setSelectedIds] = useState>(new Set()); const [bulkBusy, setBulkBusy] = useState(false); const [showMatrix, setShowMatrix] = useState(false); const [advancedMode, setAdvancedMode] = useState(() => { try { return localStorage.getItem('aether-dash-advanced') === '1'; } catch { return false; } }); const [xmrPrice, setXmrPrice] = useState(null); const toggleAdvanced = () => setAdvancedMode((prev) => { const next = !prev; try { localStorage.setItem('aether-dash-advanced', next ? '1' : '0'); } catch { /* ignore */ } return next; }); useEffect(() => { api.getRecentShares(20).then(setShares).catch(console.error); api.listBuilds().then((b) => setHasBuilds(b.length > 0)).catch(console.error); api.getConfig() .then((cfg) => { const s = cfg.server?.dashboard_subtitle?.trim(); if (s) setSubtitle(s); }) .catch(console.error); api.getAlerts().then(setRestAlerts).catch(console.error); api.getPoolStatus().then(setRestPools).catch(console.error); api.getAIActivity().then(setRestAI).catch(console.error); // XMR market price — refresh every 10 min matching server-side cache TTL const fetchPrice = () => api.getXmrPrice().then((r) => setXmrPrice(r.usd)).catch(() => {}); fetchPrice(); const priceTimer = setInterval(fetchPrice, 10 * 60 * 1000); return () => clearInterval(priceTimer); }, []); useEffect(() => { if (recentShares.length > 0) { setShares((prev) => { const merged = [...recentShares, ...prev]; const seen = new Set(); return merged.filter((s) => { const key = s.id != null ? String(s.id) : `${s.agent_id}-${s.hash}-${s.timestamp}`; if (seen.has(key)) return false; seen.add(key); return true; }).slice(0, 20); }); } }, [recentShares]); const totalHashrate = agents.reduce((sum, a) => sum + a.hashrate_15m, 0); const onlineCount = agents.filter((a) => a.status === 'online').length; const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0); const acceptedShares = agents.reduce((sum, a) => sum + a.shares_good, 0); const rejectedShares = agents.reduce((sum, a) => sum + a.shares_bad, 0); const acceptRate = totalShares > 0 ? (acceptedShares / totalShares) * 100 : 0; const avgCpu = agents.length > 0 ? agents.reduce((s, a) => s + a.cpu_usage_pct, 0) / agents.length : 0; const avgMem = agents.length > 0 ? agents.reduce((s, a) => s + a.memory_usage_pct, 0) / agents.length : 0; const onlinePct = agents.length > 0 ? (onlineCount / agents.length) * 100 : 0; useEffect(() => { const now = new Date().toLocaleTimeString(); setHashHistory((prev) => [...prev.slice(-59), { time: now, value: totalHashrate }]); setCpuHistory((prev) => [...prev.slice(-59), { time: now, value: avgCpu }]); setMemHistory((prev) => [...prev.slice(-59), { time: now, value: avgMem }]); }, [totalHashrate, avgCpu, avgMem]); const filteredAgents = useMemo(() => filterFleetAgents(agents, filters), [agents, filters]); const topAgents = useMemo( () => [...filteredAgents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 12), [filteredAgents] ); const maxAgentHash = Math.max(...topAgents.map((a) => a.hashrate_15m), 1); const activityItems = useMemo( () => shares.slice(0, 12).map((s) => ({ id: String(s.id ?? `${s.agent_id}-${s.hash}`), label: s.accepted ? 'OK' : 'BAD', ok: s.accepted, time: s.timestamp ? new Date(s.timestamp).toLocaleTimeString() : undefined, })), [shares] ); const totalShareCount = agents.reduce((sum, a) => sum + a.shares_total, 0); const alerts = fleetAlerts.length > 0 ? fleetAlerts : restAlerts; const pools = poolStatus.length > 0 ? poolStatus : restPools; const aiEntries = aiActivity.length > 0 ? aiActivity : restAI; const agentNameMap = useMemo( () => Object.fromEntries(agents.map((a) => [a.id, a.name])), [agents] ); // ── Analytics ───────────────────────────────────────────────────────────── const fleetHealth = useMemo(() => computeFleetHealth(agents, pools), [agents, pools]); const contribs = useMemo(() => contributionBars(agents), [agents]); const underperformers = useMemo(() => findUnderperformers(agents), [agents]); const medianHash = useMemo(() => fleetMedianHashrate(agents), [agents]); const lanGroups = useMemo(() => groupBySubnet(agents), [agents]); const platforms = useMemo(() => osArchBreakdown(agents), [agents]); const handleBulkAction = async (action: string) => { let targetIds = [...selectedIds]; if (action === 'restart_idle') { targetIds = agents.filter((a) => selectedIds.has(a.id) && agentIsIdleMiner(a)).map((a) => a.id); if (targetIds.length === 0) { alert('No selected online agents with idle hashrate.'); return; } action = 'restart'; } const onlineIds = targetIds.filter((id) => agents.find((a) => a.id === id)?.status === 'online'); if (onlineIds.length === 0) return; if (action === 'stop' && !window.confirm(`Stop ${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 Health — always above the fold */}

PERSONAL NETWORK · LIVE TELEMETRY

Command Deck

{subtitle}

{isConnected ? 'SIGNAL LOCKED' : 'RECONNECTING'} {agents.length} nodes registered
{advancedMode && ( )}

Fleet Pipeline

Visual progress — lit nodes mean that stage is active. Open Field Guide →

0} hasShares={totalShareCount > 0} />
Total Hashrate
{formatHashrate(totalHashrate)}
{onlineCount} engines firing
Fleet Online
{onlineCount} / {agents.length}
{agents.length - onlineCount} dormant
Accept Rate
{acceptRate.toFixed(1)}%
{acceptedShares} valid · {rejectedShares} rejected
Resources
{avgCpu.toFixed(0)}% CPU
{avgMem.toFixed(0)}% memory · fleet mean
{/* ── Analytics row — always visible ─────────────────────────────────── */} {(platforms.length > 0 || lanGroups.length > 1) && (
)} {/* ── Advanced-only panels ─────────────────────────────────────────────── */} {advancedMode && }
{advancedMode && ( )}
{advancedMode && ( )}

Share Activity Pulse

3D fleet map unavailable on this GPU — rest of the deck still works.

} >

Machine Roster

{agents.length > 0 && ( setSelectedIds(new Set(filteredAgents.map((a) => a.id)))} onBulkAction={handleBulkAction} bulkBusy={bulkBusy} /> )}
{agents.length === 0 && (

No miners on the wire

Forge an installer, deploy once per machine — nodes appear here with live neon telemetry.

)} {topAgents.map((agent, i) => (
{ setSelectedIds((prev) => { const next = new Set(prev); if (e.target.checked) next.add(agent.id); else next.delete(agent.id); return next; }); }} /> {agent.name} {agent.platform && ( {agent.platform}{agent.arch ? `/${agent.arch}` : ''} )}
{agent.status}
{(agent.tags?.length ?? 0) > 0 && (
{agent.tags!.map((t) => ( {t} ))}
)}
Hash 15m{formatHashrate(agent.hashrate_15m)}
Hash 1m{formatHashrate(agent.hashrate_1m)}
CPU / RAM{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%
Hardware{agent.cpu_cores} cores · {agent.memory_gb} GB
Shares{agent.shares_good} ok · {agent.shares_bad} bad
Node{agent.ip || '—'} · {agent.id.slice(0, 8)}
Uptime{formatUptime(agent.uptime_seconds)}
))} {agents.length > 0 && topAgents.length === 0 && (

No agents match current filters.

)}
{advancedMode && (

Share Log

{shares.length === 0 && ( )} {shares.map((share) => ( ))}
Time Agent Status Hash
No shares yet — awaiting proof of work...
{formatTime(share.timestamp)} {share.agent_id?.substring(0, 8)}… {share.accepted ? 'Accepted' : 'Rejected'} {share.hash?.substring(0, 24)}…
)} setShowMatrix(false)} /> ); } function formatTime(t: string): string { return new Date(t).toLocaleTimeString(); }