Complete private Monero miner control stack.

Implement Windows agent with RandomX mining and WebSocket fleet reporting, wire dashboard settings into the builder with saved exe paths, and add project README.
This commit is contained in:
drjones
2026-05-26 22:51:47 -07:00
commit 6c42f2b600
48 changed files with 10001 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import { useState, useEffect } from 'react';
import type { Share } from '../types';
import './Pages.css';
export default function DashboardPage() {
const { isConnected, agents, stats } = useWebSocket();
const [shares, setShares] = useState<Share[]>([]);
useEffect(() => {
api.getRecentShares(20).then(setShares).catch(console.error);
}, []);
const totalHashrate = agents.reduce((sum, a) => sum + a.hashrate_15m, 0);
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 acceptRate = totalShares > 0 ? ((acceptedShares / totalShares) * 100).toFixed(1) : '0.0';
return (
<div className="page fade-in">
<div className="page-header">
<h1>Dashboard</h1>
<div className="header-status">
<span className={`status-dot ${isConnected ? 'online' : 'offline'}`} />
<span className="status-text">{isConnected ? 'Live' : '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>
<div className="card stat-card">
<div className="stat-label">Miners 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>
<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>
</div>
{/* Agent Grid */}
<div className="section">
<h2>Active Miners</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>
</div>
)}
{agents.map((agent) => (
<div key={agent.id} className="card agent-card">
<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>
</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>
</div>
))}
</div>
</div>
{/* Recent Shares */}
<div className="section">
<h2>Recent Shares</h2>
<div className="card">
<table className="shares-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 submitted yet</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>
<span className={`status-badge ${share.accepted ? 'online' : 'error'}`}>
{share.accepted ? 'Accepted' : 'Rejected'}
</span>
</td>
<td className="hash-cell">{share.hash?.substring(0, 16)}...</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
function formatHashrate(h: number): string {
if (h >= 1_000_000) return `${(h / 1_000_000).toFixed(2)} MH/s`;
if (h >= 1_000) return `${(h / 1_000).toFixed(2)} KH/s`;
return `${h.toFixed(0)} H/s`;
}
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatTime(t: string): string {
const d = new Date(t);
return d.toLocaleTimeString();
}