From a7044f21a1a708c9700be7e6dc0ad313478fb117 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Sun, 27 Sep 2026 14:50:55 -0400 Subject: [PATCH] Rename blueprints to Collections and add Operations tab Fixes #46 --- README.md | 18 ++-- docs/deployment-blueprints.md | 62 ++++++------ src/lib/server/fleet/collections.ts | 10 +- src/lib/server/fleet/launch-recipes.ts | 12 +-- src/lib/ui/components/BlueprintBuilder.svelte | 16 +-- .../components/DeployCollectionDialog.svelte | 12 +-- .../ui/components/DeployServerDialog.svelte | 18 ++-- src/lib/ui/components/HousePartyDialog.svelte | 2 +- src/routes/+layout.svelte | 4 +- src/routes/+page.svelte | 2 +- src/routes/api/blueprints/+server.ts | 2 +- src/routes/api/blueprints/[id]/+server.ts | 4 +- src/routes/api/deployments/preview/+server.ts | 2 +- src/routes/collections/+page.svelte | 97 +++++++++++++++++++ src/routes/provisioning/+page.svelte | 83 +--------------- src/routes/provisioning/+page.ts | 11 +++ .../collections/[id]/+page.svelte | 2 +- 17 files changed, 198 insertions(+), 159 deletions(-) create mode 100644 src/routes/collections/+page.svelte create mode 100644 src/routes/provisioning/+page.ts diff --git a/README.md b/README.md index d80aa77..382a9d7 100644 --- a/README.md +++ b/README.md @@ -131,13 +131,13 @@ RCON, and selected-map verification. No token, no session, no auth freeze. If Komodo or RCON aren't configured, the sweep marks pending rows `manual` so nothing hangs forever. -## Provisioning blueprints and launch previews +## Provisioning collections and launch previews Provisioning also offers **House Party Protocol**: review the entire fleet and queue destruction in one action. This includes queued creations and reuses pending destruction requests. Each server waits for a fresh zero-player reading; occupied or unreachable servers remain queued. Containers, server files, and registrations -are removed, while saved keys and blueprints are retained. The preview must be +are removed, while saved keys and collections are retained. The preview must be reviewed again if fleet membership changes before confirmation. Servers created after confirmation are not included. Progress appears in deployment history. @@ -146,13 +146,13 @@ If deletion fails after Komodo removal, the operation detail page offers repeating Komodo deletion. Requesting destruction again also resumes that cleanup. Recovery is blocked if a newer operation exists for the same server name. -Provisioning includes a blueprint builder for whole server collections: choose +**Operations → Collections** includes a builder for whole server lineups: choose multiple T5/T6 maps and set the desired total lobbies for each. **Conform fleet** counts existing and queued servers and creates only the shortfall. The preview shows target/existing/missing counts and IDs, ports, and key requirements for new servers only. Already-satisfied targets create nothing; excess servers are kept. -Choose **Match blueprint exactly** in the preview to also remove excess lobbies -and maps outside the blueprint. Review the keep/create/remove lists and confirm +Choose **Match collection exactly** in the preview to also remove excess lobbies +and maps outside the collection. Review the keep/create/remove lists and confirm removals. Matching servers stay untouched; unstarted excess creations are cancelled, and running servers wait for a fresh zero-player check before removal. New lobbies can reserve ports from these removals and wait until they are released. The collection @@ -167,16 +167,16 @@ shows the startup limit, and its progress page shows active startups and each server's place in the shared queue. Set `DEPLOYMENT_CREATE_CONCURRENCY` to a positive integer to increase the limit. Each worker pass starts at most the available number of slots; immediate failures do not drain the remaining queue. -Existing single-server blueprints can be expanded with **Edit**. Single-server +Existing single-server collections can be expanded with **Edit**. Single-server launches and launching again from recorded recipes remain available. -Blueprints save maps and target lobby counts. Every new server uses the latest +Collections save maps and target lobby counts. Every new server uses the latest promoted scripts, plugins, and configuration when provisioning begins, including -promotions made after preview or while queued. Existing blueprints automatically +promotions made after preview or while queued. Existing collections automatically follow this policy. Deployment history records the files selected for that launch; **Launch again** uses the current promoted setup for the same game and map. The progress page explains waiting conditions, shows elapsed time, and can resume health verification of an interrupted creation when its original server remains. -See [Deployment blueprints](docs/deployment-blueprints.md) for the workflow, +See [Deployment collections](docs/deployment-blueprints.md) for the workflow, promotion policy, and API details. ## Saved server keys diff --git a/docs/deployment-blueprints.md b/docs/deployment-blueprints.md index 754b7d5..c87a054 100644 --- a/docs/deployment-blueprints.md +++ b/docs/deployment-blueprints.md @@ -1,14 +1,14 @@ -# Deployment blueprints +# Deployment collections -The Provisioning page is the starting point for reusable server collections. A -blueprint describes a whole lineup: for example, two Kino lobbies, one Moon lobby, +Open **Operations → Collections** to manage reusable server lineups. A +collection describes a whole lineup: for example, two Kino lobbies, one Moon lobby, and two Town lobbies. Counts are desired fleet totals: existing and queued servers count toward each target, and only missing servers are created. -Blueprints save game/map selections and lobby counts. Every new server uses the +Collections save game/map selections and lobby counts. Every new server uses the latest promoted scripts, plugins, baseline, and matching configuration overlays when provisioning begins. -For example, a blueprint targeting four Kino and two Der Riese lobbies, with +For example, a collection targeting four Kino and two Der Riese lobbies, with two Kino already registered, creates two Kino and two Der Riese lobbies. The preview shows **Target / Existing or queued / To create** for each map. Matching is by game and exact map variant; T6 Town and Farm are distinct even @@ -18,15 +18,15 @@ they appear in both the server registry and active creation queue. Registered servers count even if stopped or unhealthy; conforming does not restart or reconfigure them. Failed creation jobs without a registered server, destroyed records, and servers awaiting removal do not count. The default -**Fill missing servers** mode keeps excess servers and maps outside the blueprint. +**Fill missing servers** mode keeps excess servers and maps outside the collection. It fills deficits without changing existing servers' files. ## Build and launch a collection -1. Choose **Create blueprint** on Provisioning and name the lineup. +1. Choose **Create collection** on **Operations → Collections** and name the lineup. 2. Add maps from Black Ops I or II and set the desired total lobbies for each. -3. Choose **Save & preview** (or **Preview fleet changes** on a saved blueprint). - Choose **Fill missing servers** or **Match blueprint exactly**, then review +3. Choose **Save & preview** (or **Preview fleet changes** on a saved collection). + Choose **Fill missing servers** or **Match collection exactly**, then review the server IDs and game ports. 4. Lobbies with a saved key show **Saved key ready** and reuse it automatically. Enter a Plutonium key only for lobbies missing one. Update saved keys in **Keys**. @@ -34,12 +34,12 @@ It fills deficits without changing existing servers' files. mode) and follow the collection progress page. If every target is already met, choose **Done**; no operation is created. -### Match blueprint exactly +### Match collection exactly Exact mode applies to the entire registered and queued fleet across both games. The preview lists servers to keep, create, and remove. It keeps deployed lobbies before unstarted creations, then the lowest lobby numbers within each game/map -variant. It removes excess lobbies and maps outside the blueprint. Retained +variant. It removes excess lobbies and maps outside the collection. Retained servers keep their containers, files, ports, and configuration without a restart. The option is chosen per launch; opening another preview defaults to fill mode. @@ -64,17 +64,17 @@ tracks creations and removals, including removal-only plans, and lists the serve that were retained. The existing operation detail page offers removal cancellation and cleanup recovery where applicable. -Blueprints support up to 40 distinct game/map selections, 1–20 lobbies per map, +Collections support up to 40 distinct game/map selections, 1–20 lobbies per map, and 100 servers total. The host's configured port range and existing fleet may -allow fewer. A blueprint can be saved even when there is insufficient capacity -to launch it. Saving a blueprint does not capture or pin managed file versions. +allow fewer. A collection can be saved even when there is insufficient capacity +to launch it. Saving a collection does not capture or pin managed file versions. -Use **Edit** on a blueprint card to change its name, description, maps, or lobby +Use **Edit** on a collection card to change its name, description, maps, or lobby counts. Already reviewed launches keep their original lineup, including after -blueprint edits or removal; their managed files still follow the latest promotions. -Previously saved blueprints automatically use this policy without being resaved. -Blueprints saved by the earlier single-server release load as one-server lineups -and can be expanded directly. Any file snapshots in older blueprints are ignored +collection edits or removal; their managed files still follow the latest promotions. +Previously saved collections automatically use this policy without being resaved. +Collections saved by the earlier single-server release load as one-server lineups +and can be expanded directly. Any file snapshots in older collections are ignored when preparing new launches. A collection preview does not reserve capacity. At submission, all lobby IDs, @@ -85,7 +85,7 @@ the preview retains typed keys only for unchanged server IDs still missing a sav key, and resets removal confirmation. Submitting the same preview again returns the same collection, including after -a lost response or an application restart. Reusing the blueprint with a new +a lost response or an application restart. Reusing the collection with a new preview recomputes the shortfall, including servers queued by earlier launches. It does not add another full copy of the lineup. If the shortfall or assigned capacity changes before submission, the request is rejected without changes and @@ -114,7 +114,7 @@ operation links back to its collection. ## Create and review a launch 1. Choose **Single server** on Provisioning (or the deploy action on the fleet). -2. Choose the current promoted setup and select a game/map, or select a blueprint. +2. Choose the current promoted setup and select a game/map, or select a collection. 3. Choose **Preview launch**. Review the image reference, configuration sources, managed files and versions, selected map, lobby, port, and prerequisite checks. 4. Enter the Plutonium key for the displayed server ID, or reuse its saved key. @@ -132,24 +132,24 @@ rules, or credentials. Container and RCON verification happen after provisioning T6 variants retain their exact catalog slug and launch ruleset; RCON health checks verify the engine map, not the variant's start location. -## Save and reuse a blueprint +## Save and reuse a collection -Expand **Make this a reusable blueprint** in a launch preview and give it a name +Expand **Make this a reusable collection** in a launch preview and give it a name and optional description. You can save the map even when host configuration or available capacity blocks launching. Names are unique without regard to case. -Each map in a blueprint retains the game, exact map variant, and desired lobby +Each map in a collection retains the game, exact map variant, and desired lobby count. Image references, managed files, and composed configuration are resolved from the current setup for each new launch. Credentials, lobby IDs, and ports are not copied from the source server. -Use **Conform fleet** on a blueprint card to review missing servers. Remove a -blueprint from the library when it is no longer needed; its existing servers, +Use **Conform fleet** on a collection card to review missing servers. Remove a +collection from the library when it is no longer needed; its existing servers, reviewed previews, and deployment history remain intact. On a creation detail page, **Launch again** selects the original game and map with the next available lobby and port, using the latest promoted setup. The -preview also lets you save that map as a blueprint with a target of one lobby. +preview also lets you save that map as a collection with a target of one lobby. The recorded files remain visible as deployment history. Older deployments without a recorded recipe retain their history but cannot offer this action. @@ -159,7 +159,7 @@ A preview shows the currently promoted managed files, configuration, and image reference for the selected map. It does not pin those versions. New launches resolve the promoted setup again when submitted and when provisioning begins, so promotions made after a preview or while a job is queued apply to that server. -This applies to blueprint collections, single-server launches, and **Launch again**. +This applies to collection collections, single-server launches, and **Launch again**. The files selected at provisioning, their hashes and version metadata, composed configuration, selected map, and image reference are recorded in SQLite for @@ -201,6 +201,10 @@ repeats container creation or deployment as part of this recovery. ## API +Collections retain the existing `/api/blueprints` endpoints and `blueprintId` / +`blueprintName` fields for compatibility. Saved data needs no migration. Old +`/provisioning?view=blueprints` links redirect to `/collections`. + - `GET /api/blueprints`: list lineup summaries with game/map selections and counts. - `POST /api/blueprints`: save `{ name, description?, previewId }` or `{ name, description?, deploymentId }` for a single-map target of one lobby, or @@ -222,7 +226,7 @@ repeats container creation or deployment as part of this recovery. each waiting server's global `queuePosition`, and the shared queue's concurrency, active startups, and queued count. - `POST /api/deployments/preview`: preview `{ game, mapSlug }` or `{ deploymentId }` - for a new single server using the latest promoted setup. All blueprints, including one-server + for a new single server using the latest promoted setup. All collections, including one-server targets, must use the collection endpoint so existing servers count correctly. Returns an ID, expiry, recipe summary, capacity, key availability (never its value), and prerequisite checks. diff --git a/src/lib/server/fleet/collections.ts b/src/lib/server/fleet/collections.ts index 638e0d8..1b17be8 100644 --- a/src/lib/server/fleet/collections.ts +++ b/src/lib/server/fleet/collections.ts @@ -62,7 +62,7 @@ function collectionPlan(entries: BlueprintEntry[], mode: CollectionMode) { const retained = new Set(retainedServers); const matching = new Set(counts.flatMap(entry => entry.existingServers)); const removals = mode === 'exact' ? housePartyTargets().filter(name => !retained.has(name)).map(name => ({ name, - reason: matching.has(name) ? 'Exceeds the target lobby count' : 'Outside this blueprint or already awaiting removal' + reason: matching.has(name) ? 'Exceeds the target lobby count' : 'Outside this collection or already awaiting removal' })) : []; return { counts, retainedServers, removals }; } @@ -93,8 +93,8 @@ function blockersFor(servers: CollectionPreview['servers']): string[] { } export function previewCollection(blueprintId: unknown, mode: unknown = 'fill'): CollectionPreview { - if (typeof blueprintId !== 'string' || !blueprintId) throw new Error('Choose a blueprint to launch.'); - if (mode !== 'fill' && mode !== 'exact') throw new Error('Choose fill or exact blueprint mode.'); + if (typeof blueprintId !== 'string' || !blueprintId) throw new Error('Choose a collection to launch.'); + if (mode !== 'fill' && mode !== 'exact') throw new Error('Choose fill or exact collection mode.'); return db().transaction((): CollectionPreview => { const { entries, blueprintName } = blueprintEntries(blueprintId); const { counts, retainedServers, removals } = collectionPlan(entries, mode); @@ -156,8 +156,8 @@ export function queueCollection(input: { previewId?: unknown; keys?: unknown }): const result = destroyServer(server.name, 'blueprint-exact'); if (!result.ok || !result.deploymentId) throw new Error('Could not queue removal for ' + server.name); id = result.deploymentId; - db().prepare("UPDATE server_deployments SET state = 'cancelled', stage = 'Cancelled by exact blueprint', finished_at = ? WHERE id = ?").run(new Date().toISOString(), queued.id); - recordDeploymentEvent(queued.id, 'Queued creation cancelled by exact blueprint; saved key retained.'); + db().prepare("UPDATE server_deployments SET state = 'cancelled', stage = 'Cancelled by exact collection', finished_at = ? WHERE id = ?").run(new Date().toISOString(), queued.id); + recordDeploymentEvent(queued.id, 'Queued creation cancelled by exact collection; saved key retained.'); deploymentStage(id, 'Cancelled queued creation; no server was started'); finishDeployment(id, { ok: true }); } else { diff --git a/src/lib/server/fleet/launch-recipes.ts b/src/lib/server/fleet/launch-recipes.ts index c88d4e5..50f0f1e 100644 --- a/src/lib/server/fleet/launch-recipes.ts +++ b/src/lib/server/fleet/launch-recipes.ts @@ -43,7 +43,7 @@ function summarizeBlueprint(row: BlueprintRow): BlueprintSummary { export function blueprintEntries(id: string): { entries: BlueprintEntry[]; blueprintName: string } { const row = db().prepare('SELECT * FROM server_blueprints WHERE id = ?').get(id) as BlueprintRow | undefined; - if (!row) throw new Error('This blueprint is no longer available. Choose another starting point.'); + if (!row) throw new Error('This collection is no longer available. Choose another starting point.'); const entries = readEntries(row.recipe).map(({ count, game, mapSlug }) => ({ count, recipe: captureRecipe(game, mapSlug) })); return { entries, blueprintName: row.name }; } @@ -60,7 +60,7 @@ function buildEntries(input: unknown): BlueprintSelection[] { seen.add(key); return { count: entry.count as number, game: entry.game as ZombieGame, mapSlug: entry.mapSlug as string }; }); - if (entries.reduce((total, entry) => total + entry.count, 0) > 100) throw new Error('A blueprint can launch at most 100 servers.'); + if (entries.reduce((total, entry) => total + entry.count, 0) > 100) throw new Error('A collection can launch at most 100 servers.'); return entries; } @@ -103,7 +103,7 @@ export function recipeForDeployment(id: string): { recipe: LaunchRecipe; bluepri export function blueprintRecipe(id: string): { recipe: LaunchRecipe; blueprintName: string } { const { entries, blueprintName } = blueprintEntries(id); - if (entries.length !== 1 || entries[0].count !== 1) throw new Error('This blueprint launches multiple servers. Use the collection preview.'); + if (entries.length !== 1 || entries[0].count !== 1) throw new Error('This collection launches multiple servers. Use the collection preview.'); return { recipe: entries[0].recipe, blueprintName }; } @@ -112,9 +112,9 @@ export function listBlueprints(): BlueprintSummary[] { } function blueprintName(input: { name: unknown; description?: unknown }, exceptId = ''): string { - if (typeof input.name !== 'string' || !input.name.trim() || input.name.trim().length > 80) throw new Error('Give the blueprint a name of 1–80 characters.'); + if (typeof input.name !== 'string' || !input.name.trim() || input.name.trim().length > 80) throw new Error('Give the collection a name of 1–80 characters.'); if (input.description !== undefined && (typeof input.description !== 'string' || input.description.length > 500)) throw new Error('Keep the description to 500 characters.'); - if (db().prepare('SELECT id FROM server_blueprints WHERE name = ? COLLATE NOCASE AND id != ?').get(input.name.trim(), exceptId)) throw new Error('A blueprint with this name already exists. Choose a new name.'); + if (db().prepare('SELECT id FROM server_blueprints WHERE name = ? COLLATE NOCASE AND id != ?').get(input.name.trim(), exceptId)) throw new Error('A collection with this name already exists. Choose a new name.'); return input.name.trim(); } @@ -141,7 +141,7 @@ export function saveBlueprint(input: { name: unknown; description?: unknown; pre export function updateBlueprint(id: string, input: { name: unknown; description?: unknown; entries?: unknown }): BlueprintSummary { return db().transaction(() => { const row = db().prepare('SELECT * FROM server_blueprints WHERE id = ?').get(id) as BlueprintRow | undefined; - if (!row) throw new Error('Blueprint not found.'); + if (!row) throw new Error('Collection not found.'); const name = blueprintName(input, id); const entries = buildEntries(input.entries); const recipe = JSON.stringify({ format: 3, entries }); diff --git a/src/lib/ui/components/BlueprintBuilder.svelte b/src/lib/ui/components/BlueprintBuilder.svelte index a215b22..ce119e2 100644 --- a/src/lib/ui/components/BlueprintBuilder.svelte +++ b/src/lib/ui/components/BlueprintBuilder.svelte @@ -17,7 +17,7 @@ let total = $derived(rows.reduce((sum, row) => sum + (Number(row.count) || 0), 0)); const duplicate = (row: typeof rows[number]) => rows.some(other => other.key !== row.key && other.game === row.game && other.mapSlug === row.mapSlug); let valid = $derived(name.trim().length > 0 && rows.length > 0 && total <= 100 && rows.every(row => row.mapSlug && Number.isInteger(row.count) && row.count >= 1 && row.count <= 20 && !duplicate(row))); - let reason = $derived(!name.trim() ? 'Enter a blueprint name to save.' : total > 100 ? `Reduce the total by ${total - 100} to stay within 100 target lobbies.` : rows.some(duplicate) ? 'Choose each game and map once; increase its lobby count instead.' : !valid ? 'Choose a map and 1–20 target lobbies for every row.' : ''); + let reason = $derived(!name.trim() ? 'Enter a collection name to save.' : total > 100 ? `Reduce the total by ${total - 100} to stay within 100 target lobbies.` : rows.some(duplicate) ? 'Choose each game and map once; increase its lobby count instead.' : !valid ? 'Choose a map and 1–20 target lobbies for every row.' : ''); let canAdd = $derived((['t5', 't6'] as const).some(game => catalogs[game].some(map => !rows.some(row => row.game === game && row.mapSlug === map.slug)))); let wasOpen = false; let session = 0; @@ -60,22 +60,22 @@ method: blueprint ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, entries: rows.map(({ game, mapSlug, count }) => ({ game, mapSlug, count })) }) }); const body = await res.json(); - if (!res.ok) throw new Error(body.error ?? 'Could not save blueprint.'); + if (!res.ok) throw new Error(body.error ?? 'Could not save collection.'); open = false; onsaved(body.blueprint, launch); } catch (err) { error = (err as Error).message; } finally { busy = false; } } - { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}> + { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}>
-
One blueprint. Your whole lineup.

{blueprint ? 'Edit your blueprint' : 'Build a server collection'}

-

Choose the total lobbies you want for each map. Existing and queued servers count; only missing lobbies are created. Mix Black Ops I and II in the same blueprint.

+
One collection. Your whole lineup.

{blueprint ? 'Edit your collection' : 'Build a server collection'}

+

Choose the total lobbies you want for each map. Existing and queued servers count; only missing lobbies are created. Mix Black Ops I and II in the same collection.

{#if error}{/if} {#if !loaded}

Loading maps…

{#if error}{/if} {:else}
{ event.preventDefault(); void save(true); }}> -
+

Your lineup

100} role="status">{total} / 100 target lobbies
{#each rows as row, index (row.key)} @@ -89,9 +89,9 @@ {/each}
-

Blueprints save your maps and target lobby counts. Every new server uses the latest promoted scripts, plugins, and configuration. Before launch, you’ll review every new server ID, port, and key.

+

Collections save your maps and target lobby counts. Every new server uses the latest promoted scripts, plugins, and configuration. Before launch, you’ll review every new server ID, port, and key.

{#if reason}

{reason}

{/if} -
+
{/if}
diff --git a/src/lib/ui/components/DeployCollectionDialog.svelte b/src/lib/ui/components/DeployCollectionDialog.svelte index dca3445..4582771 100644 --- a/src/lib/ui/components/DeployCollectionDialog.svelte +++ b/src/lib/ui/components/DeployCollectionDialog.svelte @@ -74,19 +74,19 @@ { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}>
Collection launch

{preview?.blueprintName ?? 'Preparing your collection'}

-