From 9ad4cce1947437ec56a3fc87bb94c6558ffa285e Mon Sep 17 00:00:00 2001 From: AetherForge Date: Sun, 7 Jun 2026 00:42:57 -0700 Subject: [PATCH] Tie Mission Deck loose ends: collapse excessive blank lines (DV-15) and refresh PROBLEMS test inventory. Removes resolved DV-15 maintainability issue, documents P1 LOTL/recon coverage vs P2 live-integration gaps, and keeps FAST PATH hero intact with Pages.css import. --- PROBLEMS.md | 4 +- server/web/src/pages/MissionDeckPage.tsx | 1722 ++++++++-------------- 2 files changed, 617 insertions(+), 1109 deletions(-) diff --git a/PROBLEMS.md b/PROBLEMS.md index 678108c..5ba6c03 100644 --- a/PROBLEMS.md +++ b/PROBLEMS.md @@ -89,7 +89,6 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07. | DV-09 | Dead chart badge styles in wealth-deck CSS. | | DV-13 | Dark-only; no light / `prefers-color-scheme` path. | | DV-14 | Sidebar footer hard-coded `v0.0.1`. | -| DV-15 | `MissionDeckPage.tsx` excessive blank lines (maintainability). | ## Help / product copy gaps @@ -103,7 +102,8 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07. | Item | Notes | |------|-------| -| LOTL / triple-onion / fleet recon | Go + Vitest coverage in `tests/README.md` (P0/P1 + `-ReconOnly`); live spread lanes and E2E recon flow still stub/mock-only. | +| **P1 covered (2026-06-06)** | LOTL tier chain, triple-onion gates, fleet recon (vuln/cred/service graph), Crucible recon UI, `stats_batch` LOTL merge — Go + Vitest in `tests/README.md`; run `e2e-validate.ps1 -ReconOnly`. | +| **P2 remaining** | Live Docker/Podman start, real WinRM/GPO/systemd spread lanes, BITS/curl downloads, full `MiningChainRunner` lifecycle, E2E Crucible `lotl_tier` badge with live agent. | | Agent pathtracer Go tests | Windows impl + stub have limited coverage (`pathtracer_stub_test.go` started). | | Client WS/beacon paths | Integration-heavy; Docker Tier 2 covers Linux slice only. | | Path Tracer 2s REST poll | No WS hop progress; acceptable latency, extra load while tracing. | diff --git a/server/web/src/pages/MissionDeckPage.tsx b/server/web/src/pages/MissionDeckPage.tsx index 9b5a30c..40c8297 100644 --- a/server/web/src/pages/MissionDeckPage.tsx +++ b/server/web/src/pages/MissionDeckPage.tsx @@ -1,1148 +1,656 @@ -/** - * Mission Deck — video-game loadout screen. - * Route: /mission-deck - * Ghost / Loud / Spread chips, 3D preview, spread profile + campaign — one strike runs runForgeMission. - * Full forge options live at /forge (BuilderPage). - */ - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { Link } from 'react-router-dom'; - -import { api } from '../api/client'; - -import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types'; - -import NeonCard from '../components/NeonCard/NeonCard'; - -import { HelpTip } from '../components/HelpTip'; - -import AlsoHere from '../components/Presence/AlsoHere'; - -import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal'; - -import { useModalAmbientDuck } from '../context/AmbientMusicContext'; -import { useForge } from '../context/ForgeContext'; - -import { forgeDefaultsFromServerSmart, applySmartForgeDefaults } from '../help/forgeSmartDefaults'; - -import { lanEndpointCandidates } from '../help/endpointHelpers'; - -import { runForgePreflight, preflightHasErrors } from '../help/forgeValidation'; - -import { applyForgeFieldUpdate } from '../help/forgeRules'; - -import { - applyOperationMode, - forgeSkinClassName, - loadStoredOperationMode, - loadStoredForgeTheme, - resolveForgeSkin, - storeOperationMode, - type OperationModeId, -} from '../help/forgeOperationModes'; - -import { SPREAD_PROFILES, applySpreadProfile, type SpreadProfileId } from '../help/spreadProfiles'; - -import { - MISSION_STEPS, - MISSION_STEP_LABELS, - applyMissionPresets, - missionStepStatus, - runForgeMission, - type MissionStep, -} from '../help/forgeMission'; - -import { - MISSION_OPERATION_CHIPS, - missionChipForMode, - operationModeForChip, - type MissionOperationChip, -} from '../help/forgeMissionWizard'; - -import './Pages.css'; - +/** + * Mission Deck — video-game loadout screen. + * Route: /mission-deck + * Ghost / Loud / Spread chips, 3D preview, spread profile + campaign — one strike runs runForgeMission. + * Full forge options live at /forge (BuilderPage). + */ + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../api/client'; +import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types'; +import NeonCard from '../components/NeonCard/NeonCard'; +import { HelpTip } from '../components/HelpTip'; +import AlsoHere from '../components/Presence/AlsoHere'; +import ForgeDispenseReveal from '../components/Forge/ForgeDispenseReveal'; +import { useModalAmbientDuck } from '../context/AmbientMusicContext'; +import { useForge } from '../context/ForgeContext'; +import { forgeDefaultsFromServerSmart, applySmartForgeDefaults } from '../help/forgeSmartDefaults'; +import { lanEndpointCandidates } from '../help/endpointHelpers'; +import { runForgePreflight, preflightHasErrors } from '../help/forgeValidation'; +import { applyForgeFieldUpdate } from '../help/forgeRules'; +import { + + applyOperationMode, + forgeSkinClassName, + loadStoredOperationMode, + loadStoredForgeTheme, + resolveForgeSkin, + storeOperationMode, + type OperationModeId, +} from '../help/forgeOperationModes'; +import { SPREAD_PROFILES, applySpreadProfile, type SpreadProfileId } from '../help/spreadProfiles'; +import { + + MISSION_STEPS, + MISSION_STEP_LABELS, + applyMissionPresets, + missionStepStatus, + runForgeMission, + type MissionStep, +} from '../help/forgeMission'; +import { + + MISSION_OPERATION_CHIPS, + missionChipForMode, + operationModeForChip, + type MissionOperationChip, +} from '../help/forgeMissionWizard'; +import './Pages.css'; import './MissionDeckPage.css'; -function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds: BuildRecord[] = []): BuildRequest { - - return forgeDefaultsFromServerSmart(config, serverInfo, builds); - +function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds: BuildRecord[] = []): BuildRequest { + return forgeDefaultsFromServerSmart(config, serverInfo, builds); } -const FORGE_PROGRESS_CAP = 94; - -const FORGE_STAGES: { label: string; pct: number; minMs: number }[] = [ - - { label: 'Resolving dependencies...', pct: 6, minMs: 0 }, - - { label: 'Compiling agent source...', pct: 18, minMs: 20000 }, - - { label: 'Cross-compiling targets...', pct: 36, minMs: 90000 }, - - { label: 'Applying obfuscation...', pct: 52, minMs: 240000 }, - - { label: 'Packaging deliverable...', pct: 68, minMs: 420000 }, - - { label: 'Signing & finalizing...', pct: 82, minMs: 600000 }, - - { label: 'Still forging (may take a while)...', pct: FORGE_PROGRESS_CAP, minMs: 900000 }, - +const FORGE_PROGRESS_CAP = 94; + +const FORGE_STAGES: { label: string; pct: number; minMs: number }[] = [ + { label: 'Resolving dependencies...', pct: 6, minMs: 0 }, + { label: 'Compiling agent source...', pct: 18, minMs: 20000 }, + { label: 'Cross-compiling targets...', pct: 36, minMs: 90000 }, + { label: 'Applying obfuscation...', pct: 52, minMs: 240000 }, + { label: 'Packaging deliverable...', pct: 68, minMs: 420000 }, + { label: 'Signing & finalizing...', pct: 82, minMs: 600000 }, + { label: 'Still forging (may take a while)...', pct: FORGE_PROGRESS_CAP, minMs: 900000 }, ]; -function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) { - - if (!building) return null; - - return ( - -
- -
- - - - {stage || 'Initializing...'} - - {Math.round(progress)}% - -
- -
- -
- -
- -
- -
- - ); - +function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) { + if (!building) return null; + return ( +
+
+ + {stage || 'Initializing...'} + {Math.round(progress)}% +
+
+
+
+
+
+ ); } -function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' | 'amber' | 'gold' { - - if (chip === 'ghost') return 'cyan'; - - if (chip === 'loud') return 'magenta'; - - return 'amber'; - +function previewAccentForChip(chip: MissionOperationChip): 'cyan' | 'magenta' | 'amber' | 'gold' { + if (chip === 'ghost') return 'cyan'; + if (chip === 'loud') return 'magenta'; + return 'amber'; } -export default function MissionDeckPage() { - - const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge(); - - const forgeStageTimerRef = useRef | null>(null); - +export default function MissionDeckPage() { + + const { startForge, endForge, setStage, stage: forgeStage, progress: forgeProgress } = useForge(); + + const forgeStageTimerRef = useRef | null>(null); + const cancelTokenRef = useRef(''); - const [form, setForm] = useState(null); - - const [loadingDefaults, setLoadingDefaults] = useState(true); - - const [error, setError] = useState(''); - - const [serverInfo, setServerInfo] = useState(null); - - const [listenPort, setListenPort] = useState(8989); - - const [operationChip, setOperationChip] = useState(() => - - missionChipForMode(loadStoredOperationMode()), - - ); - - const [operationMode, setOperationMode] = useState(loadStoredOperationMode); - - const [spreadProfile, setSpreadProfile] = useState(''); - - const [missionCampaign, setMissionCampaign] = useState('mission-deck'); - - const [missionStep, setMissionStep] = useState(null); - - const [missionBusy, setMissionBusy] = useState(false); - - const [missionExportSkipped, setMissionExportSkipped] = useState(false); - - const [missionComplete, setMissionComplete] = useState(false); - - const [building, setBuilding] = useState(false); - - const [dispenseReveal, setDispenseReveal] = useState(null); - - const [equipFlash, setEquipFlash] = useState(false); - - useModalAmbientDuck(missionComplete); + const [form, setForm] = useState(null); - const selectedChipDef = MISSION_OPERATION_CHIPS.find((c) => c.id === operationChip) ?? MISSION_OPERATION_CHIPS[0]; - - const selectedProfileDef = spreadProfile ? SPREAD_PROFILES.find((p) => p.id === spreadProfile) : null; + const [loadingDefaults, setLoadingDefaults] = useState(true); - const skinClass = [ - - 'page fade-in command-deck mission-deck operator-deck-page', - - forgeSkinClassName(resolveForgeSkin(operationMode, loadStoredForgeTheme())), - - missionBusy ? 'mission-deck--equipping' : '', - - equipFlash ? 'mission-deck--equip-flash' : '', - - ] - - .filter(Boolean) - - .join(' '); + const [error, setError] = useState(''); - useEffect(() => { - - Promise.all([ - - api.getConfig(), - - api.getServerInfo().catch(() => null), - - api.listBuilds().catch(() => []), - - ]) - - .then(([config, info, builds]) => { - - if (info) { - - setServerInfo(info); - - setListenPort(config.port || info.port || 8989); - - } else { - - setListenPort(config.port || 8989); - - } - - const candidates = info ? lanEndpointCandidates(info, config.port || info?.port) : []; - - const buildList = builds as BuildRecord[]; - - const base = defaultsFromConfig( - - config, - - info ?? { port: config.port || 8989, host: '', local_ips: [], suggested_url: '', dashboard_url: '', websocket_url: '' }, - - buildList, - - ); - - const withDefaults = applySmartForgeDefaults(base, { builds: buildList, endpointCandidates: candidates }); - - const storedMode = loadStoredOperationMode(); - - setForm(applyOperationMode(withDefaults, storedMode)); - - setOperationChip(missionChipForMode(storedMode)); - - }) - - .catch(() => setError('Failed to load server config — is the control server running?')) - - .finally(() => setLoadingDefaults(false)); - - }, []); + const [serverInfo, setServerInfo] = useState(null); - useEffect(() => { - - if (!building) { - - endForge(); - - if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current); - - return; - - } - - const startMs = Date.now(); - - startForge(); - - let stageIdx = 0; - - const advance = () => { - - const elapsed = Date.now() - startMs; - - 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]; - - const nextPct = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].pct : FORGE_PROGRESS_CAP; - - const nextMs = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].minMs : 1200000; - - 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(FORGE_PROGRESS_CAP, pct)); - - forgeStageTimerRef.current = setTimeout(advance, 250); - - }; - - advance(); - - return () => { - - if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current); - - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - - }, [building]); + const [listenPort, setListenPort] = useState(8989); - useEffect(() => { - - return () => { - - const tok = cancelTokenRef.current; - - if (tok) api.cancelBuild(tok).catch(() => {}); - - }; - - }, []); - - const endpointCandidates = useMemo( - - () => (serverInfo ? lanEndpointCandidates(serverInfo, listenPort) : []), - - [serverInfo, listenPort], - + const [operationChip, setOperationChip] = useState(() => + missionChipForMode(loadStoredOperationMode()), ); - const preflightChecks = form ? runForgePreflight(form, false) : []; - - const canLaunch = form ? !preflightHasErrors(preflightChecks) : false; + const [operationMode, setOperationMode] = useState(loadStoredOperationMode); - const updateField = (key: keyof BuildRequest, value: unknown) => { - - setForm((prev) => (prev ? applyForgeFieldUpdate(prev, key, value) : prev)); - - }; + const [spreadProfile, setSpreadProfile] = useState(''); - const selectOperationChip = (chip: MissionOperationChip) => { - - const modeId = operationModeForChip(chip); - - setOperationChip(chip); - - setOperationMode(modeId); - - storeOperationMode(modeId); - - setForm((prev) => (prev ? applyOperationMode(prev, modeId) : prev)); - - }; + const [missionCampaign, setMissionCampaign] = useState('mission-deck'); - const selectSpreadProfile = (id: SpreadProfileId | '') => { - - setSpreadProfile(id); - - if (id) { - - setForm((prev) => (prev ? applySpreadProfile(prev, id) : prev)); - - } - - }; + const [missionStep, setMissionStep] = useState(null); - const finishForgeSuccess = async (result: BuildResponse) => { - - setStage('Build complete!', 100); - - setDispenseReveal(result); - - }; + const [missionBusy, setMissionBusy] = useState(false); - const handleKillBuild = useCallback(async () => { - - const tok = cancelTokenRef.current; - - if (tok) { - - try { - - await api.cancelBuild(tok); - - } catch { - - /* ignore */ - - } - - } - - setBuilding(false); - + const [missionExportSkipped, setMissionExportSkipped] = useState(false); + + const [missionComplete, setMissionComplete] = useState(false); + + const [building, setBuilding] = useState(false); + + const [dispenseReveal, setDispenseReveal] = useState(null); + + const [equipFlash, setEquipFlash] = useState(false); + useModalAmbientDuck(missionComplete); + + const selectedChipDef = MISSION_OPERATION_CHIPS.find((c) => c.id === operationChip) ?? MISSION_OPERATION_CHIPS[0]; + + const selectedProfileDef = spreadProfile ? SPREAD_PROFILES.find((p) => p.id === spreadProfile) : null; + + const skinClass = [ + 'page fade-in command-deck mission-deck operator-deck-page', + forgeSkinClassName(resolveForgeSkin(operationMode, loadStoredForgeTheme())), + missionBusy ? 'mission-deck--equipping' : '', + equipFlash ? 'mission-deck--equip-flash' : '', + ] + .filter(Boolean) + .join(' '); + useEffect(() => { + Promise.all([ + api.getConfig(), + api.getServerInfo().catch(() => null), + api.listBuilds().catch(() => []), + ]) + .then(([config, info, builds]) => { + if (info) { + setServerInfo(info); + setListenPort(config.port || info.port || 8989); + } else { + setListenPort(config.port || 8989); + } + + const candidates = info ? lanEndpointCandidates(info, config.port || info?.port) : []; + + const buildList = builds as BuildRecord[]; + + const base = defaultsFromConfig( + config, + info ?? { port: config.port || 8989, host: '', local_ips: [], suggested_url: '', dashboard_url: '', websocket_url: '' }, + buildList, + ); + + const withDefaults = applySmartForgeDefaults(base, { builds: buildList, endpointCandidates: candidates }); + + const storedMode = loadStoredOperationMode(); + setForm(applyOperationMode(withDefaults, storedMode)); + setOperationChip(missionChipForMode(storedMode)); + }) + .catch(() => setError('Failed to load server config — is the control server running?')) + .finally(() => setLoadingDefaults(false)); }, []); - - const handleEquipAndStrike = async () => { - - if (!form) return; - - setError(''); - - setMissionComplete(false); - - setMissionExportSkipped(false); - - setEquipFlash(true); - - setTimeout(() => setEquipFlash(false), 900); - - const normalized = applyMissionPresets(form, operationMode, spreadProfile); - - setForm(normalized); - - const checks = runForgePreflight(normalized, false); - - if (preflightHasErrors(checks)) { - - setError('Blocked — fix wallet or control URL errors before forging.'); - - return; - + useEffect(() => { + if (!building) { + endForge(); + if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current); + return; } - const serverBase = (normalized.server_url || serverInfo?.suggested_url || window.location.origin).replace(/\/$/, ''); - - const cancelToken = crypto.randomUUID(); - - cancelTokenRef.current = cancelToken; - - setMissionBusy(true); - - setMissionStep('configure'); - - setBuilding(true); + const startMs = Date.now(); + startForge(); + let stageIdx = 0; - try { - - const result = await runForgeMission({ - - form: normalized, - - operationMode, - - spreadProfile, - - campaign: missionCampaign, - - serverBase, - - api, - - cancelToken, - - onStep: (step) => { - - setMissionStep(step); - - if (step === 'forge') startForge(); - - }, - - }); - - setMissionExportSkipped(result.exportSkipped); - - setMissionComplete(true); - - await finishForgeSuccess(result.build); - - } catch (err: unknown) { - - const msg = err instanceof Error ? err.message : 'Mission failed'; - - if (msg !== 'build cancelled') { - - setMissionStep('error'); - - setError(msg); - - } - - } finally { - - cancelTokenRef.current = ''; - - setMissionBusy(false); - - setBuilding(false); - - } - + const advance = () => { + + const elapsed = Date.now() - startMs; + 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]; + + const nextPct = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].pct : FORGE_PROGRESS_CAP; + + const nextMs = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].minMs : 1200000; + + 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(FORGE_PROGRESS_CAP, pct)); + forgeStageTimerRef.current = setTimeout(advance, 250); + }; + advance(); + return () => { + if (forgeStageTimerRef.current) clearTimeout(forgeStageTimerRef.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [building]); + useEffect(() => { + return () => { + + const tok = cancelTokenRef.current; + if (tok) api.cancelBuild(tok).catch(() => {}); + }; + }, []); + + const endpointCandidates = useMemo( + () => (serverInfo ? lanEndpointCandidates(serverInfo, listenPort) : []), + [serverInfo, listenPort], + ); + + const preflightChecks = form ? runForgePreflight(form, false) : []; + + const canLaunch = form ? !preflightHasErrors(preflightChecks) : false; + + const updateField = (key: keyof BuildRequest, value: unknown) => { + setForm((prev) => (prev ? applyForgeFieldUpdate(prev, key, value) : prev)); }; - if (loadingDefaults) { - - return ( - -
- -
- -
- -

FAST PATH

- -

Mission Deck

- -
- -
- -

Loading loadout defaults…

- -
- - ); - - } + const selectOperationChip = (chip: MissionOperationChip) => { - if (!form) { - - return ( - -
- -
- -
- -

FAST PATH

- -

Mission Deck

- -
- -
- -

{error || 'Failed to load loadout defaults.'}

- -
- - ); - - } + const modeId = operationModeForChip(chip); + setOperationChip(chip); + setOperationMode(modeId); + storeOperationMode(modeId); + setForm((prev) => (prev ? applyOperationMode(prev, modeId) : prev)); + }; - return ( - -
- -
- -
- -

FAST PATH

- -

- Mission Deck -

- -

- - Pick a preset loadout and forge once — without the full Forge form. Install commands are on Builds when you are done. - -

- -

- - Forge - - {' — every build option · '} - - Emberwake - - {' — track campaigns · '} - - Builds - - {' — pinned install commands · '} - - - - Field guide - - - -

- -
- -
- - - - Full Forge - - - - - - Emberwake - - - - - - Builds - - - -
- -
+ const selectSpreadProfile = (id: SpreadProfileId | '') => { + setSpreadProfile(id); + if (id) { + setForm((prev) => (prev ? applySpreadProfile(prev, id) : prev)); + } + }; - + const finishForgeSuccess = async (result: BuildResponse) => { + setStage('Build complete!', 100); + setDispenseReveal(result); + }; -
- - + const handleKillBuild = useCallback(async () => { -
- - - -
- -
- -
- - - - {operationChip === 'ghost' ? '◈' : operationChip === 'loud' ? '✦' : '⬡'} - - - -
- -
- -
- -
- -

2 — Your loadout

- -

{selectedChipDef.label}

- -

{selectedChipDef.blurb}

- -
- -
- -
Spread profile
- -
{selectedProfileDef?.label ?? 'None'}
- -
- -
- -
Campaign
- -
{missionCampaign || '—'}
- -
- -
- -
Worker
- -
{form.worker_name || '—'}
- -
- -
- -
Deliverable
- -
{form.spread_kit ? 'Spread Kit' : form.fusion_enabled ? 'Fusion' : 'Single platform'}
- -
- -
- -
- - + const tok = cancelTokenRef.current; + if (tok) { + try { + await api.cancelBuild(tok); + } catch { + /* ignore */ -
- - - -

- - One click: forge and export a spread kit when your profile needs it.{' '} - - - -

- - {!canLaunch && !missionBusy && ( - -

Fix preflight errors in your loadout before equipping.

- - )} - - {error &&

{error}

} - + } + } + setBuilding(false); + }, []); + + const handleEquipAndStrike = async () => { + if (!form) return; + setError(''); + setMissionComplete(false); + setMissionExportSkipped(false); + setEquipFlash(true); + setTimeout(() => setEquipFlash(false), 900); + + const normalized = applyMissionPresets(form, operationMode, spreadProfile); + setForm(normalized); + + const checks = runForgePreflight(normalized, false); + if (preflightHasErrors(checks)) { + setError('Blocked — fix wallet or control URL errors before forging.'); + return; + } + + const serverBase = (normalized.server_url || serverInfo?.suggested_url || window.location.origin).replace(/\/$/, ''); + + const cancelToken = crypto.randomUUID(); + cancelTokenRef.current = cancelToken; + setMissionBusy(true); + setMissionStep('configure'); + setBuilding(true); + try { + + const result = await runForgeMission({ + form: normalized, + operationMode, + spreadProfile, + campaign: missionCampaign, + serverBase, + api, + cancelToken, + onStep: (step) => { + setMissionStep(step); + if (step === 'forge') startForge(); + }, + }); + setMissionExportSkipped(result.exportSkipped); + setMissionComplete(true); + await finishForgeSuccess(result.build); + } catch (err: unknown) { + + const msg = err instanceof Error ? err.message : 'Mission failed'; + if (msg !== 'build cancelled') { + setMissionStep('error'); + setError(msg); + } + } finally { + cancelTokenRef.current = ''; + setMissionBusy(false); + setBuilding(false); + } + }; + if (loadingDefaults) { + return ( +
+
+
+

FAST PATH

+

Mission Deck

- - {(missionBusy || missionStep) && ( - - - -

- - Forge run - -

- - {missionBusy && ( - - - - )} - - - -
- -

PIPELINE STEPS

- -
- - {MISSION_STEPS.map((step) => { - - const status = missionStep - - ? missionStepStatus(step, missionStep, missionExportSkipped) - - : 'pending'; - - return ( - - - - {status === 'done' ? '✓' : status === 'active' ? '●' : status === 'skipped' ? '—' : status === 'error' ? '✕' : '○'} - - {' '} - - {MISSION_STEP_LABELS[step]} - - {step === 'export' && missionExportSkipped ? ' (n/a)' : ''} - - - - ); - - })} - -
- -
- -
- - )} - -
- - - + +

Loading loadout defaults…

+ ); + } + if (!form) { + return ( +
+
+
+

FAST PATH

+

Mission Deck

+
+
+

{error || 'Failed to load loadout defaults.'}

+
+ ); + } + return ( +
+
+
+

FAST PATH

+

+ Mission Deck +

+

+ Pick a preset loadout and forge once — without the full Forge form. Install commands are on Builds when you are done. +

+

+ Forge + {' — every build option · '} + Emberwake + {' — track campaigns · '} + Builds + {' — pinned install commands · '} + + Field guide + +

+
+
+ + Full Forge + + + Emberwake + + + Builds + +
+
+ +
+ +
+ +
+
+
+ + {operationChip === 'ghost' ? '◈' : operationChip === 'loud' ? '✦' : '⬡'} + +
+
+
+
+

2 — Your loadout

+

{selectedChipDef.label}

+

{selectedChipDef.blurb}

+
+
+
Spread profile
+
{selectedProfileDef?.label ?? 'None'}
+
+
+
Campaign
+
{missionCampaign || '—'}
+
+
+
Worker
+
{form.worker_name || '—'}
+
+
+
Deliverable
+
{form.spread_kit ? 'Spread Kit' : form.fusion_enabled ? 'Fusion' : 'Single platform'}
+
+
+
+ +
+ +

+ One click: forge and export a spread kit when your profile needs it.{' '} + +

+ {!canLaunch && !missionBusy && ( +

Fix preflight errors in your loadout before equipping.

+ )} + {error &&

{error}

} +
+ {(missionBusy || missionStep) && ( + +

+ Forge run +

+ {missionBusy && ( + + )} + +
+

PIPELINE STEPS

+
+ {MISSION_STEPS.map((step) => { - {dispenseReveal && ( - - setDispenseReveal(null)} /> - + const status = missionStep + ? missionStepStatus(step, missionStep, missionExportSkipped) + : 'pending'; + return ( + + {status === 'done' ? '✓' : status === 'active' ? '●' : status === 'skipped' ? '—' : status === 'error' ? '✕' : '○'} + {' '} + {MISSION_STEP_LABELS[step]} + {step === 'export' && missionExportSkipped ? ' (n/a)' : ''} + + ); + })} +
+
+
+ )} +
+ +
+ {dispenseReveal && ( + setDispenseReveal(null)} /> )} + {missionComplete && ( +
setMissionComplete(false)} + > +
e.stopPropagation()}> +

Forge complete

+

+ Your build is ready. Copy per-machine install commands from{' '} + setMissionComplete(false)}>Builds + ; track campaign hits in{' '} + setMissionComplete(false)}>Emberwake. +

+
+ setMissionComplete(false)}> + View install commands in Builds → + + setMissionComplete(false)}> + Emberwake + + +
+
+
+ )} +
+ ); +} - {missionComplete && ( - -
setMissionComplete(false)} - - > - -
e.stopPropagation()}> - -

Forge complete

- -

- - Your build is ready. Copy per-machine install commands from{' '} - - setMissionComplete(false)}>Builds - - ; track campaign hits in{' '} - - setMissionComplete(false)}>Emberwake. - -

- -
- - setMissionComplete(false)}> - - View install commands in Builds → - - - - setMissionComplete(false)}> - - Emberwake - - - - - -
- -
- -
- - )} - -
- - ); - -} - \ No newline at end of file