Redesign dashboard with steampunk neon theme and live gauges.

Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages.
This commit is contained in:
drjones
2026-05-27 00:43:34 -07:00
parent 6db9a33a20
commit 9d223b8137
20 changed files with 1638 additions and 307 deletions

View File

@@ -1,8 +1,10 @@
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect, useMemo, type CSSProperties } from 'react';
import type { Share } from '../types';
import HashrateChart from '../components/Charts/HashrateChart';
import GaugeRing from '../components/Charts/GaugeRing';
import NeonCard from '../components/NeonCard/NeonCard';
import './Pages.css';
export default function DashboardPage() {
@@ -10,6 +12,7 @@ export default function DashboardPage() {
const [shares, setShares] = useState<Share[]>([]);
const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]);
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
const [memHistory, setMemHistory] = useState<{ time: string; value: number }[]>([]);
useEffect(() => {
api.getRecentShares(20).then(setShares).catch(console.error);
@@ -20,77 +23,125 @@ export default function DashboardPage() {
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).toFixed(1) : '0.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 }]);
}, [totalHashrate, avgCpu]);
setMemHistory((prev) => [...prev.slice(-59), { time: now, value: avgMem }]);
}, [totalHashrate, avgCpu, avgMem]);
const topAgents = useMemo(
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 6),
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 8),
[agents]
);
return (
<div className="page fade-in">
<div className="page-header">
<div>
<h1>Fleet Dashboard</h1>
<p className="page-subtitle">Live monitoring for every installed miner on your network</p>
</div>
<div className="header-status">
<span className={`status-dot ${isConnected ? 'online' : 'offline'}`} />
<span className="status-text">{isConnected ? 'Live feed' : 'Reconnecting...'}</span>
</div>
</div>
const maxAgentHash = Math.max(...topAgents.map((a) => a.hashrate_15m), 1);
<div className="grid-4 stats-grid">
<div className="card stat-card accent-cyan">
<div className="stat-label">Fleet Hashrate</div>
<div className="stat-value hashrate">{formatHashrate(totalHashrate)}</div>
<div className="stat-sub">{onlineCount} miners active</div>
return (
<div className="page fade-in command-deck">
<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">
Your private mining fleet across the LAN brass gauges, neon pulse, real-time hashrate.
</p>
</div>
<div className="card stat-card accent-green">
<div className="stat-label">Online</div>
<div className="stat-value">{onlineCount} / {agents.length}</div>
<div className="stat-sub">{agents.length - onlineCount} offline</div>
<div className="deck-hero-status">
<div className={`live-beacon ${isConnected ? 'on' : ''}`}>
<span className="beacon-ring" />
<span className="beacon-core" />
</div>
<div>
<span className="font-tech live-label">{isConnected ? 'SIGNAL LOCKED' : 'RECONNECTING'}</span>
<span className="live-sub">{agents.length} nodes registered</span>
</div>
</div>
<div className="card stat-card accent-purple">
<div className="stat-label">Accept Rate</div>
<div className="stat-value accepted">{acceptRate}%</div>
<div className="stat-sub">{acceptedShares} good · {rejectedShares} bad</div>
</div>
<div className="card stat-card accent-yellow">
<div className="stat-label">Resource Avg</div>
</header>
<section className="gauge-row">
<NeonCard accent="cyan" className="gauge-card" hud>
<GaugeRing
value={totalHashrate}
max={Math.max(totalHashrate * 1.2, 1000)}
label="Fleet Hash"
sublabel="15m avg"
color="var(--neon-cyan)"
size={110}
/>
</NeonCard>
<NeonCard accent="green" className="gauge-card" hud>
<GaugeRing value={onlinePct} label="Online" sublabel={`${onlineCount}/${agents.length}`} color="var(--neon-green)" size={110} />
</NeonCard>
<NeonCard accent="purple" className="gauge-card" hud>
<GaugeRing value={acceptRate} label="Accept" sublabel="share rate" color="var(--neon-purple)" size={110} />
</NeonCard>
<NeonCard accent="amber" className="gauge-card" hud>
<GaugeRing value={avgCpu} label="CPU" sublabel={`RAM ${avgMem.toFixed(0)}%`} color="var(--neon-amber)" size={110} />
</NeonCard>
</section>
<div className="grid-4 stats-grid steampunk-stats">
<NeonCard accent="cyan" className="stat-card-wrap">
<div className="stat-label font-tech">Total Hashrate</div>
<div className="stat-value hashrate neon-glow-cyan">{formatHashrate(totalHashrate)}</div>
<div className="stat-sub">{onlineCount} engines firing</div>
</NeonCard>
<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>
<div className="stat-sub">{agents.length - onlineCount} dormant</div>
</NeonCard>
<NeonCard accent="purple" className="stat-card-wrap">
<div className="stat-label font-tech">Accept Rate</div>
<div className="stat-value neon-glow-purple">{acceptRate.toFixed(1)}%</div>
<div className="stat-sub">{acceptedShares} valid · {rejectedShares} rejected</div>
</NeonCard>
<NeonCard accent="amber" className="stat-card-wrap">
<div className="stat-label font-tech">Resources</div>
<div className="stat-value">{avgCpu.toFixed(0)}% CPU</div>
<div className="stat-sub">{avgMem.toFixed(0)}% RAM fleet average</div>
</div>
<div className="stat-sub">{avgMem.toFixed(0)}% memory · fleet mean</div>
</NeonCard>
</div>
<div className="grid-2 chart-row">
<div className="card">
<HashrateChart data={hashHistory} title="Fleet Hashrate (15m window)" color="#06b6d4" unit="H/s" />
</div>
<div className="card">
<HashrateChart data={cpuHistory} title="Average CPU Usage" color="#8b5cf6" unit="%" />
</div>
<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>
</div>
<div className="section">
<h2>Machine Detail</h2>
<NeonCard accent="brass" className="chart-row-full" tilt3d>
<HashrateChart data={memHistory} title="Memory Load — Fleet Average" color="#ffb020" unit="%" height={220} />
</NeonCard>
<section className="section">
<h2 className="section-title font-display">
<span className="section-ornament"></span> Machine Roster
<span className="section-line" />
</h2>
<div className="agent-grid">
{agents.length === 0 && (
<div className="card empty-state">
<h3>No miners connected yet</h3>
<p>Build an installer in Miner Builder, run it once on a Windows PC, and it will appear here automatically.</p>
</div>
<NeonCard accent="brass" className="empty-state">
<div className="empty-icon"></div>
<h3>No miners on the wire</h3>
<p>Forge an installer, deploy once per machine nodes appear here with live neon telemetry.</p>
</NeonCard>
)}
{topAgents.map((agent) => (
<div key={agent.id} className="card agent-card detailed">
{topAgents.map((agent, i) => (
<NeonCard
key={agent.id}
accent={agent.status === 'online' ? 'cyan' : 'brass'}
className="agent-card detailed machine-panel"
style={{ animationDelay: `${i * 0.05}s` } as CSSProperties}
>
<div className="agent-card-header">
<div className="agent-name">
<span className={`status-dot ${agent.status}`} />
@@ -98,26 +149,33 @@ export default function DashboardPage() {
</div>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
</div>
<div className="agent-detail-lines">
<div><span>Hashrate 15m</span><strong>{formatHashrate(agent.hashrate_15m)}</strong></div>
<div><span>Hashrate 1m</span><strong>{formatHashrate(agent.hashrate_1m)}</strong></div>
<div><span>Hashrate 15s</span><strong>{formatHashrate(agent.hashrate_15s)}</strong></div>
<div><span>CPU / RAM</span><strong>{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%</strong></div>
<div><span>Cores / RAM</span><strong>{agent.cpu_cores} cores · {agent.memory_gb} GB</strong></div>
<div><span>Shares</span><strong>{agent.shares_good} ok · {agent.shares_bad} bad · {agent.shares_total} total</strong></div>
<div><span>Network</span><strong>{agent.ip || 'unknown'} · v{agent.version || '?'}</strong></div>
<div><span>Uptime</span><strong>{formatUptime(agent.uptime_seconds)}</strong></div>
<div><span>Last seen</span><strong>{new Date(agent.last_seen).toLocaleString()}</strong></div>
<div className="agent-hash-bar">
<div
className="agent-hash-fill"
style={{ width: `${(agent.hashrate_15m / maxAgentHash) * 100}%` }}
/>
</div>
</div>
<div className="agent-detail-lines">
<div><span>Hash 15m</span><strong className="neon-glow-cyan">{formatHashrate(agent.hashrate_15m)}</strong></div>
<div><span>Hash 1m</span><strong>{formatHashrate(agent.hashrate_1m)}</strong></div>
<div><span>CPU / RAM</span><strong>{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%</strong></div>
<div><span>Hardware</span><strong>{agent.cpu_cores} cores · {agent.memory_gb} GB</strong></div>
<div><span>Shares</span><strong>{agent.shares_good} ok · {agent.shares_bad} bad</strong></div>
<div><span>Node</span><strong className="mono-sm">{agent.ip || '—'} · {agent.id.slice(0, 8)}</strong></div>
<div><span>Uptime</span><strong>{formatUptime(agent.uptime_seconds)}</strong></div>
</div>
</NeonCard>
))}
</div>
</div>
</section>
<div className="section">
<h2>Recent Shares</h2>
<div className="card table-card">
<table className="shares-table">
<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>
@@ -128,24 +186,24 @@ export default function DashboardPage() {
</thead>
<tbody>
{shares.length === 0 && (
<tr><td colSpan={4} className="empty-table">No shares submitted yet</td></tr>
<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">{formatTime(share.timestamp)}</td>
<td>{share.agent_id?.substring(0, 8)}...</td>
<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">{share.hash?.substring(0, 20)}...</td>
<td className="hash-cell mono-sm">{share.hash?.substring(0, 24)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</NeonCard>
</section>
</div>
);
}