Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
76 lines
4.4 KiB
Svelte
76 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import { isDialogBackdrop } from '$ui/dialog.js';
|
|
import Button from './primitives/Button.svelte';
|
|
|
|
let { open = $bindable(false), onqueued }: { open?: boolean; onqueued: (count: number) => void } = $props();
|
|
let dialog: HTMLDialogElement | undefined = $state();
|
|
let servers = $state<string[] | null>(null);
|
|
let busy = $state(false);
|
|
let error = $state('');
|
|
let wasOpen = false;
|
|
|
|
$effect(() => {
|
|
if (!dialog) return;
|
|
if (open && !wasOpen) { wasOpen = true; dialog.showModal(); void review(); }
|
|
else if (!open && wasOpen) { wasOpen = false; dialog.close(); }
|
|
});
|
|
|
|
async function review() {
|
|
busy = true; error = ''; servers = null;
|
|
try {
|
|
const res = await fetch('/api/deployments/house-party');
|
|
if (!res.ok) throw new Error('Could not load the fleet.');
|
|
servers = (await res.json()).servers;
|
|
} catch (err) { error = (err as Error).message; }
|
|
finally { busy = false; }
|
|
}
|
|
|
|
async function launch() {
|
|
if (busy || !servers?.length) return;
|
|
busy = true; error = '';
|
|
try {
|
|
const res = await fetch('/api/deployments/house-party', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ servers })
|
|
});
|
|
const body = await res.json();
|
|
if (!res.ok) {
|
|
if (res.status === 409) servers = null;
|
|
throw new Error(body.error ?? 'Could not queue destruction.');
|
|
}
|
|
open = false;
|
|
onqueued(body.deployments.length);
|
|
} catch (err) { error = (err as Error).message; }
|
|
finally { busy = false; }
|
|
}
|
|
</script>
|
|
|
|
<dialog bind:this={dialog} aria-label="Destroy fleet" onclick={event => { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}>
|
|
<div class="content">
|
|
<header><h2>Queue fleet removal</h2></header>
|
|
<p>Queue destruction of every server, including servers being created. Each waits until a fresh player count confirms it is empty. Unreachable servers stay queued.</p>
|
|
<p>Destruction removes containers, server files, and fleet registrations. Saved keys and blueprints are kept.</p>
|
|
<p>In Active deployments, cancel individual waiting requests or all waiting removals. Once a server begins removal, that removal cannot be cancelled or undone.</p>
|
|
{#if error}<p class="error" role="alert">{error}</p>{/if}
|
|
{#if servers}
|
|
<strong>{servers.length ? `${servers.length} server${servers.length === 1 ? '' : 's'} to destroy` : 'The house is already empty.'}</strong>
|
|
{#if servers.length}<ul>{#each servers as server}<li>{server}</li>{/each}</ul><p class="caption">Existing destruction requests are reused. Servers added after this request are not included.</p>{/if}
|
|
{:else}<p>{busy ? 'Reviewing the fleet…' : 'Refresh the preview to review the current fleet.'}</p>{/if}
|
|
<div class="actions">
|
|
<Button variant="ghost" disabled={busy} onclick={() => open = false}>Cancel</Button>
|
|
{#if !servers}<Button disabled={busy} onclick={review}>Refresh preview</Button>
|
|
{:else}<Button variant="danger" loading={busy} disabled={busy || !servers.length} onclick={launch}>Queue destruction of {servers.length} server{servers.length === 1 ? '' : 's'}</Button>{/if}
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<style>
|
|
dialog { width: min(620px, 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); }
|
|
.content { display: grid; gap: 20px; padding: 28px; } h2, p { margin: 0; } h2 { font-size: 25px; }
|
|
p { color: var(--text-secondary); font-size: 13px; line-height: 1.6; } .error { color: var(--state-danger); } .caption { font-size: 12px; }
|
|
ul { margin: 0; padding: 12px 12px 12px 30px; max-height: 220px; overflow: auto; background: var(--surface-sunken); border-radius: var(--radius-md); }
|
|
li { padding: 5px 0; font-family: var(--font-mono); font-size: 12px; overflow-wrap: anywhere; }
|
|
.actions { display: flex; gap: 12px; justify-content: flex-end; flex-wrap: wrap; }
|
|
</style>
|