From bd7d1ed86f2316cc8c14ed564cbabb1a46263bf8 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Wed, 7 Oct 2026 13:24:00 -0400 Subject: [PATCH] Remove key entry from launch dialogs --- docs/guides/collections.md | 23 ++++---- .../components/DeployCollectionDialog.svelte | 58 +++++++------------ .../ui/components/DeployServerDialog.svelte | 30 +++++----- src/routes/server-keys/+page.svelte | 2 +- 4 files changed, 48 insertions(+), 65 deletions(-) diff --git a/docs/guides/collections.md b/docs/guides/collections.md index 71d1ab4..08b8724 100644 --- a/docs/guides/collections.md +++ b/docs/guides/collections.md @@ -33,8 +33,9 @@ It fills deficits without changing existing servers' files. Choose **Fill missing servers** or **Match collection exactly**, then review the server IDs and estimated capacity. Native host ports are assigned later. 4. Launches covered by unused keys show **Game key available**. Each game’s - available keys are counted once across the full lineup. Add an unused Plutonium - key for each remaining launch, or manage the game pools in **Keys**. + available keys are counted once across the full lineup. If more keys are + needed, add them under **Administration → Keys**, then refresh the preview. + Launch is enabled when every new server has an available game key. 5. Choose **Queue N servers** (or confirm removals and choose **Apply** in exact mode) and follow the collection progress page. If every target is already met, choose **Done**; no operation is created. @@ -102,10 +103,9 @@ Apply validates the reviewed configuration, lobby IDs, keys, and fleet changes, one database transaction, and returns the progress page ID. Native jobs initially have no host or port; each job performs its capacity checks asynchronously during provisioning. If a server lacks an available game key or its reviewed identity changed, -no jobs or key changes from that submission are committed. Legacy submissions also -revalidate their reviewed ports. Refreshing -the preview retains typed keys for launches in the same game that still need -keys, and resets removal confirmation. +no jobs or key reservations from that submission are committed. Legacy submissions +also revalidate their reviewed ports. Refreshing the preview rechecks pool +availability and resets removal confirmation. Submitting the same preview again returns the same collection, including after a lost response or an application restart. Reusing the collection with a new @@ -145,13 +145,14 @@ remain visible in Provisioning activity and history. 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. Use an available key from the game pool, or enter an unused key for that game. +4. The launch automatically uses an available key from the game pool. If none is + available, manage the pool under **Administration → Keys**, then refresh the + preview. 5. Choose **Launch server** and follow the operation to verified health. -A preview lasts 30 minutes. It neither reserves capacity nor saves a key. If -another launch takes the lobby, submission stops before saving the key; -refresh the preview and review the newly assigned ID. A typed key can follow -the new lobby within the same game. Pool availability is checked again at +A preview lasts 30 minutes. It reserves neither capacity nor a key. If +another launch takes the lobby, submission stops before reserving a key; +refresh the preview and review the newly assigned ID. Pool availability is checked again at submission, and keys are reserved atomically so concurrent launches cannot share a credential. diff --git a/src/lib/ui/components/DeployCollectionDialog.svelte b/src/lib/ui/components/DeployCollectionDialog.svelte index f708926..5ccb4f9 100644 --- a/src/lib/ui/components/DeployCollectionDialog.svelte +++ b/src/lib/ui/components/DeployCollectionDialog.svelte @@ -1,6 +1,6 @@ { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}> @@ -113,10 +97,10 @@ {#if open && games.length} { preview = null; confirmRemovals = false; }} />{/if}

{mode === 'exact' ? 'Keep matching lobbies, add missing ones, and remove excess lobbies and maps outside this collection across both games.' : 'Create missing lobbies and keep all existing servers.'}

{#if error}{/if} - {#if !preview}

{busy ? 'Preparing the collection…' : 'Review the fleet changes, capacity estimate, and server keys. Queued launches wait automatically when hosts need more capacity or preparation.'}

{#if !busy}{/if} + {#if !preview}

{busy ? 'Preparing the collection…' : 'Review the fleet changes, capacity estimate, and game key availability. Queued launches wait automatically when hosts need more capacity or preparation.'}

{#if !busy}{/if} {:else} - {#if keyNotice}

{keyNotice}

{/if} - {#if expired}

This preview expired. Refresh before launch; entered keys stay with their game.

{/if} + {#if assignmentNotice}

{assignmentNotice}

{/if} + {#if expired}

This preview expired. Refresh before launch to recheck capacity and key availability.

{/if}
{preview.mode === 'exact' ? `${preview.retainedServers.length} keep · ${preview.servers.length} create · ${preview.removals.length} remove` : preview.servers.length ? `${preview.servers.length} missing server${preview.servers.length === 1 ? '' : 's'} to create` : 'Collection already satisfied'} {#each preview.entries as entry}{/each}
MapTargetExisting / queuedTo create
{entry.recipe.mapName} {entry.recipe.game.toUpperCase()}{entry.count}{entry.existing}{entry.missing}

Targets count inventory, including stopped servers; they do not mean every server is ready to play. {preview.mode === 'exact' ? 'Existing deployed lobbies are kept before queued creations, then the lowest lobby numbers. Retained servers are not changed or restarted.' : 'Only missing servers are created. Extra servers remain and existing servers are not updated.'}

@@ -141,7 +125,8 @@ {#each preview.blockers as blocker}{/each} {#each preview.warnings ?? [] as warning}

{warning}

{/each} {#if preview.servers.length || preview.removals.length} - {#if preview.servers.length}

Available keys are assigned from each game’s pool. Add an unused key for each remaining launch below. Manage pools in Keys (opens in a new tab). Apply saves the reviewed jobs and opens their progress page. Each launch waits for a startup slot and host capacity.

{/if} + {#if preview.servers.length}

Available keys are assigned automatically from each game’s pool. Apply saves the reviewed jobs and opens their progress page. Each launch waits for a startup slot and host capacity.

{/if} + {#if preview.servers.some(server => !server.hasSavedKey)}

There are not enough unused keys for these launches. Add keys to the affected game pools in Keys (opens in a new tab), then Refresh preview to enable launch.

{/if} {#if preview.removals.length && preview.servers.some(server => !server.hasSavedKey)}

Keys held by servers awaiting removal are still in use. To reuse those keys, remove the servers first and refresh this preview after cleanup finishes.

{/if}
{ event.preventDefault(); void launch(); }}> {#if preview.removals.length} @@ -159,14 +144,14 @@
{#if index === 0 || preview.servers[index - 1].entryIndex !== server.entryIndex}{preview.entries[server.entryIndex].recipe.game.toUpperCase()} · {preview.entries[server.entryIndex].recipe.mapName}{/if}{#if server.name}{:else}{preview.entries[server.entryIndex].recipe.mapName}{/if}{server.name ?? 'No available lobby'} · {server.native ? 'Host and port assigned when provisioning starts' : server.port === null ? 'No available port' : `Port ${server.port}`}
{#if server.name} - {#if server.hasSavedKey}

Game key available

- {:else}{/if} + {#if server.hasSavedKey}

Game key available

+ {:else}

No unused {preview.entries[server.entryIndex].recipe.game.toUpperCase()} key available

{/if} {/if} {/each} {/if} - +
{:else}

{preview.mode === 'exact' ? 'Your fleet already matches this collection.' : 'Your fleet already meets or exceeds every target in this collection.'} No servers will be created or removed.

{/if}
Setup and launch requirements

Each new server is automatically placed on an eligible host with the latest game files and prepared runtime. CPU, memory, host readiness, synced files, runtime availability, and local port reservations are checked again before deployment. Existing servers keep their current files.

{#each preview.entries as entry}
{/each}
@@ -185,10 +170,11 @@ .removals li { display: grid; gap: 4px; font-size: 13px; } .confirm-removals { display: flex; align-items: flex-start; gap: 10px; font-size: 13px; } .confirm-removals input { width: auto; margin-top: 2px; } - .key-ready { color: var(--state-online); font-size: 13px; font-weight: 600; } + .key-status { font-size: 13px; font-weight: 600; } + .key-ready { color: var(--state-online); } + .key-missing { color: var(--state-warning); } a { color: var(--accent-primary-hover); } - @media (max-width: 600px) { .server-row .key-ready { grid-column: 2; } } - input { scroll-margin-block: 32px 190px; } + @media (max-width: 600px) { .server-row .key-status { grid-column: 2; } } .launch-footer { position: sticky; bottom: -28px; background: var(--surface); z-index: 2; padding: 16px 0 20px; border-top: 1px solid var(--border-strong); box-shadow: 0 -12px 16px var(--surface); } .map-group { font-size: 12px; color: var(--accent-primary-hover); } .coverage { margin-top: 16px; font-size: 14px; }.coverage summary { cursor: pointer; }.coverage-map { padding-top: 14px; }.coverage ul { list-style: none; padding: 0; }.coverage li { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }.coverage a { color: var(--accent-primary-hover); }.coverage li span { color: var(--text-secondary); } @@ -196,7 +182,7 @@ dialog { width: min(820px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface); color: var(--text-primary); }dialog::backdrop { background: rgb(0 0 0 / .7); backdrop-filter: blur(5px); }.composer { display: grid; gap: 20px; padding: 28px; } header, .section-title, .actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }h2, h3, p { margin: 0; }h2 { font-size: 25px; overflow-wrap: anywhere; }h3 { font-size: 16px; }.eyebrow { display: block; font-size: var(--text-xs); color: var(--text-secondary); text-transform: uppercase; letter-spacing: .1em; margin-bottom: 7px; }.close { border: 0; background: none; color: var(--text-secondary); font-size: 26px; cursor: pointer; }.muted, .caption { font-size: 13px; color: var(--text-secondary); line-height: 1.6; }.caption { font-size: var(--text-xs); }.error { color: var(--state-danger); font-size: 13px; line-height: 1.6; } .lineup { background: var(--surface-sunken); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 20px; }.lineup > strong { font-size: 18px; }.quantity { color: var(--accent-primary-hover); font-weight: 600; }.lineup small { color: var(--text-secondary); }.lineup table { width: 100%; margin: 16px 0; border-collapse: collapse; font-size: 12px; table-layout: fixed; }.lineup th, .lineup td { text-align: center; padding: 10px 4px; border-bottom: 1px solid var(--border-subtle); overflow-wrap: anywhere; }.lineup th:first-child { text-align: left; width: 34%; }.lineup thead { color: var(--text-secondary); font-size: var(--text-xs); } - form { display: grid; gap: 20px; }.servers { margin: 0; padding: 0; list-style: none; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); overflow: hidden; }.server-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) 220px; align-items: center; gap: 16px; padding: 20px 16px; }.server-row + .server-row { border-top: 1px solid var(--border-subtle); }.order { font-family: var(--font-mono); color: var(--text-tertiary); font-size: 12px; align-self: start; padding-top: 3px; }.server-identity { display: grid; gap: 8px; min-width: 0; }.server-row strong { font-size: 14px; overflow-wrap: anywhere; }.server-id { font-family: var(--font-mono); font-size: 11px; line-height: 1.6; color: var(--text-secondary); overflow-wrap: anywhere; }label { display: grid; gap: 7px; font-size: var(--text-xs); color: var(--text-secondary); }input { width: 100%; box-sizing: border-box; border: 1px solid var(--border-control); border-radius: 7px; background: var(--surface-sunken); color: var(--text-primary); padding: 10px; font-size: 13px; }.actions { justify-content: flex-end; }.text-button { border: 0; background: none; color: var(--accent-primary-hover); cursor: pointer; font-size: 12px; }.recipes { border-top: 1px solid var(--border-subtle); padding-top: 20px; }.recipes summary { cursor: pointer; font-size: 13px; }.recipe { padding: 20px 0; border-bottom: 1px solid var(--border-subtle); } + form { display: grid; gap: 20px; }.servers { margin: 0; padding: 0; list-style: none; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); overflow: hidden; }.server-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) 220px; align-items: center; gap: 16px; padding: 20px 16px; }.server-row + .server-row { border-top: 1px solid var(--border-subtle); }.order { font-family: var(--font-mono); color: var(--text-tertiary); font-size: 12px; align-self: start; padding-top: 3px; }.server-identity { display: grid; gap: 8px; min-width: 0; }.server-row strong { font-size: 14px; overflow-wrap: anywhere; }.server-id { font-family: var(--font-mono); font-size: 11px; line-height: 1.6; color: var(--text-secondary); overflow-wrap: anywhere; }label { display: grid; gap: 7px; font-size: var(--text-xs); color: var(--text-secondary); }.actions { justify-content: flex-end; }.text-button { border: 0; background: none; color: var(--accent-primary-hover); cursor: pointer; font-size: 12px; }.recipes { border-top: 1px solid var(--border-subtle); padding-top: 20px; }.recipes summary { cursor: pointer; font-size: 13px; }.recipe { padding: 20px 0; border-bottom: 1px solid var(--border-subtle); } .launch-footer { display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap; }.requirements { margin-top: 16px; }button:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 3px; } - @media (max-width: 600px) { .composer { padding: 20px; }.lineup { padding: 16px; }.server-row { grid-template-columns: 24px minmax(0, 1fr); gap: 12px; padding: 16px; }.server-row label { grid-column: 2; }.launch-footer { align-items: stretch; flex-direction: column; gap: 16px; }.actions { justify-content: space-between; }h2 { font-size: 22px; } } + @media (max-width: 600px) { .composer { padding: 20px; }.lineup { padding: 16px; }.server-row { grid-template-columns: 24px minmax(0, 1fr); gap: 12px; padding: 16px; }.launch-footer { align-items: stretch; flex-direction: column; gap: 16px; }.actions { justify-content: space-between; }h2 { font-size: 22px; } } diff --git a/src/lib/ui/components/DeployServerDialog.svelte b/src/lib/ui/components/DeployServerDialog.svelte index fd3bdf5..554a1b0 100644 --- a/src/lib/ui/components/DeployServerDialog.svelte +++ b/src/lib/ui/components/DeployServerDialog.svelte @@ -25,7 +25,6 @@ let preview = $state(null); let hostSelection = $state({ libraryIds: {}, image: '' }); let hostReady = $state(false); - let plutoKey = $state(''); let busy = $state(false); let loaded = $state(false); let error = $state(''); @@ -35,7 +34,7 @@ let saving = $state(false); let wasOpen = false; let session = 0; - let keyNotice = $state(''); + let lobbyNotice = $state(''); let selectionNotice = $state(''); let now = $state(Date.now()); let heading: HTMLHeadingElement; @@ -50,7 +49,7 @@ async function initialize() { const current = ++session; - preview = null; plutoKey = ''; error = ''; loaded = false; busy = false; keyNotice = ''; selectionNotice = ''; + preview = null; error = ''; loaded = false; busy = false; lobbyNotice = ''; selectionNotice = ''; blueprintName = ''; description = ''; savedName = ''; saving = false; blueprintId = initialBlueprintId; game = initialGame; @@ -77,7 +76,7 @@ function selectionChanged() { if (preview) selectionNotice = 'The launch settings or game files changed. Review the launch again with the latest files.'; - preview = null; plutoKey = ''; keyNotice = ''; + preview = null; lobbyNotice = ''; } async function review() { @@ -88,7 +87,7 @@ if (!hostReady) { error = 'Wait for a host to finish preparing the latest game files and runtime.'; return; } const current = session; const previousName = preview?.name; - busy = true; error = ''; savedName = ''; keyNotice = ''; selectionNotice = ''; + busy = true; error = ''; savedName = ''; lobbyNotice = ''; selectionNotice = ''; try { const res = await fetch('/api/provisioning/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...(sourceDeploymentId ? { deploymentId: sourceDeploymentId } : { game, mapSlug }), @@ -97,8 +96,7 @@ const body = await res.json(); if (!res.ok) throw new Error(body.error ?? 'Could not prepare launch preview.'); if (current === session) { - if (previousName && previousName !== body.name) keyNotice = 'The available lobby changed. Any entered key can still be used for this game.'; - if (body.hasSavedKey) plutoKey = ''; + if (previousName && previousName !== body.name) lobbyNotice = 'The available lobby changed. Review the updated server before launching.'; preview = body; now = Date.now(); await tick(); heading?.focus(); } @@ -107,12 +105,12 @@ } async function launch() { - if (!preview || !preview.ready || !hostReady || expired || busy) return; + if (!preview || !preview.ready || !preview.hasSavedKey || !hostReady || expired || busy || saving) return; busy = true; error = ''; try { const res = await fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ game: preview.recipe.game, gameMode: 'zombie', mapSlug: preview.recipe.mapSlug, - expectedName: preview.name, previewId: preview.id, plutoKey: plutoKey.trim() || undefined }) }); + expectedName: preview.name, previewId: preview.id }) }); const body = await res.json(); if (!res.ok || !body.ok) throw new Error(body.error ?? 'Could not queue this launch.'); open = false; @@ -147,8 +145,8 @@

{error ? 'Your launch choices could not be loaded.' : 'Loading your maps and collections…'}

{#if error}{/if} {:else if preview} - {#if keyNotice}

{keyNotice}

{/if} - {#if expired}

This preview expired. Refresh it to recheck capacity. Entered keys stay with this game when you refresh.

{/if} + {#if lobbyNotice}

{lobbyNotice}

{/if} + {#if expired}

This preview expired. Refresh it to recheck capacity and key availability.

{/if}
{#if preview.blueprintName}

From collection: {preview.blueprintName}

{/if}

Before you launch

    {#each preview.checks as check}
  • {check.state === 'pass' ? '✓' : check.state === 'blocked' ? '!' : 'i'}
    {check.label}

    {check.detail}

  • {/each}
@@ -156,11 +154,9 @@
{ event.preventDefault(); void launch(); }}>
Your new server{preview.name ?? 'Waiting for an available lobby'}

An available key from this game’s pool is assigned when you launch.

{#if preview.hasSavedKey}

Game key available

- {:else}{/if} -

{preview.hasSavedKey ? 'An unused key from this game’s pool will be assigned automatically. Availability is checked again at launch.' : 'Add an unused key for this game. Launch saves it in the game pool and assigns it to this server until removal is confirmed.'} Manage saved keys in a new tab; your launch stays open here.

-
- {#if !preview.hasSavedKey && !plutoKey.trim()}

Enter an unused key for this game to enable launch.

{/if} -

Preview valid until {$timeFormat.minuteTime(preview.expiresAt)}. Capacity is checked again at launch. The host and port are assigned automatically.

+ {:else}

No unused {preview.recipe.game.toUpperCase()} keys are available. Add a key in Keys (opens in a new tab), then Refresh preview to enable launch.

{/if} +
+

Preview valid until {$timeFormat.minuteTime(preview.expiresAt)}. Capacity and key availability are checked again at launch. The host and port are assigned automatically.

Make this a reusable collection

Save this game and map with a target of one lobby. Future launches use the latest promoted setup and create only missing lobbies.

{#if savedName}

“{savedName}” is ready in your collection library.

{:else} @@ -172,7 +168,7 @@ {:else}
{ event.preventDefault(); void review(); }}> - {#if selectedBlueprint}

{selectedBlueprint.totalServers} target server{selectedBlueprint.totalServers === 1 ? '' : 's'}

{#each selectedBlueprint.entries as entry}

{entry.count}× {entry.mapName} · {entry.game.toUpperCase()}

{/each}

Review missing servers and their keys together. Every new server uses the latest promoted scripts, plugins, and configuration.

{#if selectedBlueprint.description}

{selectedBlueprint.description}

{/if}
+ {#if selectedBlueprint}

{selectedBlueprint.totalServers} target server{selectedBlueprint.totalServers === 1 ? '' : 's'}

{#each selectedBlueprint.entries as entry}

{entry.count}× {entry.mapName} · {entry.game.toUpperCase()}

{/each}

Review missing servers and game key availability together. Every new server uses the latest promoted scripts, plugins, and configuration.

{#if selectedBlueprint.description}

{selectedBlueprint.description}

{/if}
{:else}

Pick a map to review its current promoted scripts, plugins, and configuration. You can save the map as a collection for future launches.

{/if}
Know what you’re launching.

Review the current promoted setup, available lobby, and launch prerequisites before creating your server.

diff --git a/src/routes/server-keys/+page.svelte b/src/routes/server-keys/+page.svelte index 22dbe9b..e461c76 100644 --- a/src/routes/server-keys/+page.svelte +++ b/src/routes/server-keys/+page.svelte @@ -104,7 +104,7 @@ {#if notice}

{notice}

{/if}

Saved keys

{#if loading}

Loading keys…

-{:else if !filtered.length} +{:else if !filtered.length} {:else}
Game / labelAvailabilityLast savedActions