import { useState, useEffect, useMemo } from 'react'; import { useWebSocket } from '../hooks/useWebSocket'; import { api } from '../api/client'; import { formatHashrate } from '../help/fleetFilters'; import { platformIcon } from '../help/platform'; import './ROIPage.css'; // ── helpers ─────────────────────────────────────────────────────────────── function fmt(n: number, decimals = 2) { return n.toFixed(decimals); } function fmtUSD(n: number): string { const sign = n < 0 ? '-' : ''; const abs = Math.abs(n); if (abs >= 1000) return `${sign}$${(abs / 1000).toFixed(2)}k`; return `${sign}$${abs.toFixed(2)}`; } function effBadge(pct: number): { label: string; cls: string } { if (pct >= 75) return { label: 'TOP', cls: 'top' }; if (pct >= 40) return { label: 'MID', cls: 'mid' }; if (pct > 0) return { label: 'LOW', cls: 'low' }; return { label: 'OFF', cls: 'off' }; } const WATT_PER_CORE_ESTIMATE = 15; // rough W per active CPU core const XMR_COINGECKO_FALLBACK = 150; // offline fallback price USD // ── ROI Page ────────────────────────────────────────────────────────────── export default function ROIPage() { const { agents } = useWebSocket(); const [xmrPrice, setXmrPrice] = useState(null); const [xmrPriceAt, setXmrPriceAt] = useState(null); const [priceLoading, setPriceLoading] = useState(true); const [estXmrDay, setEstXmrDay] = useState(null); const [kwh, setKwh] = useState(() => { try { return parseFloat(localStorage.getItem('roi-kwh') ?? '0.10'); } catch { return 0.10; } }); const [sparkData, setSparkData] = useState([]); // Fetch XMR price on mount, refresh every 10min useEffect(() => { const fetch = () => { setPriceLoading(true); api.getXmrPrice() .then((r) => { setXmrPrice(r.usd); setXmrPriceAt(r.fetched_at); }) .catch(() => setXmrPrice(XMR_COINGECKO_FALLBACK)) .finally(() => setPriceLoading(false)); }; fetch(); const t = setInterval(fetch, 10 * 60 * 1000); return () => clearInterval(t); }, []); // Earnings estimate from fleet hashrate const onlineAgents = useMemo(() => agents.filter((a) => a.status === 'online'), [agents]); const totalHashrate = useMemo(() => onlineAgents.reduce((s, a) => s + (a.hashrate_15m ?? 0), 0), [onlineAgents]); useEffect(() => { if (totalHashrate <= 0) { setEstXmrDay(null); return; } api.getEarningsEstimate(totalHashrate) .then((r) => setEstXmrDay(r.xmr_per_day ?? null)) .catch(() => setEstXmrDay(null)); }, [totalHashrate]); // Spark history — sample every 4s useEffect(() => { const id = setInterval(() => { setSparkData((prev) => [...prev.slice(-29), totalHashrate]); }, 4000); return () => clearInterval(id); }, [totalHashrate]); // Save kWh preference const handleKwh = (v: number) => { setKwh(v); try { localStorage.setItem('roi-kwh', String(v)); } catch { /* noop */ } }; // ── Derived numbers ────────────────────────────────────────────────────── const price = xmrPrice ?? XMR_COINGECKO_FALLBACK; const xmrPerDay = estXmrDay ?? 0; const usdPerDay = xmrPerDay * price; const usdPerWeek = usdPerDay * 7; const usdPerMonth = usdPerDay * 30; // Electricity cost estimate const totalCores = onlineAgents.reduce((s, a) => s + (a.cpu_cores ?? 0), 0); const estimatedWatts = totalCores * WATT_PER_CORE_ESTIMATE; const kwhPerDay = (estimatedWatts / 1000) * 24; const electricityCostDay = kwhPerDay * kwh; const netProfitDay = usdPerDay - electricityCostDay; // Per-node profitability — sorted by USD/day desc const nodeProfit = useMemo(() => { const maxHash = Math.max(...agents.map((a) => a.hashrate_15m ?? 0), 1); return agents .map((a) => { const hr = a.hashrate_15m ?? 0; const pct = hr / maxHash; // Linear interpolation of fleet earnings by hashrate share const nodeXmrDay = xmrPerDay > 0 && totalHashrate > 0 ? (hr / totalHashrate) * xmrPerDay : 0; const nodeUsdDay = nodeXmrDay * price; const nodeCores = a.cpu_cores ?? 0; const nodeWatts = nodeCores * WATT_PER_CORE_ESTIMATE; const nodeKwhDay = (nodeWatts / 1000) * 24; const nodeElecCost = nodeKwhDay * kwh; const nodeNet = nodeUsdDay - nodeElecCost; return { a, hr, pct, nodeUsdDay, nodeXmrDay, nodeNet }; }) .sort((x, y) => y.nodeUsdDay - x.nodeUsdDay); }, [agents, xmrPerDay, totalHashrate, price, kwh]); // Platform breakdown const platformStats = useMemo(() => { const byPlatform: Record = {}; for (const a of onlineAgents) { const p = a.platform ?? 'unknown'; if (!byPlatform[p]) byPlatform[p] = { count: 0, hashrate: 0 }; byPlatform[p].count++; byPlatform[p].hashrate += a.hashrate_15m ?? 0; } const maxHr = Math.max(...Object.values(byPlatform).map((v) => v.hashrate), 1); return Object.entries(byPlatform) .sort((a, b) => b[1].hashrate - a[1].hashrate) .map(([platform, stats]) => ({ platform, ...stats, pct: stats.hashrate / maxHr })); }, [onlineAgents]); // Mining method breakdown const methodStats = useMemo(() => { const counts: Record = {}; for (const a of onlineAgents) { const m = a.active_method ?? 'unknown'; counts[m] = (counts[m] ?? 0) + 1; } return Object.entries(counts).sort((a, b) => b[1] - a[1]); }, [onlineAgents]); const maxSparkVal = Math.max(...sparkData, 1); // ── Empty state ────────────────────────────────────────────────────────── if (agents.length === 0) { return (
💹 No nodes online. Deploy agents to start tracking ROI.
); } // ── Render ─────────────────────────────────────────────────────────────── return (
{/* ── Hero ─────────────────────────────────────────────────────────── */}

FINANCIAL INTELLIGENCE

ROI Dashboard

Live earnings · per-node profitability · net profit after electricity

XMR {priceLoading ? (
fetching…
) : ( <> ${xmrPrice?.toFixed(2) ?? '—'} USD{xmrPriceAt ? ` · ${new Date(xmrPriceAt).toLocaleTimeString()}` : ''} )}
{/* ── KPI Row ──────────────────────────────────────────────────────── */}
XMR / DAY
{xmrPerDay > 0 ? fmt(xmrPerDay, 6) : '—'}
at {formatHashrate(totalHashrate)}
USD / DAY
{usdPerDay > 0 ? fmtUSD(usdPerDay) : '—'}
gross revenue
USD / MONTH
{usdPerMonth > 0 ? fmtUSD(usdPerMonth) : '—'}
30-day projection
NET PROFIT / DAY
{usdPerDay > 0 ? fmtUSD(netProfitDay) : '—'}
after electricity est.
ONLINE NODES
{onlineAgents.length}
of {agents.length} total
EST. POWER DRAW
{estimatedWatts > 0 ? `${estimatedWatts}W` : '—'}
{totalCores} cores × {WATT_PER_CORE_ESTIMATE}W est.
{/* ── Main grid row 1 ──────────────────────────────────────────────── */}
{/* Hashrate sparkline + projections */}
EARNINGS PROJECTION
{/* Spark */} {sparkData.length > 1 && (
{sparkData.map((v, i) => (
))}
)}
TODAY
{usdPerDay > 0 ? fmtUSD(usdPerDay) : '—'}
THIS WEEK
{usdPerWeek > 0 ? fmtUSD(usdPerWeek) : '—'}
THIS MONTH
{usdPerMonth > 0 ? fmtUSD(usdPerMonth) : '—'}
THIS YEAR
{usdPerDay > 0 ? fmtUSD(usdPerDay * 365) : '—'}
{/* Net profit calculator */}
ELECTRICITY RATE
handleKwh(parseFloat(e.target.value) || 0)} /> $/kWh
≈ {kwhPerDay.toFixed(1)} kWh/day · {fmtUSD(electricityCostDay)}/day cost
NET DAILY PROFIT {usdPerDay > 0 ? fmtUSD(netProfitDay) : '—'}
{/* Platform breakdown */}
PLATFORM BREAKDOWN
{platformStats.length === 0 ? (

No online agents.

) : (
{platformStats.map(({ platform, count, hashrate, pct }) => { const colors: Record = { windows: '#00e8f5', linux: '#39ff14', darwin: '#b24bf3', }; const color = colors[platform.toLowerCase()] ?? '#ffb020'; return (
{platformIcon(platform)} {platform}
{count}n · {formatHashrate(hashrate)}
); })}
)} {/* Mining method chips */} {methodStats.length > 0 && ( <>
ACTIVE MINING METHODS
{methodStats.map(([method, count]) => ( {method} · {count} ))}
)}
{/* ── Node profitability table (full width) ────────────────────────── */}
NODE PROFITABILITY RANKING sorted by USD/day
{nodeProfit.slice(0, 25).map(({ a, hr, pct, nodeUsdDay, nodeXmrDay, nodeNet }, idx) => { const isOffline = a.status !== 'online'; const badge = effBadge(pct * 100); return ( ); })}
# NODE PLATFORM HASHRATE XMR/DAY USD/DAY NET/DAY SHARE EFF
{idx + 1} {a.name} {platformIcon(a.platform)} {a.platform ?? '—'} {hr > 0 ? formatHashrate(hr) : } {nodeXmrDay > 0 ? nodeXmrDay.toFixed(6) : '—'} {nodeUsdDay > 0 ? fmtUSD(nodeUsdDay) : '—'} = 0 ? '#39ff14' : '#ff6b6b' }}> {nodeUsdDay > 0 ? fmtUSD(nodeNet) : '—'}
{badge.label}
{nodeProfit.length > 25 && (

… {nodeProfit.length - 25} more nodes

)}
); }