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,363 @@
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import type { ServerConfig } from '../types';
import './Pages.css';
export default function SettingsPage() {
const [config, setConfig] = useState<ServerConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState('');
useEffect(() => {
api.getConfig()
.then(setConfig)
.catch(console.error)
.finally(() => setLoading(false));
}, []);
const updateField = (path: string, value: any) => {
if (!config) return;
const newConfig = { ...config };
const keys = path.split('.');
let obj: any = newConfig;
for (let i = 0; i < keys.length - 1; i++) {
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
setConfig(newConfig);
};
const handleSave = async () => {
if (!config) return;
setSaving(true);
setSaveMessage('');
try {
const updated = await api.updateConfig(config);
setConfig(updated);
setSaveMessage('✅ Settings saved successfully');
setTimeout(() => setSaveMessage(''), 3000);
} catch (err: any) {
setSaveMessage(`❌ Failed to save: ${err.message}`);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="page fade-in">
<div className="page-header"><h1>Settings</h1></div>
<div className="card"><p>Loading settings...</p></div>
</div>
);
}
if (!config) {
return (
<div className="page fade-in">
<div className="page-header"><h1>Settings</h1></div>
<div className="card"><p>Failed to load settings</p></div>
</div>
);
}
return (
<div className="page fade-in">
<div className="page-header">
<h1>Settings</h1>
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
{saving ? '💾 Saving...' : '💾 Save Settings'}
</button>
</div>
{saveMessage && (
<div className={`save-message ${saveMessage.includes('✅') ? 'success' : 'error'}`}>
{saveMessage}
</div>
)}
<div className="settings-grid">
{/* Pool Configuration */}
<div className="card settings-section">
<h2>Pool Connection</h2>
<p className="section-desc">Configure which Monero pool your miners connect to.</p>
<div className="form-group">
<label className="label">Pool Host</label>
<input
type="text"
className="input"
value={config.pool.host}
onChange={(e) => updateField('pool.host', e.target.value)}
placeholder="pool.supportxmr.com"
/>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Port</label>
<input
type="number"
className="input"
value={config.pool.port}
onChange={(e) => updateField('pool.port', parseInt(e.target.value) || 3333)}
/>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={config.pool.use_tls}
onChange={(e) => updateField('pool.use_tls', e.target.checked)}
/>
<span>Use TLS/SSL</span>
</label>
</div>
</div>
<div className="form-group">
<label className="label">Password (optional)</label>
<input
type="text"
className="input"
value={config.pool.password}
onChange={(e) => updateField('pool.password', e.target.value)}
placeholder="x"
/>
</div>
</div>
{/* Wallet Configuration */}
<div className="card settings-section">
<h2>Wallet</h2>
<p className="section-desc">Default wallet address for new miners.</p>
<div className="form-group">
<label className="label">XMR Wallet Address</label>
<input
type="text"
className="input mono"
value={config.wallet.address}
onChange={(e) => updateField('wallet.address', e.target.value)}
placeholder="4..."
/>
</div>
<div className="form-group">
<label className="label">Payment ID (optional)</label>
<input
type="text"
className="input mono"
value={config.wallet.payment_id}
onChange={(e) => updateField('wallet.payment_id', e.target.value)}
/>
</div>
</div>
{/* Default Agent Config */}
<div className="card settings-section">
<h2>Default Agent Configuration</h2>
<p className="section-desc">Default settings applied to newly built miners.</p>
<div className="form-row">
<div className="form-group">
<label className="label">Threads</label>
<input
type="number"
className="input"
min={1}
max={128}
value={config.default_agent_config.threads}
onChange={(e) => updateField('default_agent_config.threads', parseInt(e.target.value) || 1)}
/>
</div>
<div className="form-group">
<label className="label">CPU Priority</label>
<select
className="select"
value={config.default_agent_config.cpu_priority}
onChange={(e) => updateField('default_agent_config.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={config.default_agent_config.max_cpu_usage_pct}
onChange={(e) => updateField('default_agent_config.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={config.default_agent_config.min_free_ram_mb}
onChange={(e) => updateField('default_agent_config.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={config.default_agent_config.mining_mode}
onChange={(e) => updateField('default_agent_config.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>
{config.default_agent_config.mining_mode === 'idle' && (
<div className="form-row">
<div className="form-group">
<label className="label">Idle CPU Threshold (%)</label>
<input
type="number"
className="input"
min={1}
max={100}
value={config.default_agent_config.idle_threshold_pct}
onChange={(e) => updateField('default_agent_config.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"
min={1}
value={config.default_agent_config.idle_duration_minutes}
onChange={(e) => updateField('default_agent_config.idle_duration_minutes', parseInt(e.target.value) || 5)}
/>
</div>
</div>
)}
{config.default_agent_config.mining_mode === 'scheduled' && (
<div className="form-row">
<div className="form-group">
<label className="label">Start Time</label>
<input
type="time"
className="input"
value={config.default_agent_config.schedule_start}
onChange={(e) => updateField('default_agent_config.schedule_start', e.target.value)}
/>
</div>
<div className="form-group">
<label className="label">End Time</label>
<input
type="time"
className="input"
value={config.default_agent_config.schedule_end}
onChange={(e) => updateField('default_agent_config.schedule_end', e.target.value)}
/>
</div>
</div>
)}
</div>
{/* Background / Silent Mode */}
<div className="card settings-section">
<h2>Background & Deployment</h2>
<p className="section-desc">How miners behave on target machines.</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={config.background.silent_mode}
onChange={(e) => updateField('background.silent_mode', e.target.checked)}
/>
<span>Silent Mode (no console window, runs hidden)</span>
</label>
</div>
<div className="form-group">
<label className="label">Run As</label>
<select
className="select"
value={config.background.run_as}
onChange={(e) => updateField('background.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={config.background.auto_start}
onChange={(e) => updateField('background.auto_start', e.target.checked)}
/>
<span>Auto-start with Windows</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={config.background.minimize_to_tray}
onChange={(e) => updateField('background.minimize_to_tray', e.target.checked)}
/>
<span>Minimize to System Tray</span>
</label>
</div>
</div>
{/* Alerts */}
<div className="card settings-section">
<h2>Alerts</h2>
<p className="section-desc">Configure thresholds for fleet health alerts.</p>
<div className="form-group">
<label className="label">Offline Threshold (minutes)</label>
<input
type="number"
className="input"
min={1}
value={config.alerts.offline_threshold_minutes}
onChange={(e) => updateField('alerts.offline_threshold_minutes', parseInt(e.target.value) || 5)}
/>
<span className="form-hint">Alert if agent hasn't reported in this many minutes</span>
</div>
<div className="form-group">
<label className="label">Hashrate Drop Threshold (%)</label>
<input
type="number"
className="input"
min={1}
max={100}
value={config.alerts.hashrate_drop_threshold_pct}
onChange={(e) => updateField('alerts.hashrate_drop_threshold_pct', parseInt(e.target.value) || 50)}
/>
<span className="form-hint">Alert if hashrate drops by this percentage</span>
</div>
<div className="form-group">
<label className="label">Rejection Rate Threshold (%)</label>
<input
type="number"
className="input"
min={1}
max={100}
value={config.alerts.rejection_rate_threshold_pct}
onChange={(e) => updateField('alerts.rejection_rate_threshold_pct', parseInt(e.target.value) || 5)}
/>
<span className="form-hint">Alert if share rejection rate exceeds this percentage</span>
</div>
</div>
</div>
</div>
);
}