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