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,191 @@
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import type { Agent, HashrateSample } from '../types';
import './Pages.css';
export default function AgentsPage() {
const [agents, setAgents] = useState<Agent[]>([]);
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const [hashrateHistory, setHashrateHistory] = useState<HashrateSample[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.listAgents()
.then(setAgents)
.catch(console.error)
.finally(() => setLoading(false));
}, []);
const selectAgent = async (agent: Agent) => {
setSelectedAgent(agent);
try {
const history = await api.getAgentStats(agent.id, 60);
setHashrateHistory(history);
} catch (err) {
console.error(err);
}
};
return (
<div className="page fade-in">
<div className="page-header">
<h1>Agents</h1>
<span className="header-count">{agents.length} total</span>
</div>
{loading ? (
<div className="card empty-state">
<p>Loading agents...</p>
</div>
) : agents.length === 0 ? (
<div className="card empty-state">
<div className="empty-icon">🖥</div>
<h3>No agents registered</h3>
<p>Deploy a miner to a Windows machine and it will appear here automatically.</p>
</div>
) : (
<div className="agents-layout">
<div className="agents-list">
{agents.map((agent) => (
<div
key={agent.id}
className={`card agent-list-item ${selectedAgent?.id === agent.id ? 'selected' : ''}`}
onClick={() => selectAgent(agent)}
>
<div className="agent-list-header">
<div className="agent-list-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-list-details">
<span>Hashrate: {formatHashrate(agent.hashrate_15m)}</span>
<span>Shares: {agent.shares_good}/{agent.shares_total}</span>
</div>
<div className="agent-list-meta">
<span>{agent.ip}</span>
<span>v{agent.version || '?'}</span>
<span>{agent.cpu_cores} cores</span>
</div>
</div>
))}
</div>
{selectedAgent && (
<div className="agent-detail card">
<h2>{selectedAgent.name}</h2>
<div className="agent-detail-grid">
<div className="detail-item">
<span className="detail-label">Status</span>
<span className={`status-badge ${selectedAgent.status}`}>
{selectedAgent.status}
</span>
</div>
<div className="detail-item">
<span className="detail-label">Wallet</span>
<span className="detail-value mono">{selectedAgent.wallet?.substring(0, 20)}...</span>
</div>
<div className="detail-item">
<span className="detail-label">IP Address</span>
<span className="detail-value">{selectedAgent.ip}</span>
</div>
<div className="detail-item">
<span className="detail-label">Version</span>
<span className="detail-value">{selectedAgent.version || 'Unknown'}</span>
</div>
<div className="detail-item">
<span className="detail-label">CPU Cores</span>
<span className="detail-value">{selectedAgent.cpu_cores}</span>
</div>
<div className="detail-item">
<span className="detail-label">Memory</span>
<span className="detail-value">{selectedAgent.memory_gb} GB</span>
</div>
<div className="detail-item">
<span className="detail-label">CPU Usage</span>
<span className="detail-value">{selectedAgent.cpu_usage_pct.toFixed(1)}%</span>
</div>
<div className="detail-item">
<span className="detail-label">Uptime</span>
<span className="detail-value">{formatUptime(selectedAgent.uptime_seconds)}</span>
</div>
</div>
<div className="detail-section">
<h3>Hashrate</h3>
<div className="hashrate-detail-grid">
<div className="hashrate-item">
<span className="detail-label">15s</span>
<span className="hashrate-value">{formatHashrate(selectedAgent.hashrate_15s)}</span>
</div>
<div className="hashrate-item">
<span className="detail-label">1m</span>
<span className="hashrate-value">{formatHashrate(selectedAgent.hashrate_1m)}</span>
</div>
<div className="hashrate-item">
<span className="detail-label">15m</span>
<span className="hashrate-value">{formatHashrate(selectedAgent.hashrate_15m)}</span>
</div>
</div>
</div>
<div className="detail-section">
<h3>Shares</h3>
<div className="shares-detail-grid">
<div className="share-stat good">
<span className="share-count">{selectedAgent.shares_good}</span>
<span className="share-label">Accepted</span>
</div>
<div className="share-stat bad">
<span className="share-count">{selectedAgent.shares_bad}</span>
<span className="share-label">Rejected</span>
</div>
<div className="share-stat total">
<span className="share-count">{selectedAgent.shares_total}</span>
<span className="share-label">Total</span>
</div>
</div>
</div>
{hashrateHistory.length > 0 && (
<div className="detail-section">
<h3>Hashrate History (last {hashrateHistory.length} samples)</h3>
<div className="hashrate-chart">
{hashrateHistory.reverse().map((sample, i) => (
<div
key={sample.id}
className="chart-bar"
style={{
height: `${Math.max(5, (sample.hashrate / Math.max(...hashrateHistory.map(s => s.hashrate))) * 100)}%`,
}}
title={`${formatHashrate(sample.hashrate)} at ${new Date(sample.timestamp).toLocaleTimeString()}`}
/>
))}
</div>
</div>
)}
</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`;
}