import { useState, useEffect, useRef } from 'react'; import { api } from '../api/client'; import type { ServerConfig } from '../types'; import { HelpTip, FieldHint } from '../components/HelpTip'; import NeonCard from '../components/NeonCard/NeonCard'; import './Pages.css'; export default function SettingsPage() { const [config, setConfig] = useState(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(null); useEffect(() => { 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', open_firewall_on_start: cfg.server?.open_firewall_on_start ?? true, }, }); 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; setSaving(true); setSaveMessage(''); try { const updated = await api.updateConfig(config); 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 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); 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 (

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, }; 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.

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

DETECTED LAN ENDPOINTS

Suggested: {serverInfo.suggested_url}

{serverInfo.local_ips?.length > 0 && (

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

)}

Set Public URL below if you want the Forge to default to a specific address.

)}

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)} />
updateField('server.dashboard_subtitle', e.target.value)} />

Upstream Pool

The control server connects here and relays work to your fleet (not per-miner in this tab).

updateField('pool.host', e.target.value)} />
updateField('pool.port', parseInt(e.target.value) || 3333)} />
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)} />

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 and email when fleet thresholds fire (offline, hashrate crash, rejection spike).

updateField('alerts.telegram_bot_token', e.target.value)} placeholder="123456:ABC…" />
updateField('alerts.telegram_chat_id', e.target.value)} placeholder="-100…" />
{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)} />
)}

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.

); }