Add multi-server blueprint collections and atomic launches

This commit is contained in:
2026-09-18 00:08:54 -04:00
parent 782c7c0bda
commit 989931a0f2
20 changed files with 868 additions and 50 deletions
@@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import Button from './primitives/Button.svelte';
import LaunchRecipe from './LaunchRecipe.svelte';
import DeployCollectionDialog from './DeployCollectionDialog.svelte';
import type { BlueprintSummary, LaunchPreview } from '$lib/deployment-plan.js';
import type { ZombieMapEntry, ZombieGame } from '$lib/map-aliases.js';
@@ -13,6 +14,7 @@
let catalogs = $state<Record<ZombieGame, ZombieMapEntry[]>>({ t5: [], t6: [] });
let blueprints = $state<BlueprintSummary[]>([]);
let blueprintId = $state('');
let collectionOpen = $state(false);
let maps = $derived(catalogs[game]);
let selectedBlueprint = $derived(blueprints.find(b => b.id === blueprintId));
let preview = $state<LaunchPreview | null>(null);
@@ -54,6 +56,9 @@
async function review() {
if (busy) return;
if (!sourceDeploymentId && selectedBlueprint && selectedBlueprint.totalServers > 1) {
open = false; collectionOpen = true; return;
}
const current = session;
busy = true; error = ''; plutoKey = ''; preview = null; savedName = '';
try {
@@ -125,8 +130,8 @@
<p class="muted">{busy ? 'Preparing the original launch recipe…' : 'Review the original recipe with a fresh lobby and port.'}</p><Button disabled={busy} onclick={review}>Review launch</Button>
{:else}
<form onsubmit={event => { event.preventDefault(); void review(); }}>
<label>Start with<select bind:value={blueprintId} disabled={busy}><option value="">Current promoted setup</option>{#each blueprints as blueprint}<option value={blueprint.id}>{blueprint.name} · {blueprint.recipe.mapName}</option>{/each}</select></label>
{#if selectedBlueprint}<section class="recipe-panel"><LaunchRecipe recipe={selectedBlueprint.recipe} />{#if selectedBlueprint.description}<p class="muted">{selectedBlueprint.description}</p>{/if}</section>
<label>Start with<select bind:value={blueprintId} disabled={busy}><option value="">Current promoted setup</option>{#each blueprints as blueprint}<option value={blueprint.id}>{blueprint.name} · {blueprint.totalServers} server{blueprint.totalServers === 1 ? '' : 's'}</option>{/each}</select></label>
{#if selectedBlueprint}<section class="recipe-panel">{#if selectedBlueprint.totalServers > 1}<h3>{selectedBlueprint.totalServers} servers in this blueprint</h3>{#each selectedBlueprint.entries as entry}<p>{entry.count}× {entry.recipe.mapName} · {entry.recipe.game.toUpperCase()}</p>{/each}<p class="muted">Continue to review all servers and their keys together.</p>{:else}<LaunchRecipe recipe={selectedBlueprint.recipe} />{/if}{#if selectedBlueprint.description}<p class="muted">{selectedBlueprint.description}</p>{/if}</section>
{:else}<p class="muted">Pick a map. We’ll gather its promoted scripts, plugins, and configuration into a recipe you can review and save.</p><div class="fields"><label>Game<select bind:value={game} disabled={busy} onchange={event => mapSlug = catalogs[event.currentTarget.value as ZombieGame][0]?.slug ?? ''}><option value="t5">Black Ops I (T5)</option><option value="t6">Black Ops II (T6)</option></select></label><label>Map<select bind:value={mapSlug} disabled={busy}>{#each maps as map}<option value={map.slug}>{map.friendly}</option>{/each}</select></label></div>{/if}
<div class="promise"><strong>Know what you’re launching.</strong><p>Review the exact managed files, available lobby, and launch prerequisites before creating your server.</p></div>
<div class="launch-actions"><Button variant="ghost" disabled={busy} onclick={() => open = false}>Cancel</Button><Button type="submit" variant="primary" loading={busy} disabled={busy || (!blueprintId && !mapSlug)}>Preview launch →</Button></div>
@@ -134,6 +139,7 @@
{/if}
</div>
</dialog>
<DeployCollectionDialog bind:open={collectionOpen} {blueprintId} />
<style>
dialog { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: 18px; padding: 0; width: min(700px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }