Add EC2 Launch Template strain genesis for AWS horizontal scale.

Ship POST /api/v1/forge/launch-template with cloud-init user-data embedding genesis snapshot hash and strain card ID; first template-tagged auth pins SpreadGeneration=0 and ParentAgentID=template.
This commit is contained in:
AetherForge
2026-06-07 09:57:11 -07:00
parent 7d15888ab0
commit 7191bda6fd
12 changed files with 462 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import { useState } from 'react';
import { api } from '../../api/client';
import { LAUNCH_TEMPLATE_FILES, downloadLaunchTemplateFile, type LaunchTemplateExportResponse } from '../../help/launchTemplateExport';
export default function LaunchTemplateExportPanel({ serverBase, buildId = '', campaign = '', strainCardId = '' }: {
serverBase: string; buildId?: string; campaign?: string; strainCardId?: string;
}) {
const [busy, setBusy] = useState(false);
const [err, setErr] = useState('');
const [last, setLast] = useState<LaunchTemplateExportResponse | null>(null);
const onGenerate = async () => {
setErr('');
setBusy(true);
try {
const resp = await api.forgeLaunchTemplate({
server_url: serverBase,
build_id: buildId.trim() || undefined,
campaign: campaign.trim() || undefined,
strain_card_id: strainCardId.trim() || undefined,
});
if (!resp.success) throw new Error(resp.error ?? 'launch template export failed');
setLast(resp);
} catch (e) {
setErr(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
};
return (
<div className="crucible-launch-template" style={{ marginTop: '0.75rem' }}>
<p className="crucible-seek-blurb" style={{ marginBottom: '0.5rem' }}>
EC2 Launch Template strain genesis cloud-init embeds genesis snapshot hash, strain card ID, and server URL.
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}>
<button type="button" className="button crucible-op-btn" disabled={busy || !serverBase.trim()} onClick={() => void onGenerate()}>
{busy ? 'Generating…' : 'Generate launch template'}
</button>
{last ? LAUNCH_TEMPLATE_FILES.map((f) => (
<button key={f.id} type="button" className="button crucible-op-btn" onClick={() => downloadLaunchTemplateFile(last, f.field, f.label)}>
{f.label}
</button>
)) : null}
</div>
{err ? <p className="form-error" style={{ marginTop: '0.35rem' }}>{err}</p> : null}
</div>
);
}