Introduce an outbound Python host agent, Debian installer, durable remote operations, immutable game-file libraries, host-aware deployments and ports, and occupancy-safe campaigns and draining. Native hosts do not require Komodo. Add administrator, operator and viewer accounts, protected management routes, secure sessions, password recovery, audit history and separate telemetry tokens. Document first-admin setup, reporter migration and live-host acceptance checks. Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type checking, the production build, compiled SSR/API smoke tests and administrator recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
60 lines
3.3 KiB
Svelte
60 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import Button from './primitives/Button.svelte';
|
|
|
|
let { open = $bindable(false), name, players }: { open?: boolean; name: string; players: number | null } = $props();
|
|
let dialog = $state<HTMLDialogElement>();
|
|
let submitting = $state(false);
|
|
let error = $state('');
|
|
$effect(() => {
|
|
if (!dialog) return;
|
|
if (open && !dialog.open) { error = ''; dialog.showModal(); }
|
|
else if (!open && dialog.open) dialog.close();
|
|
});
|
|
|
|
async function destroy() {
|
|
if (submitting) return;
|
|
submitting = true;
|
|
error = '';
|
|
try {
|
|
const res = await fetch(`/api/servers/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
const body = await res.json();
|
|
if (!res.ok || !body.ok || !body.deploymentId) throw new Error(body.error ?? 'Could not queue destruction. Please retry.');
|
|
await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`);
|
|
open = false;
|
|
} catch (err) { error = (err as Error).message; }
|
|
finally { submitting = false; }
|
|
}
|
|
</script>
|
|
|
|
<dialog bind:this={dialog} aria-labelledby="destroy-title" onclose={() => open = false}
|
|
oncancel={(event) => { if (submitting) event.preventDefault(); }}>
|
|
<form onsubmit={(event) => { event.preventDefault(); void destroy(); }}>
|
|
<h2 id="destroy-title">Queue server removal?</h2>
|
|
<p class="name">{name}</p>
|
|
<p>This stops and removes the container, its writable server files, and campaign details. Shared game libraries, provisioning history, and the saved server key are retained.</p>
|
|
<div class="notice">
|
|
{#if players !== null && players > 0}<strong>{players} player{players === 1 ? '' : 's'} currently online.</strong>{/if}
|
|
Removal waits until a live check confirms the server is empty. Follow progress or cancel the waiting request on the provisioning page.
|
|
</div>
|
|
<p class="irreversible">You can cancel while waiting. Once removal begins, it cannot be undone.</p>
|
|
{#if error}<p role="alert" class="error">{error}</p>{/if}
|
|
<footer>
|
|
<Button variant="ghost" disabled={submitting} onclick={() => open = false}>Cancel</Button>
|
|
<Button variant="danger" type="submit" loading={submitting}>Queue server removal</Button>
|
|
</footer>
|
|
</form>
|
|
</dialog>
|
|
|
|
<style>
|
|
dialog { width: min(520px, calc(100vw - 32px)); 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 / .65); backdrop-filter: blur(4px); }
|
|
form { padding: 28px; } h2 { margin: 0 0 12px; font-size: 22px; }
|
|
p { line-height: 1.6; color: var(--text-secondary); }
|
|
.name { font-family: var(--font-mono); color: var(--text-primary); overflow-wrap: anywhere; }
|
|
.notice { padding: 16px; background: var(--surface-raised); border-radius: var(--radius-md); line-height: 1.6; }
|
|
strong { display: block; color: var(--state-warning); margin-bottom: 4px; }
|
|
.irreversible { font-size: 13px; } .error { color: var(--state-danger); }
|
|
footer { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
|
|
</style>
|