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:
70
server/web/src/components/PoolPresetPicker.css
Normal file
70
server/web/src/components/PoolPresetPicker.css
Normal 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));
|
||||
}
|
||||
244
server/web/src/components/PoolPresetPicker.tsx
Normal file
244
server/web/src/components/PoolPresetPicker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
21
server/web/src/components/SetupBanner.tsx
Normal file
21
server/web/src/components/SetupBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user