Upgrade dashboard, builder, and agent resource controls.
Dark UI with hashrate graphs, inline setting help, percent-based threads/RAM, configurable process name and display modes, and LAN-aware run.bat startup banner.
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { api } from '../api/client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { Share } from '../types';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import './Pages.css';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { isConnected, agents, stats } = useWebSocket();
|
||||
const { isConnected, agents } = useWebSocket();
|
||||
const [shares, setShares] = useState<Share[]>([]);
|
||||
const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getRecentShares(20).then(setShares).catch(console.error);
|
||||
@@ -16,97 +19,104 @@ export default function DashboardPage() {
|
||||
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).toFixed(1) : '0.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;
|
||||
|
||||
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]);
|
||||
|
||||
const topAgents = useMemo(
|
||||
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 6),
|
||||
[agents]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page fade-in">
|
||||
<div className="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<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' : 'Reconnecting...'}</span>
|
||||
<span className="status-text">{isConnected ? 'Live feed' : 'Reconnecting...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid-4 stats-grid">
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Total Hashrate</div>
|
||||
<div className="stat-value hashrate">
|
||||
{formatHashrate(totalHashrate)}
|
||||
</div>
|
||||
<div className="stat-sub">across {onlineCount} active miners</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Miners Online</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>
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Shares Accepted</div>
|
||||
<div className="stat-value accepted">{acceptedShares}</div>
|
||||
<div className="stat-sub">{acceptRate}% accept rate</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">
|
||||
<div className="stat-label">Total Shares</div>
|
||||
<div className="stat-value">{totalShares}</div>
|
||||
<div className="stat-sub">{totalShares - acceptedShares} rejected</div>
|
||||
<div className="card stat-card accent-yellow">
|
||||
<div className="stat-label">Resource Avg</div>
|
||||
<div className="stat-value">{avgCpu.toFixed(0)}% CPU</div>
|
||||
<div className="stat-sub">{avgMem.toFixed(0)}% RAM fleet average</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* Agent Grid */}
|
||||
<div className="section">
|
||||
<h2>Active Miners</h2>
|
||||
<h2>Machine Detail</h2>
|
||||
<div className="agent-grid">
|
||||
{agents.length === 0 && (
|
||||
<div className="card empty-state">
|
||||
<div className="empty-icon">🖥️</div>
|
||||
<h3>No miners connected</h3>
|
||||
<p>Build and deploy a miner using the Miner Builder to get started.</p>
|
||||
<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>
|
||||
)}
|
||||
{agents.map((agent) => (
|
||||
<div key={agent.id} className="card agent-card">
|
||||
{topAgents.map((agent) => (
|
||||
<div key={agent.id} className="card agent-card detailed">
|
||||
<div className="agent-card-header">
|
||||
<div className="agent-name">
|
||||
<span className={`status-dot ${agent.status}`} />
|
||||
<span>{agent.name}</span>
|
||||
</div>
|
||||
<span className={`status-badge ${agent.status}`}>
|
||||
{agent.status}
|
||||
</span>
|
||||
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
|
||||
</div>
|
||||
<div className="agent-card-stats">
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Hashrate</span>
|
||||
<span className="agent-stat-value">{formatHashrate(agent.hashrate_15m)}</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">CPU</span>
|
||||
<span className="agent-stat-value">{agent.cpu_usage_pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Shares</span>
|
||||
<span className="agent-stat-value">{agent.shares_good}</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Uptime</span>
|
||||
<span className="agent-stat-value">{formatUptime(agent.uptime_seconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-card-footer">
|
||||
<span className="agent-meta">{agent.ip || 'Unknown IP'}</span>
|
||||
<span className="agent-meta">v{agent.version || '?'}</span>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Shares */}
|
||||
<div className="section">
|
||||
<h2>Recent Shares</h2>
|
||||
<div className="card">
|
||||
<div className="card table-card">
|
||||
<table className="shares-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -118,9 +128,7 @@ 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 submitted yet</td></tr>
|
||||
)}
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
@@ -131,7 +139,7 @@ export default function DashboardPage() {
|
||||
{share.accepted ? 'Accepted' : 'Rejected'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="hash-cell">{share.hash?.substring(0, 16)}...</td>
|
||||
<td className="hash-cell">{share.hash?.substring(0, 20)}...</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -158,6 +166,5 @@ function formatUptime(seconds: number): string {
|
||||
}
|
||||
|
||||
function formatTime(t: string): string {
|
||||
const d = new Date(t);
|
||||
return d.toLocaleTimeString();
|
||||
return new Date(t).toLocaleTimeString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user