fix: show saved keys as ready in deployment previews

Replace saved-key inputs with a ready indicator and direct key updates to Keys. Clear hidden key overrides on preview refresh and update the blueprint documentation.

Fixes #41
This commit is contained in:
2026-09-27 11:08:05 -04:00
parent c4ae73fa7a
commit 1aa7f15371
3 changed files with 16 additions and 7 deletions
+2 -2
View File
@@ -26,8 +26,8 @@ destroy servers or change existing servers' files.
1. Choose **Create blueprint** on Deployments 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** to review every new server ID and game port.
4. Enter each lobby's Plutonium key, or leave its field blank to reuse the key
saved for that exact server ID.
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**.
5. Choose **Queue N servers** and follow their collection progress page.
If every target is already met, choose **Done**; no operation is created.
@@ -40,7 +40,7 @@
if (current === session) {
const next = body as CollectionPreview;
const names = new Set(next.servers.map(server => server.name));
keys = Object.fromEntries(next.servers.filter(server => server.name).map(server => [server.name!, keys[server.name!] ?? '']));
keys = Object.fromEntries(next.servers.filter(server => server.name && !server.hasSavedKey).map(server => [server.name!, keys[server.name!] ?? '']));
if (previous && (previous.servers.length !== next.servers.length || previous.servers.some(server => !names.has(server.name)))) keyNotice = 'Server assignments changed. Keys were retained only for the exact IDs still shown below. Review the new assignments.';
else if (previous) keyNotice = 'Preview refreshed. Entered keys are retained for these same server IDs.';
preview = next; now = Date.now();
@@ -84,14 +84,17 @@
<div class="section-title"><h3>{preview.servers.length ? 'Your launch order' : 'Nothing to create'}</h3><button class="text-button" disabled={busy} onclick={review}>Refresh preview</button></div>
{#each preview.blockers as blocker}<p class="error" role="alert">{blocker}</p>{/each}
{#if preview.servers.length}
<p class="muted">Add a key for each new lobby below, or reuse its saved key. Fleet counts and available slots are checked again when you queue the launch.</p>
<p class="muted">Saved keys are reused automatically. Add keys only where required below. Update saved keys in <a href="/server-keys" target="_blank" rel="noopener">Keys (opens in a new tab)</a>. Fleet counts and available slots are checked again when you queue the launch.</p>
<form onsubmit={event => { event.preventDefault(); void launch(); }}>
<ol class="servers" aria-label="Server launch order">
{#each preview.servers as server, index (server.name ?? `unavailable-${index}`)}
<li class="server-row">
<span class="order" aria-hidden="true">{String(index + 1).padStart(2, '0')}</span>
<div class="server-identity">{#if index === 0 || preview.servers[index - 1].entryIndex !== server.entryIndex}<span class="map-group">{preview.entries[server.entryIndex].recipe.game.toUpperCase()} · {preview.entries[server.entryIndex].recipe.mapName}</span>{/if}<strong>{#if server.name}<ServerIdentity name={server.name} />{:else}{preview.entries[server.entryIndex].recipe.mapName}{/if}</strong><span class="server-id">{server.name ?? 'No available lobby'} · {server.port === null ? 'No available port' : `Port ${server.port}`}</span></div>
{#if server.name}<label><span>{server.hasSavedKey ? 'Saved key available' : 'Server key required'}</span><input aria-label={`Key for ${server.name}`} data-missing={!server.hasSavedKey && !keys[server.name]?.trim()} type="password" bind:value={keys[server.name]} disabled={busy} maxlength="512" autocomplete="off" placeholder={server.hasSavedKey ? 'Leave blank to reuse' : 'Paste this server’s key'} /></label>{/if}
{#if server.name}
{#if server.hasSavedKey}<p class="key-ready"><span aria-hidden="true">✓</span> Saved key ready</p>
{:else}<label><span>Server key required</span><input aria-label={`Key for ${server.name}`} data-missing={!keys[server.name]?.trim()} type="password" bind:value={keys[server.name]} disabled={busy} maxlength="512" autocomplete="off" placeholder="Paste this server’s key" /></label>{/if}
{/if}
</li>
{/each}
</ol>
@@ -104,6 +107,9 @@
</dialog>
<style>
.key-ready { color: var(--state-online); font-size: 13px; font-weight: 600; }
a { color: var(--accent-primary-hover); }
@media (max-width: 600px) { .server-row .key-ready { grid-column: 2; } }
input { scroll-margin-block: 32px 190px; }
.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); }
@@ -79,6 +79,7 @@
if (!res.ok) throw new Error(body.error ?? 'Could not prepare launch preview.');
if (current === session) {
if (previousName && previousName !== body.name) { plutoKey = ''; keyNotice = 'The available server ID changed. Enter the key for the new ID below; the previous key was not transferred.'; }
if (body.hasSavedKey) plutoKey = '';
preview = body; now = Date.now();
await tick(); heading?.focus();
}
@@ -133,8 +134,9 @@
</section>
<form onsubmit={event => { event.preventDefault(); void launch(); }}>
<div class="identity"><span class="eyebrow">Your new server</span><strong>{preview.name ?? 'Waiting for an available lobby'}</strong><p>{preview.port ? `Port ${preview.port} · ` : ''}Fresh identity, with its own server key.</p></div>
<label>Plutonium server key<input type="password" bind:value={plutoKey} disabled={busy || saving || !preview.name} maxlength="512" autocomplete="off" placeholder={preview.hasSavedKey ? 'Saved key available — leave blank to reuse' : 'Paste the key for this server ID'} /></label>
<p class="muted">{preview.hasSavedKey ? 'A key is stored for this exact server ID; its validity is checked when the game starts. Enter a replacement here to update it at launch.' : 'Add the key here. It is saved for this exact server ID when you launch, including if provisioning fails.'} <a href={`/server-keys?server=${encodeURIComponent(preview.name ?? '')}`} target="_blank" rel="noopener">Manage saved keys in a new tab</a>; your launch stays open here.</p>
{#if preview.hasSavedKey}<p class="key-ready"><span aria-hidden="true">✓</span> Saved key ready</p>
{:else}<label>Plutonium server key<input type="password" bind:value={plutoKey} disabled={busy || saving || !preview.name} maxlength="512" autocomplete="off" placeholder="Paste the key for this server ID" /></label>{/if}
<p class="muted">{preview.hasSavedKey ? 'The saved key for this server ID will be reused automatically. Its validity is checked when the game starts. Update it in Keys.' : 'Add the key here. It is saved for this exact server ID when you launch, including if provisioning fails.'} <a href={`/server-keys?server=${encodeURIComponent(preview.name ?? '')}`} target="_blank" rel="noopener">Manage saved keys in a new tab</a>; your launch stays open here.</p>
<div class="launch-actions">{#if !sourceDeploymentId}<Button variant="ghost" disabled={busy || saving} onclick={() => { preview = null; plutoKey = ''; error = ''; }}>Back</Button>{/if}<Button type="submit" variant="primary" loading={busy} disabled={busy || saving || expired || !preview.ready || (!preview.hasSavedKey && !plutoKey.trim())}>Launch server</Button></div>
{#if !preview.hasSavedKey && !plutoKey.trim()}<p class="muted">Enter this server’s key to enable launch.</p>{/if}
<p class="caption">Preview valid until {$timeFormat.minuteTime(preview.expiresAt)}. Lobby and port availability are checked again at launch.</p>
@@ -160,6 +162,7 @@
<DeployCollectionDialog bind:open={collectionOpen} {blueprintId} />
<style>
.key-ready { color: var(--state-online); font-weight: 600; }
dialog { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); padding: 0; width: min(700px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }
dialog::backdrop { background: rgb(0 0 0 / .7); backdrop-filter: blur(5px); }
.composer { display: grid; gap: 24px; padding: 30px; } header, .section-title { display: flex; justify-content: space-between; align-items: center; gap: 16px; }