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:
191
server/web/src/pages/AgentsPage.tsx
Normal file
191
server/web/src/pages/AgentsPage.tsx
Normal 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user