Add pool presets, operator bake-ins, and USB pack improvements

- Pool preset checkboxes with failover in Calibrate and Forge

- Tier 1/2 UX: setup banner, forge next worker, LAN defaults, blueprint prompt

- USB: auto-install forge tools, seed config.json, sync LAUNCH.bat
This commit is contained in:
AetherForge
2026-06-01 21:06:19 -07:00
parent 1e0482ebe1
commit 1413a9394f
29 changed files with 1078 additions and 314 deletions

View File

@@ -0,0 +1,70 @@
.pool-preset-picker {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.pool-preset-hint {
margin: 0;
}
.pool-preset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.65rem 1rem;
}
.pool-preset-group {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.5rem 0.65rem;
border: 1px solid var(--border-dim, rgba(0, 255, 200, 0.15));
border-radius: 6px;
background: rgba(0, 0, 0, 0.2);
}
.pool-preset-provider {
font-size: 0.75rem;
letter-spacing: 0.06em;
color: var(--neon-cyan, #0ff);
margin-bottom: 0.15rem;
}
.pool-preset-option {
font-size: 0.85rem;
align-items: flex-start;
}
.pool-preset-sub {
opacity: 0.65;
font-size: 0.78rem;
}
.pool-preset-custom-fields {
margin-top: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.pool-preset-custom-fields .input.mono {
flex: 2 1 160px;
}
.pool-preset-order {
font-size: 0.82rem;
padding: 0.5rem 0.65rem;
border-left: 3px solid var(--neon-cyan, #0ff);
background: rgba(0, 40, 50, 0.25);
}
.pool-preset-order ol {
margin: 0.35rem 0 0;
padding-left: 1.25rem;
}
.pool-preset-manual {
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 1px dashed var(--border-dim, rgba(255, 255, 255, 0.12));
}

View File

@@ -0,0 +1,244 @@
import { useEffect, useMemo, useState } from 'react';
import type { BackupPool } from '../types';
import {
XMR_POOL_PRESETS,
DEFAULT_PRESET_IDS,
orderedPoolsFromSelection,
detectPresetIds,
detectCustomEndpoint,
applyPoolsToForgeFields,
type PoolForgeFields,
} from '../help/poolPresets';
import './PoolPresetPicker.css';
interface PoolPresetPickerProps {
host: string;
port: number;
tls: boolean;
pass: string;
backups?: BackupPool[];
onChange: (next: PoolForgeFields) => void;
/** Show manual host/port fields below presets (Forge advanced). */
showManualFields?: boolean;
}
export default function PoolPresetPicker({
host,
port,
tls,
pass,
backups = [],
onChange,
showManualFields = false,
}: PoolPresetPickerProps) {
const [selectedIds, setSelectedIds] = useState<string[]>(() => {
const detected = detectPresetIds(host, port, tls, backups);
return detected.length ? detected : [...DEFAULT_PRESET_IDS];
});
const customDetected = detectCustomEndpoint(host, port, tls, backups);
const [useCustom, setUseCustom] = useState(!!customDetected);
const [customHost, setCustomHost] = useState(customDetected?.host ?? '');
const [customPort, setCustomPort] = useState(customDetected?.port ?? 3333);
const [customTls, setCustomTls] = useState(customDetected?.tls ?? false);
useEffect(() => {
const detected = detectPresetIds(host, port, tls, backups);
if (detected.length) setSelectedIds(detected);
const c = detectCustomEndpoint(host, port, tls, backups);
if (c) {
setUseCustom(true);
setCustomHost(c.host);
setCustomPort(c.port);
setCustomTls(c.tls);
}
}, [host, port, tls, backups]);
const orderedPreview = useMemo(() => {
const custom: BackupPool | null = useCustom && customHost.trim()
? { host: customHost.trim(), port: customPort || 3333, tls: customTls, pass: pass || 'x' }
: null;
return orderedPoolsFromSelection(selectedIds, pass || 'x', custom);
}, [selectedIds, pass, useCustom, customHost, customPort, customTls]);
const applySelection = (ids: string[], customOn: boolean, cHost: string, cPort: number, cTls: boolean) => {
const custom: BackupPool | null =
customOn && cHost.trim()
? { host: cHost.trim(), port: cPort || 3333, tls: cTls, pass: pass || 'x' }
: null;
const pools = orderedPoolsFromSelection(ids, pass || 'x', custom);
onChange(applyPoolsToForgeFields(pools));
};
const togglePreset = (id: string) => {
const next = selectedIds.includes(id)
? selectedIds.filter((x) => x !== id)
: [...selectedIds, id];
if (!next.length) return;
setSelectedIds(next);
applySelection(next, useCustom, customHost, customPort, customTls);
};
const grouped = useMemo(() => {
const map = new Map<string, typeof XMR_POOL_PRESETS>();
for (const p of XMR_POOL_PRESETS) {
const list = map.get(p.provider) ?? [];
list.push(p);
map.set(p.provider, list);
}
return [...map.entries()];
}, []);
return (
<div className="pool-preset-picker">
<p className="form-hint pool-preset-hint">
Check one or more pools. The control server and forged miners try each in order and use the first
reachable endpoint (round-robin failover). Wallet address only no pool account needed.
</p>
<div className="pool-preset-grid">
{grouped.map(([provider, presets]) => (
<div key={provider} className="pool-preset-group">
<span className="pool-preset-provider font-tech">{provider}</span>
{presets.map((p) => (
<label key={p.id} className="pool-preset-option checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={selectedIds.includes(p.id)}
onChange={() => togglePreset(p.id)}
/>
<span>
<code>{p.host}:{p.port}</code>
{p.tls ? ' TLS' : ''}
<span className="pool-preset-sub"> ({p.hint})</span>
</span>
</label>
))}
</div>
))}
</div>
<div className="pool-preset-custom">
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={useCustom}
onChange={(e) => {
const on = e.target.checked;
setUseCustom(on);
applySelection(selectedIds, on, customHost, customPort, customTls);
}}
/>
<span>Add custom pool</span>
</label>
{useCustom && (
<div className="pool-preset-custom-fields form-row">
<input
type="text"
className="input mono"
placeholder="pool.example.com"
value={customHost}
onChange={(e) => {
setCustomHost(e.target.value);
applySelection(selectedIds, true, e.target.value, customPort, customTls);
}}
/>
<input
type="number"
className="input"
min={1}
max={65535}
value={customPort}
onChange={(e) => {
const v = e.target.valueAsNumber || 3333;
setCustomPort(v);
applySelection(selectedIds, true, customHost, v, customTls);
}}
/>
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={customTls}
onChange={(e) => {
setCustomTls(e.target.checked);
applySelection(selectedIds, true, customHost, customPort, e.target.checked);
}}
/>
<span>TLS</span>
</label>
</div>
)}
</div>
{orderedPreview.length > 0 && (
<div className="pool-preset-order">
<span className="font-tech">Failover order</span>
<ol>
{orderedPreview.map((p, i) => (
<li key={`${p.host}:${p.port}:${p.tls}`}>
{i === 0 ? 'Primary' : `Backup ${i}`}: {p.host}:{p.port}
{p.tls ? ' (TLS)' : ''}
</li>
))}
</ol>
</div>
)}
{showManualFields && (
<div className="pool-preset-manual form-row">
<div className="form-group">
<label className="label">Pool Host</label>
<input
type="text"
className="input mono"
value={host}
onChange={(e) =>
onChange({
pool_host: e.target.value,
pool_port: port,
pool_tls: tls,
backup_pools: backups,
})
}
/>
</div>
<div className="form-group">
<label className="label">Port</label>
<input
type="number"
className="input"
min={1}
max={65535}
value={port}
onChange={(e) =>
onChange({
pool_host: host,
pool_port: e.target.valueAsNumber || 3333,
pool_tls: tls,
backup_pools: backups,
})
}
/>
</div>
<label className="checkbox-label" style={{ alignSelf: 'flex-end' }}>
<input
type="checkbox"
className="checkbox"
checked={tls}
onChange={(e) =>
onChange({
pool_host: host,
pool_port: port,
pool_tls: e.target.checked,
backup_pools: backups,
})
}
/>
<span>TLS</span>
</label>
</div>
)}
</div>
);
}

View File

@@ -57,6 +57,9 @@ export default function SessionGate({ children }: { children: ReactNode }) {
<form className="session-gate-card card" onSubmit={handleLogin}>
<h1 className="font-display">AetherForge</h1>
<p className="form-hint">Sign in to open the command deck.</p>
<p className="form-hint" style={{ marginTop: '0.5rem' }}>
First run: password is in the LAUNCH console or <code className="mono-sm">data\login-credentials.json</code> next to the server data folder.
</p>
<label className="label" htmlFor="session-user">Username</label>
<input id="session-user" className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
<label className="label" htmlFor="session-pass">Password</label>

View File

@@ -0,0 +1,21 @@
import { Link } from 'react-router-dom';
import NeonCard from './NeonCard/NeonCard';
import type { SetupStatus } from '../help/setupStatus';
export default function SetupBanner({ status }: { status: SetupStatus }) {
if (!status.needsCalibration) return null;
return (
<NeonCard accent="amber" className="calibrate-banner" hud style={{ marginBottom: '1rem' }}>
<p className="font-tech">SETUP INCOMPLETE</p>
<ul className="form-hint" style={{ margin: '0.5rem 0', paddingLeft: '1.25rem' }}>
{status.reasons.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
<p className="form-hint">Finish one-time calibration so Forge and the pool relay use your wallet and LAN URL.</p>
<Link to="/settings" className="btn btn-primary btn-sm" style={{ marginTop: '0.75rem' }}>
Open Calibrate
</Link>
</NeonCard>
);
}