feat: Build Manager, Crucible ops deck, branding, and portable Cloudflare tunnel

Add Build Manager with pin-to-dropper, Crucible multi-node terminal with SSH probe/wake, Command Deck chart balance and pretty stats, AetherForge logo and sacred geometry UI, Field Guide refresh, and LAUNCH.bat Cloudflare MSI + token service install flow.
This commit is contained in:
AetherForge
2026-05-30 22:38:48 -07:00
parent e6b8d84edf
commit 9232f4c448
45 changed files with 4222 additions and 406 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../api/client';
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo, BlueprintInfo, FusionEstimate } from '../types';
import { HelpTip, FieldHint } from '../components/HelpTip';
@@ -19,11 +19,9 @@ import {
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';
import AuthDownloadButton from '../components/AuthDownloadButton';
import { buildRequestFromRecord } from '../help/buildManager';
import DownloadButton from '../components/DownloadButton';
import { downloadApiFile } from '../api/download';
import { useForge } from '../context/ForgeContext';
import {
fusionPayloadKind,
fusionTitleFromFilename,
@@ -50,6 +48,37 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
return forgeDefaultsFromServerSmart(config, serverInfo, builds);
}
// Simulated stage timeline — (label, target% completed at this point, min ms from start)
const FORGE_STAGES: { label: string; pct: number; minMs: number }[] = [
{ label: 'Resolving dependencies...', pct: 8, minMs: 0 },
{ label: 'Compiling agent source...', pct: 28, minMs: 800 },
{ label: 'Cross-compiling targets...', pct: 52, minMs: 2500 },
{ label: 'Applying obfuscation...', pct: 68, minMs: 5000 },
{ label: 'Packaging deliverable...', pct: 82, minMs: 8000 },
{ label: 'Signing & finalizing...', pct: 93, minMs: 11000 },
{ label: 'Almost done...', pct: 98, minMs: 15000 },
];
function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) {
if (!building) return null;
return (
<div className="forge-progress-wrap" aria-live="polite">
<div className="forge-progress-header">
<span className="forge-progress-icon"></span>
<span className="forge-progress-stage">{stage || 'Initializing...'}</span>
<span className="forge-progress-pct">{Math.round(progress)}%</span>
</div>
<div className="forge-progress-track">
<div
className="forge-progress-fill"
style={{ width: `${progress}%` }}
/>
<div className="forge-progress-glow" style={{ left: `${progress}%` }} />
</div>
</div>
);
}
const FORGE_MODE_KEY = 'aetherforge-forge-mode';
function loadSimpleMode(): boolean {
@@ -63,12 +92,16 @@ function loadSimpleMode(): boolean {
}
export default function BuilderPage() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge();
const forgeStageTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [form, setForm] = useState<BuildRequest | null>(null);
const [building, setBuilding] = useState(false);
const [error, setError] = useState('');
const [lastBuild, setLastBuild] = useState<BuildResponse | null>(null);
const [recentBuilds, setRecentBuilds] = useState<BuildRecord[]>([]);
const [showRecent, setShowRecent] = useState(false);
const [loadingDefaults, setLoadingDefaults] = useState(true);
const [fusionPrepFile, setFusionPrepFile] = useState<File | null>(null);
const [fusionBatchFiles, setFusionBatchFiles] = useState<File[]>([]);
@@ -92,6 +125,45 @@ export default function BuilderPage() {
const [refreshingEndpoints, setRefreshingEndpoints] = useState(false);
const [simpleMode, setSimpleMode] = useState(loadSimpleMode);
// Drive simulated stage progress while a single build is running
useEffect(() => {
if (!building || batchJob) {
endForge();
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
return;
}
const startMs = Date.now();
startForge();
let stageIdx = 0;
const advance = () => {
const elapsed = Date.now() - startMs;
// Find the furthest stage whose minMs has been reached
let next = 0;
for (let i = 0; i < FORGE_STAGES.length; i++) {
if (elapsed >= FORGE_STAGES[i].minMs) next = i;
else break;
}
const s = FORGE_STAGES[next];
// Smoothly interpolate within this stage toward the next stage's target %
const nextPct = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].pct : 98;
const nextMs = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].minMs : 20000;
const stageElapsed = elapsed - s.minMs;
const stageDur = nextMs - s.minMs;
const frac = stageDur > 0 ? Math.min(1, stageElapsed / stageDur) : 0;
const pct = s.pct + (nextPct - s.pct) * frac;
if (next !== stageIdx) stageIdx = next;
setStage(s.label, Math.min(98, pct));
forgeStageTimerRef.current = setTimeout(advance, 250);
};
advance();
return () => {
if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [building, batchJob]);
const setForgeMode = (simple: boolean) => {
setSimpleMode(simple);
try {
@@ -142,32 +214,28 @@ export default function BuilderPage() {
try {
const builds = await api.listBuilds();
setRecentBuilds(builds);
setShowRecent(true);
} catch (err) {
console.error(err);
}
};
// Handle ?reforge=<buildId> links from Build Manager page
useEffect(() => {
const reforgeId = searchParams.get('reforge');
if (!reforgeId || recentBuilds.length === 0) return;
const match = recentBuilds.find((b) => b.id === reforgeId);
if (match) {
reForgeFromBuild(match);
setSearchParams({}, { replace: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, recentBuilds]);
const finishForgeSuccess = async (result: BuildResponse) => {
setStage('Build complete!', 100);
setLastBuild(result);
loadRecentBuilds();
const url = result.bundle_download_url || result.download_url;
const name = result.bundle_file_name || result.file_name;
if (url && name) {
try {
await downloadApiFile(url, name);
} catch (err) {
console.error('Auto-download failed:', err);
}
return;
}
if (result.download_url && result.file_name) {
try {
await downloadApiFile(result.download_url, result.file_name);
} catch (err) {
console.error('Auto-download failed:', err);
}
}
// No auto-download — user downloads from the strip below or Build Manager page
};
// Blueprint: save current form as a named blueprint
@@ -250,10 +318,6 @@ export default function BuilderPage() {
}
};
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) => {
@@ -493,7 +557,8 @@ export default function BuilderPage() {
const candidates = lanEndpointCandidates(info, config.port || info.port);
const base = defaultsFromConfig(config, info, builds);
const kind = form ? deriveDeliverableType(form) : 'single';
const merged = applySmartForgeDefaults({ ...base, fusion_enabled: form.fusion_enabled }, { builds, endpointCandidates: candidates });
// Preserve the user's manually-entered server_url — don't overwrite with LAN IP on defaults refresh
const merged = applySmartForgeDefaults({ ...base, fusion_enabled: form.fusion_enabled, server_url: form.server_url || base.server_url }, { builds, endpointCandidates: candidates });
setForm(applyDeliverableType(merged, kind));
setBlueprintMsg('✅ Recommended defaults applied');
setTimeout(() => setBlueprintMsg(''), 2500);
@@ -1881,6 +1946,8 @@ export default function BuilderPage() {
</div>
)}
<ForgeProgressBar building={building && !batchJob} stage={forgeStage} progress={forgeProgress} />
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<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`}
@@ -1900,146 +1967,39 @@ export default function BuilderPage() {
</div>
{lastBuild?.success && (
<div className="card recent-builds">
<h2>Installer Ready</h2>
<div className="build-success">
<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>
)}
<p><strong>File:</strong> {lastBuild.file_name}</p>
<p><strong>Size:</strong> {((lastBuild.file_size || 0) / 1024 / 1024).toFixed(2)} MB</p>
{lastBuild.fusion_export_dir && (
<>
<p><strong>Movie deliverables folder:</strong></p>
<code className="path-display">{lastBuild.fusion_export_dir}</code>
</>
)}
{lastBuild.export_path && (
<>
<p><strong>Your file (project root):</strong></p>
<code className="path-display">{lastBuild.export_path}</code>
{!lastBuild.fusion_export_dir && (
<p className="form-hint">Fusion output keeps prep icon + File Description / version strings from your uploaded prep.exe (Windows).</p>
)}
{(lastBuild.obfuscated || lastBuild.signed) && (
<p className="form-hint">
{lastBuild.obfuscated && 'Garble obfuscation applied. '}
{lastBuild.signed && 'Authenticode signature applied.'}
</p>
)}
</>
)}
<p className="form-hint">Archive copy: <code className="mono-sm">{lastBuild.file_path}</code></p>
<div className="download-actions">
{lastBuild.download_url && lastBuild.file_name && (
<DownloadButton
apiPath={lastBuild.download_url}
filename={lastBuild.file_name}
className="btn btn-primary btn-lg"
>
Download {lastBuild.file_name}
</DownloadButton>
<div className="forge-last-build-strip">
<div className="forge-last-build-info">
<span className="forge-last-build-check"></span>
<div>
<span className="forge-last-build-name">{lastBuild.file_name || 'Build ready'}</span>
{lastBuild.file_size != null && (
<span className="forge-last-build-size">
{((lastBuild.file_size || 0) / 1024 / 1024).toFixed(1)} MB
</span>
)}
{!lastBuild.bundle_download_url && lastBuild.build_id && lastBuild.extra_files?.map((f) => (
<DownloadButton
key={f.file_name}
apiPath={api.buildArtifactUrl(lastBuild.build_id!, f.file_name)}
filename={f.file_name}
className="btn btn-secondary"
>
Download {f.file_name}
</DownloadButton>
))}
<p className="form-hint">
{lastBuild.bundle_file_name
? 'One ZIP per title — extract and run the runner only (agent is inside it, hidden).'
: 'Saved to your browser Downloads when the forge completes. Click again if needed.'}
</p>
{lastBuild.fusion_enabled && <span className="forge-last-build-tag">FUSION</span>}
{lastBuild.obfuscated && <span className="forge-last-build-tag">GARBLED</span>}
{lastBuild.signed && <span className="forge-last-build-tag">SIGNED</span>}
</div>
{lastBuild.uninstall_export_path && (
<p><strong>Uninstaller copy:</strong> <code className="mono-sm">{lastBuild.uninstall_export_path}</code></p>
)}
{lastBuild.uninstall_download_url && (
<>
<p><strong>Uninstaller:</strong> {lastBuild.uninstall_file_name}</p>
{!lastBuild.uninstall_export_path && (
<code className="path-display">{lastBuild.uninstall_path}</code>
)}
<AuthDownloadButton
apiPath={lastBuild.uninstall_download_url}
filename={lastBuild.uninstall_file_name || 'uninstall.ps1'}
className="btn btn-outline"
>
Download uninstall script
</AuthDownloadButton>
</>
)}
</div>
</div>
)}
{showRecent && (
<div className="card recent-builds">
<div className="recent-header">
<h2>Build Manager</h2>
<button className="btn btn-outline" onClick={() => setShowRecent(false)}>Close</button>
<div className="forge-last-build-actions">
{lastBuild.download_url && lastBuild.file_name && (
<DownloadButton
apiPath={lastBuild.download_url}
filename={lastBuild.file_name}
className="btn btn-primary btn-sm"
>
Download
</DownloadButton>
)}
<button
type="button"
className="btn btn-outline btn-sm"
onClick={() => navigate('/builds')}
>
View in Build Manager
</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="build-manager-grid">
{recentBuilds.map((build) => {
const downloadUrl = `${window.location.origin}${api.buildDownloadUrl(build.id)}`;
const exeName =
build.file_path?.replace(/^.*[/\\]/, '') || `install-${build.worker_name}.exe`;
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} />
<DownloadButton
apiPath={api.buildDownloadUrl(build.id)}
filename={exeName}
className="btn btn-outline"
>
Download
</DownloadButton>
<AuthDownloadButton
apiPath={api.buildUninstallUrl(build.id)}
filename={`uninstall-${build.worker_name || 'worker'}.ps1`}
className="btn btn-outline"
>
Uninstall script
</AuthDownloadButton>
<button type="button" className="btn btn-primary" disabled={building} onClick={() => reForgeFromBuild(build)}>
Re-forge
</button>
</div>
);
})}
</div>
)}
</div>
)}
</div>