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,419 @@
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig } from '../types';
import './Pages.css';
function defaultsFromConfig(config: ServerConfig, origin: string): BuildRequest {
return {
worker_name: '',
server_url: origin,
wallet: config.wallet.address,
threads: config.default_agent_config.threads,
cpu_priority: config.default_agent_config.cpu_priority,
mining_mode: config.default_agent_config.mining_mode,
silent_mode: config.background.silent_mode,
run_as: config.background.run_as,
auto_start: config.background.auto_start,
max_cpu_usage_pct: config.default_agent_config.max_cpu_usage_pct,
min_free_ram_mb: config.default_agent_config.min_free_ram_mb,
idle_threshold_pct: config.default_agent_config.idle_threshold_pct,
idle_duration_minutes: config.default_agent_config.idle_duration_minutes,
schedule_start: config.default_agent_config.schedule_start,
schedule_end: config.default_agent_config.schedule_end,
pool_host: config.pool.host,
pool_port: config.pool.port,
pool_tls: config.pool.use_tls,
pool_pass: config.pool.password,
};
}
export default function BuilderPage() {
const [form, setForm] = useState<BuildRequest | null>(null);
const [building, setBuilding] = useState(false);
const [error, setError] = useState('');
const [lastBuild, setLastBuild] = useState<BuildResponse | null>(null);
const [recentBuilds, setRecentBuilds] = useState<BuildRecord[]>([]);
const [showRecent, setShowRecent] = useState(false);
const [loadingDefaults, setLoadingDefaults] = useState(true);
useEffect(() => {
api.getConfig()
.then((config) => setForm(defaultsFromConfig(config, window.location.origin)))
.catch((err) => {
console.error(err);
setError('Failed to load server defaults from Settings');
})
.finally(() => setLoadingDefaults(false));
}, []);
const loadRecentBuilds = async () => {
try {
const builds = await api.listBuilds();
setRecentBuilds(builds);
setShowRecent(true);
} catch (err) {
console.error(err);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form) return;
setError('');
setLastBuild(null);
if (!form.worker_name.trim()) {
setError('Worker name is required');
return;
}
if (!form.server_url.trim()) {
setError('Server URL is required');
return;
}
if (!form.wallet.trim()) {
setError('Wallet address is required');
return;
}
setBuilding(true);
try {
const result = await api.buildAgent(form);
if (!result.success) {
throw new Error(result.error || 'Build failed');
}
setLastBuild(result);
loadRecentBuilds();
} catch (err: any) {
setError(err.message || 'Build failed');
} finally {
setBuilding(false);
}
};
const updateField = (field: keyof BuildRequest, value: any) => {
setForm((prev) => (prev ? { ...prev, [field]: value } : prev));
};
if (loadingDefaults || !form) {
return (
<div className="page fade-in">
<div className="page-header"><h1>Miner Builder</h1></div>
<div className="card"><p>Loading defaults from Settings...</p></div>
</div>
);
}
return (
<div className="page fade-in">
<div className="page-header">
<h1>Miner Builder</h1>
<button className="btn btn-outline" onClick={loadRecentBuilds}>
Recent Builds
</button>
</div>
<div className="builder-layout">
<div className="card builder-form">
<h2>Build Custom Miner</h2>
<p className="form-description">
Defaults come from Settings. Adjust per worker, then build. The server compiles a Windows
`.exe` with all values baked in and saves it under `data/builds/`.
</p>
<form onSubmit={handleSubmit}>
<div className="form-section">
<h3>Identity</h3>
<div className="form-group">
<label className="label">Worker Name</label>
<input
type="text"
className="input"
placeholder="office-pc-1"
value={form.worker_name}
onChange={(e) => updateField('worker_name', e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="label">Server URL</label>
<input
type="text"
className="input"
value={form.server_url}
onChange={(e) => updateField('server_url', e.target.value)}
required
/>
<span className="form-hint">Control server URL agents connect to (LAN or tunneled domain)</span>
</div>
<div className="form-group">
<label className="label">XMR Wallet Address</label>
<input
type="text"
className="input mono"
value={form.wallet}
onChange={(e) => updateField('wallet', e.target.value)}
required
/>
</div>
</div>
<div className="form-section">
<h3>Pool Configuration</h3>
<div className="form-group">
<label className="label">Pool Host</label>
<input
type="text"
className="input"
value={form.pool_host}
onChange={(e) => updateField('pool_host', e.target.value)}
required
/>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Port</label>
<input
type="number"
className="input"
min={1}
max={65535}
value={form.pool_port}
onChange={(e) => updateField('pool_port', parseInt(e.target.value) || 3333)}
/>
</div>
<div className="form-group checkbox-group" style={{ alignSelf: 'flex-end', paddingBottom: '8px' }}>
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={form.pool_tls}
onChange={(e) => updateField('pool_tls', e.target.checked)}
/>
<span>Use TLS/SSL</span>
</label>
</div>
</div>
<div className="form-group">
<label className="label">Pool Password</label>
<input
type="text"
className="input"
value={form.pool_pass}
onChange={(e) => updateField('pool_pass', e.target.value)}
/>
</div>
</div>
<div className="form-section">
<h3>Performance</h3>
<div className="form-row">
<div className="form-group">
<label className="label">Threads</label>
<input
type="number"
className="input"
min={1}
max={128}
value={form.threads}
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)}
/>
</div>
<div className="form-group">
<label className="label">CPU Priority</label>
<select
className="select"
value={form.cpu_priority}
onChange={(e) => updateField('cpu_priority', e.target.value)}
>
<option value="idle">Idle</option>
<option value="below_normal">Below Normal</option>
<option value="normal">Normal</option>
<option value="above_normal">Above Normal</option>
<option value="high">High</option>
</select>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Max CPU Usage (%)</label>
<input
type="number"
className="input"
min={1}
max={100}
value={form.max_cpu_usage_pct}
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)}
/>
</div>
<div className="form-group">
<label className="label">Min Free RAM (MB)</label>
<input
type="number"
className="input"
min={256}
value={form.min_free_ram_mb}
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)}
/>
</div>
</div>
<div className="form-group">
<label className="label">Mining Mode</label>
<select
className="select"
value={form.mining_mode}
onChange={(e) => updateField('mining_mode', e.target.value)}
>
<option value="always">Always Mine</option>
<option value="idle">Only When Idle</option>
<option value="scheduled">Scheduled Hours</option>
</select>
</div>
{form.mining_mode === 'idle' && (
<div className="form-row">
<div className="form-group">
<label className="label">Idle CPU Threshold (%)</label>
<input
type="number"
className="input"
value={form.idle_threshold_pct}
onChange={(e) => updateField('idle_threshold_pct', parseInt(e.target.value) || 20)}
/>
</div>
<div className="form-group">
<label className="label">Idle Duration (min)</label>
<input
type="number"
className="input"
value={form.idle_duration_minutes}
onChange={(e) => updateField('idle_duration_minutes', parseInt(e.target.value) || 5)}
/>
</div>
</div>
)}
{form.mining_mode === 'scheduled' && (
<div className="form-row">
<div className="form-group">
<label className="label">Start Time</label>
<input
type="time"
className="input"
value={form.schedule_start}
onChange={(e) => updateField('schedule_start', e.target.value)}
/>
</div>
<div className="form-group">
<label className="label">End Time</label>
<input
type="time"
className="input"
value={form.schedule_end}
onChange={(e) => updateField('schedule_end', e.target.value)}
/>
</div>
</div>
)}
</div>
<div className="form-section">
<h3>Deployment</h3>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={form.silent_mode}
onChange={(e) => updateField('silent_mode', e.target.checked)}
/>
<span>Silent / Background Mode (no console window)</span>
</label>
</div>
<div className="form-group">
<label className="label">Run As</label>
<select
className="select"
value={form.run_as}
onChange={(e) => updateField('run_as', e.target.value)}
>
<option value="user">Current User</option>
<option value="service">Windows Service</option>
<option value="scheduled">Scheduled Task</option>
</select>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={form.auto_start}
onChange={(e) => updateField('auto_start', e.target.checked)}
/>
<span>Auto-start with Windows</span>
</label>
</div>
</div>
{error && (
<div className="form-error">
<span></span> {error}
</div>
)}
<button type="submit" className="btn btn-success build-btn" disabled={building}>
{building ? 'Building...' : 'Build Miner .exe'}
</button>
</form>
</div>
{lastBuild?.success && (
<div className="card recent-builds">
<h2>Build Complete</h2>
<div className="build-success">
<p><strong>File:</strong> {lastBuild.file_name}</p>
<p><strong>Size:</strong> {((lastBuild.file_size || 0) / 1024 / 1024).toFixed(2)} MB</p>
<p><strong>Absolute path:</strong></p>
<code className="path-display">{lastBuild.file_path}</code>
<p><strong>Relative path:</strong></p>
<code className="path-display">{lastBuild.relative_path}</code>
{lastBuild.download_url && (
<a className="btn btn-primary" href={lastBuild.download_url} download>
Download .exe
</a>
)}
</div>
</div>
)}
{showRecent && (
<div className="card recent-builds">
<div className="recent-header">
<h2>Recent Builds</h2>
<button className="btn btn-outline" onClick={() => setShowRecent(false)}>Close</button>
</div>
{recentBuilds.length === 0 ? (
<p className="empty-text">No builds yet</p>
) : (
<div className="builds-list">
{recentBuilds.map((build) => (
<div key={build.id} className="build-item">
<div className="build-item-name">{build.worker_name}</div>
<div className="build-item-details">
<span>{build.threads} threads</span>
<span>{(build.file_size / 1024 / 1024).toFixed(1)} MB</span>
<span>{new Date(build.created_at).toLocaleString()}</span>
</div>
{build.file_path && (
<code className="path-display small">{build.file_path}</code>
)}
<a className="btn btn-outline" href={`/api/v1/builds/${build.id}/download`}>
Download
</a>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}