feat: fleet intelligence dashboard -- health score, XMR price, contribution map, analytics
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user