feat: fleet intelligence dashboard -- health score, XMR price, contribution map, analytics

This commit is contained in:
drjones
2026-05-30 15:10:50 -07:00
parent 2e36483158
commit 117801f882
9 changed files with 826 additions and 61 deletions

View File

@@ -7,7 +7,17 @@ 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 } from '../components/Fleet/FleetPanels';
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';
@@ -21,6 +31,14 @@ import {
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();
@@ -37,6 +55,18 @@ export default function DashboardPage() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [bulkBusy, setBulkBusy] = useState(false);
const [showMatrix, setShowMatrix] = useState(false);
const [advancedMode, setAdvancedMode] = useState<boolean>(() => {
try { return localStorage.getItem('aether-dash-advanced') === '1'; } catch { return false; }
});
const [xmrPrice, setXmrPrice] = useState<number | null>(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);
@@ -49,6 +79,11 @@ export default function DashboardPage() {
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(() => {
@@ -111,6 +146,14 @@ export default function DashboardPage() {
[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') {
@@ -135,15 +178,18 @@ export default function DashboardPage() {
}
};
return ( <div className="page fade-in command-deck">
return (
<div className="page fade-in command-deck">
<AlertBanner alerts={alerts} />
{/* Fleet Health — always above the fold */}
<FleetHealthCard health={fleetHealth} />
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">PERSONAL NETWORK · LIVE TELEMETRY</p>
<h1>Command Deck</h1>
<p className="page-subtitle">
{subtitle}
</p>
<p className="page-subtitle">{subtitle}</p>
</div>
<div className="deck-hero-status">
<div className={`live-beacon ${isConnected ? 'on' : ''}`}>
@@ -154,9 +200,22 @@ export default function DashboardPage() {
<span className="font-tech live-label">{isConnected ? 'SIGNAL LOCKED' : 'RECONNECTING'}</span>
<span className="live-sub">{agents.length} nodes registered</span>
</div>
<button className="button matrix-toggle-btn" onClick={() => setShowMatrix(true)} style={{ marginLeft: '1rem', background: 'transparent', border: '1px solid #0f0', color: '#0f0', fontFamily: 'monospace' }}>
[RAW_STREAM]
<button
className={`dash-mode-btn${advancedMode ? ' active' : ''}`}
onClick={toggleAdvanced}
title={advancedMode ? 'Switch to Overview (hide charts / logs)' : 'Switch to Advanced (show all panels)'}
>
{advancedMode ? '[OVERVIEW]' : '[ADVANCED]'}
</button>
{advancedMode && (
<button
className="button matrix-toggle-btn"
onClick={() => setShowMatrix(true)}
style={{ background: 'transparent', border: '1px solid #0f0', color: '#0f0', fontFamily: 'monospace' }}
>
[RAW_STREAM]
</button>
)}
</div>
</header>
@@ -205,7 +264,7 @@ export default function DashboardPage() {
<div className="stat-value hashrate neon-glow-cyan">{formatHashrate(totalHashrate)}</div>
<div className="stat-sub">{onlineCount} engines firing</div>
</NeonCard>
<EarningsEstimator hashrate={totalHashrate} />
<EarningsEstimator hashrate={totalHashrate} xmrPrice={xmrPrice} />
<NeonCard accent="green" className="stat-card-wrap">
<div className="stat-label font-tech">Fleet Online</div>
<div className="stat-value accepted">{onlineCount} <span className="stat-dim">/ {agents.length}</span></div>
@@ -223,22 +282,37 @@ export default function DashboardPage() {
</NeonCard>
</div>
{/* ── Analytics row — always visible ─────────────────────────────────── */}
<ContributionBars bars={contribs} xmrPrice={xmrPrice} />
<UnderperformerList underperformers={underperformers} medianHashrate={medianHash} />
{(platforms.length > 0 || lanGroups.length > 1) && (
<div className="grid-2" style={{ gap: '1rem', marginTop: '1rem' }}>
<OSArchBreakdown platforms={platforms} />
<LANGroupView groups={lanGroups} />
</div>
)}
<PoolStatusPanel pools={pools} />
<AIActivityPanel entries={aiEntries} agentNames={agentNameMap} />
{/* ── Advanced-only panels ─────────────────────────────────────────────── */}
{advancedMode && <AIActivityPanel entries={aiEntries} agentNames={agentNameMap} />}
<div className="grid-2 chart-row">
<NeonCard accent="cyan" tilt3d>
<HashrateChart data={hashHistory} title="Fleet Hashrate Wave" color="#00f5ff" unit="H/s" height={300} />
</NeonCard>
<NeonCard accent="magenta" tilt3d>
<HashrateChart data={cpuHistory} title="CPU Pressure" color="#ff2da6" unit="%" height={300} />
</NeonCard>
{advancedMode && (
<NeonCard accent="magenta" tilt3d>
<HashrateChart data={cpuHistory} title="CPU Pressure" color="#ff2da6" unit="%" height={300} />
</NeonCard>
)}
</div>
<NeonCard accent="brass" className="chart-row-full" tilt3d>
<HashrateChart data={memHistory} title="Memory Load — Fleet Average" color="#ffb020" unit="%" height={220} />
</NeonCard>
{advancedMode && (
<NeonCard accent="brass" className="chart-row-full" tilt3d>
<HashrateChart data={memHistory} title="Memory Load — Fleet Average" color="#ffb020" unit="%" height={220} />
</NeonCard>
)}
<NeonCard accent="purple" className="section" hud>
<h2 className="section-title font-display">
@@ -347,41 +421,43 @@ export default function DashboardPage() {
)}
</div>
</section>
<section className="section">
<h2 className="section-title font-display">
<span className="section-ornament"></span> Share Log
<span className="section-line" />
</h2>
<NeonCard accent="brass" className="table-card" hud>
<table className="shares-table steampunk-table">
<thead>
<tr>
<th>Time</th>
<th>Agent</th>
<th>Status</th>
<th>Hash</th>
</tr>
</thead>
<tbody>
{shares.length === 0 && (
<tr><td colSpan={4} className="empty-table">No shares yet awaiting proof of work...</td></tr>
)}
{shares.map((share) => (
<tr key={share.id}>
<td className="time-cell font-tech">{formatTime(share.timestamp)}</td>
<td className="mono-sm">{share.agent_id?.substring(0, 8)}</td>
<td>
<span className={`status-badge ${share.accepted ? 'online' : 'error'}`}>
{share.accepted ? 'Accepted' : 'Rejected'}
</span>
</td>
<td className="hash-cell mono-sm">{share.hash?.substring(0, 24)}</td>
{advancedMode && (
<section className="section">
<h2 className="section-title font-display">
<span className="section-ornament"></span> Share Log
<span className="section-line" />
</h2>
<NeonCard accent="brass" className="table-card" hud>
<table className="shares-table steampunk-table">
<thead>
<tr>
<th>Time</th>
<th>Agent</th>
<th>Status</th>
<th>Hash</th>
</tr>
))}
</tbody>
</table>
</NeonCard>
</section>
</thead>
<tbody>
{shares.length === 0 && (
<tr><td colSpan={4} className="empty-table">No shares yet awaiting proof of work...</td></tr>
)}
{shares.map((share) => (
<tr key={share.id}>
<td className="time-cell font-tech">{formatTime(share.timestamp)}</td>
<td className="mono-sm">{share.agent_id?.substring(0, 8)}</td>
<td>
<span className={`status-badge ${share.accepted ? 'online' : 'error'}`}>
{share.accepted ? 'Accepted' : 'Rejected'}
</span>
</td>
<td className="hash-cell mono-sm">{share.hash?.substring(0, 24)}</td>
</tr>
))}
</tbody>
</table>
</NeonCard>
</section>
)}
<MatrixStreamOverlay active={showMatrix} onClose={() => setShowMatrix(false)} />
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.