diff --git a/src/lib/activity.ts b/src/lib/activity.ts index 573ce39..7d64287 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -1,6 +1,6 @@ export const activityTypes = { create: 'Create server', start: 'Start server', restart: 'Restart server', rebuild: 'Rebuild server', - stop: 'Stop server', remove: 'Remove server', update: 'Game update', files: 'File changes', + stop: 'Stop server', remove: 'Destroy server', update: 'Game update', files: 'File changes', health: 'Health finding', drift: 'File drift', host: 'Host operation', beta: 'Beta test' } as const; export type ActivityType = keyof typeof activityTypes; diff --git a/src/lib/host-operation-labels.ts b/src/lib/host-operation-labels.ts index e4fe07d..d5a471f 100644 --- a/src/lib/host-operation-labels.ts +++ b/src/lib/host-operation-labels.ts @@ -1,7 +1,7 @@ export const hostOperationTitles: Record = { 'instance.create': 'Create server container', 'instance.rebuild': 'Rebuild server container', 'instance.start': 'Start server container', 'instance.restart': 'Restart server container', - 'instance.stop': 'Stop server container', 'instance.remove': 'Remove server container', + 'instance.stop': 'Stop server container', 'instance.remove': 'Destroy server container', 'instance.cancel-create': 'Cancel server creation', 'instance.inspect': 'Check server container', 'host.inspect': 'Check host', 'host.decommission': 'Remove host', 'runtime.pull': 'Prepare container runtime', 'library.download': 'Download game files', 'library.import': 'Import game files', diff --git a/src/lib/provisioning.ts b/src/lib/provisioning.ts index 21e8280..4de4ecc 100644 --- a/src/lib/provisioning.ts +++ b/src/lib/provisioning.ts @@ -2,11 +2,11 @@ export const provisioningActions = ['create', 'start', 'restart', 'rebuild', 'st export type ProvisioningAction = typeof provisioningActions[number]; export type RuntimeAction = Exclude; export const actionLabels: Record = { - create: 'Create', start: 'Start', restart: 'Restart', rebuild: 'Rebuild', stop: 'Stop', destroy: 'Remove' + create: 'Create', start: 'Start', restart: 'Restart', rebuild: 'Rebuild', stop: 'Stop', destroy: 'Destroy' }; export const actionCompleted: Record = { create: 'Server creation completed', start: 'Server started', restart: 'Server restarted', - rebuild: 'Server rebuilt', stop: 'Server stopped', destroy: 'Server removed' + rebuild: 'Server rebuilt', stop: 'Server stopped', destroy: 'Server destroyed' }; export const runtimeActions: RuntimeAction[] = ['start', 'restart', 'rebuild', 'stop']; export const runtimeDescriptions: Record = { diff --git a/src/lib/server/activity.ts b/src/lib/server/activity.ts index 18f02ff..3a93bef 100644 --- a/src/lib/server/activity.ts +++ b/src/lib/server/activity.ts @@ -62,7 +62,7 @@ items AS ( CASE WHEN d.state IN ('failed','interrupted','cancelled','warning') THEN d.state WHEN m.failed=1 THEN 'failed' WHEN m.pending=1 AND d.state='done' THEN 'verifying' WHEN m.cancelled=1 THEN 'cancelled' ELSE d.state END AS rawState, - CASE WHEN m.is_update=1 THEN 'Update game' ELSE ${labelCase('d.action', { create: 'Create server', start: 'Start server', restart: 'Restart server', rebuild: 'Rebuild server', stop: 'Stop server', destroy: 'Remove server' }, 'Server operation')} END AS title, + CASE WHEN m.is_update=1 THEN 'Update game' ELSE ${labelCase('d.action', { create: 'Create server', start: 'Start server', restart: 'Restart server', rebuild: 'Rebuild server', stop: 'Stop server', destroy: 'Destroy server' }, 'Server operation')} END AS title, CASE WHEN m.failed=1 AND d.state='done' THEN 'The restart completed, but maintenance verification failed.' WHEN m.pending=1 AND d.state='done' THEN 'The restart completed. Maintenance verification is still in progress.' WHEN m.revision IS NOT NULL THEN 'Target game revision r' || m.revision || '.' diff --git a/src/lib/ui/components/DeployCollectionDialog.svelte b/src/lib/ui/components/DeployCollectionDialog.svelte index ebba741..e51cdc4 100644 --- a/src/lib/ui/components/DeployCollectionDialog.svelte +++ b/src/lib/ui/components/DeployCollectionDialog.svelte @@ -211,10 +211,10 @@ {#if preview.removals.length}
-

Remove {preview.removals.length} server{preview.removals.length === 1 ? '' : 's'}

+

Destroy {preview.removals.length} server{preview.removals.length === 1 ? '' : 's'}

Containers, files, and registrations will be removed. Occupied or unreachable servers wait for a fresh empty-player check; unstarted creations are cancelled.

    {#each preview.removals as server}
  • {serverTitle(server.name)}{server.name}{server.reason}
  • {/each}
- +
{/if}
diff --git a/src/lib/ui/components/DestroyServerModal.svelte b/src/lib/ui/components/DestroyServerModal.svelte index d058b5f..e776cf1 100644 --- a/src/lib/ui/components/DestroyServerModal.svelte +++ b/src/lib/ui/components/DestroyServerModal.svelte @@ -19,7 +19,7 @@ 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 server removal. Please retry.'); + if (!res.ok || !body.ok || !body.deploymentId) throw new Error(body.error ?? 'Could not queue server destruction. Please retry.'); await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`); open = false; } catch (err) { error = (err as Error).message; } @@ -27,17 +27,17 @@ } - +

{lifecycleImpact('destroy', players, round)}

This stops and removes the container, its writable server files, and registration. Shared game libraries, recorded history, and the saved server key are retained.

Follow progress or cancel the waiting request on the provisioning page.

-

You can cancel while waiting. Once removal begins, it cannot be undone.

+

You can cancel while waiting. Once destruction begins, it cannot be undone.

{#if error}{/if}
{#snippet actions()} - + {/snippet}
diff --git a/src/lib/ui/components/FleetActionsDialog.svelte b/src/lib/ui/components/FleetActionsDialog.svelte new file mode 100644 index 0000000..e7be900 --- /dev/null +++ b/src/lib/ui/components/FleetActionsDialog.svelte @@ -0,0 +1,142 @@ + + + +
+ +

{descriptions[action]}

+ {#if error}{/if} + {#if loading}

Loading the latest server status…

+ {:else} +
+ + {targets.length} selected · {eligible.length} eligible + +
+ {#if servers.length} +
+ Servers + {#each servers as server (server.name)} + {@const available = eligible.some(item => item.name === server.name)} + {@const health = serverHealth(server, $uiNow)} + + {/each} +
+ {:else if !error}

No servers are available.

{/if} + {/if} + {#if action !== 'start'}

Each server waits for a live check confirming it is empty. Follow progress or cancel waiting jobs in Provisioning.

{/if} + {#if destructive}

Shared game libraries, recorded history, saved keys, and collections are retained.

{/if} + {#if destructive}{/if} + {#if failures.length}

Some requests could not be queued:

    {#each failures as failure}
  • {serverTitle(failure.server)}: {failure.error}
  • {/each}
{/if} + {#if busy}

Queuing {actionLabels[action].toLowerCase()} for the selected servers…

{/if} +
+ {#snippet actions()} + + + {/snippet} +
+ + diff --git a/src/lib/ui/components/HousePartyDialog.svelte b/src/lib/ui/components/HousePartyDialog.svelte deleted file mode 100644 index c8fc711..0000000 --- a/src/lib/ui/components/HousePartyDialog.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - { if (isDialogBackdrop(event, dialog) && !busy) open = false; }} oncancel={event => { if (busy) event.preventDefault(); }} onclose={() => open = false}> -
-

Remove fleet servers?

-

Queue removal of every server, including servers being created. Each waits until a fresh player count confirms it is empty. Unreachable servers stay queued.

-

Removal deletes containers, server files, and fleet registrations. Saved keys and collections are kept.

-

In Provisioning’s Active tab, cancel individual waiting requests or all waiting removals. Once a server begins removal, that removal cannot be cancelled or undone.

- {#if error}{/if} - {#if servers} - {servers.length ? `${servers.length} server${servers.length === 1 ? '' : 's'} to remove` : 'No servers to remove.'} - {#if servers.length}
    {#each servers as server}
  • {serverTitle(server)}
  • {/each}

Existing removal requests are reused. Servers added after this request are not included.

{/if} - {:else}

{busy ? 'Reviewing the fleet…' : 'Refresh the preview to review the current fleet.'}

{/if} -
- - {#if !servers} - {:else}{/if} -
-
-
- - diff --git a/src/lib/ui/components/LifecycleDialog.svelte b/src/lib/ui/components/LifecycleDialog.svelte index f324eb3..6517aba 100644 --- a/src/lib/ui/components/LifecycleDialog.svelte +++ b/src/lib/ui/components/LifecycleDialog.svelte @@ -27,7 +27,7 @@ const impactPlayers = $derived(initialServer ? players : selected && health?.state === 'online' && freshServer(selected, $uiNow) ? selected.last_players : null); const impactRound = $derived(initialServer ? round : selected ? currentRound(selected, selected.telemetry, $uiNow) : null); const title = $derived(initialServer ? `${actionLabels[action]} ${serverTitle(initialServer)}?` : 'Manage server'); - const submitLabel = $derived(action === 'start' ? 'Start server' : action === 'destroy' ? 'Remove server when empty' : `${actionLabels[action]} when empty`); + const submitLabel = $derived(action === 'start' ? 'Start server' : action === 'destroy' ? 'Destroy server when empty' : `${actionLabels[action]} when empty`); $effect(() => { if (!open) return; const fixedServer = initialServer; @@ -92,7 +92,7 @@ {#if !server}{/if} {#if server && !applicable}{/if} {#each available as value}{/each} - {#if server}{/if} + {#if server}{/if} {/if} {#if loading}

Checking the latest server status…

diff --git a/src/lib/ui/fleet-actions.ts b/src/lib/ui/fleet-actions.ts new file mode 100644 index 0000000..286a7b4 --- /dev/null +++ b/src/lib/ui/fleet-actions.ts @@ -0,0 +1,34 @@ +import type { FleetServer } from '../fleet-overview.js'; +import type { RuntimeAction } from '../provisioning.js'; +import { applicableRuntimeActions } from './lifecycle-actions.js'; + +export const fleetActions = ['restart', 'start', 'stop', 'rebuild', 'destroy'] as const; +export type FleetAction = RuntimeAction | 'destroy'; +export interface FleetActionFailure { server: string; error: string } +export interface FleetActionResult { + queued: { server: string; deploymentId: string }[]; + failed: FleetActionFailure[]; +} + +export function fleetActionAvailable(server: FleetServer, action: FleetAction, now: number): boolean { + return action === 'destroy' || applicableRuntimeActions(server, now).includes(action); +} + +/** Submit only the reviewed selection. Each server keeps its own durable job and result. */ +export async function queueFleetAction(action: FleetAction, servers: readonly string[], request: typeof fetch = fetch): Promise { + const result: FleetActionResult = { queued: [], failed: [] }; + for (const server of new Set(servers)) { + try { + const response = action === 'destroy' + ? await request(`/api/servers/${encodeURIComponent(server)}`, { method: 'DELETE' }) + : await request('/api/provisioning', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server, action }) }); + const body = await response.json(); + if (!response.ok || !body.deploymentId) { + result.failed.push({ server, error: body.error ?? 'Could not queue this operation.' }); + } else result.queued.push({ server, deploymentId: body.deploymentId }); + } catch { + result.failed.push({ server, error: 'Could not confirm whether the request was queued. Check Provisioning before trying again.' }); + } + } + return result; +} diff --git a/src/lib/ui/lifecycle-actions.ts b/src/lib/ui/lifecycle-actions.ts index 151cd28..ae01d57 100644 --- a/src/lib/ui/lifecycle-actions.ts +++ b/src/lib/ui/lifecycle-actions.ts @@ -17,7 +17,7 @@ export function applicableRuntimeActions(server: FleetServer, now: number): Runt /** Counts must come from fresh observations; null must never read as an empty lobby. */ export function lifecycleImpact(action: RuntimeAction | 'destroy', players: number | null, round: number | null): string { if (action === 'start') return 'Start the existing container, then verify that the game is responding.'; - const label = action === 'destroy' ? 'Removal' : actionLabels[action]; + const label = action === 'destroy' ? 'Destruction' : actionLabels[action]; if (players === null || !Number.isSafeInteger(players) || players < 0) { return `Player count is unknown. ${label} waits for a live check confirming the lobby is empty.`; } diff --git a/src/routes/collections/launches/[id]/+page.svelte b/src/routes/collections/launches/[id]/+page.svelte index 5db1768..3019619 100644 --- a/src/routes/collections/launches/[id]/+page.svelte +++ b/src/routes/collections/launches/[id]/+page.svelte @@ -174,7 +174,7 @@
{stateLabel(server)}

{server.error ?? (server.state === 'queued' ? queueHint(server) : server.state === 'done' ? server.action === 'destroy' ? server.stage : 'Startup checks passed.' : server.stage)}

-
{server.action === 'destroy' ? 'Remove server' : server.port === null ? 'Create server · Port not assigned yet' : `Create server · Port ${server.port}`}{needsAttention(server) ? 'Review & recover' : 'View operation'}
+
{server.action === 'destroy' ? 'Destroy server' : server.port === null ? 'Create server · Port not assigned yet' : `Create server · Port ${server.port}`}{needsAttention(server) ? 'Review & recover' : 'View operation'}
{#if server.cancellation} diff --git a/src/routes/provisioning/+page.svelte b/src/routes/provisioning/+page.svelte index c8d7580..218c363 100644 --- a/src/routes/provisioning/+page.svelte +++ b/src/routes/provisioning/+page.svelte @@ -11,7 +11,7 @@ import Badge from '$ui/components/primitives/Badge.svelte'; import Button from '$ui/components/primitives/Button.svelte'; import EmptyState from '$ui/components/primitives/EmptyState.svelte'; - import HousePartyDialog from '$ui/components/HousePartyDialog.svelte'; + import FleetActionsDialog from '$ui/components/FleetActionsDialog.svelte'; import Freshness from '$ui/components/Freshness.svelte'; import RelativeTime from '$ui/components/RelativeTime.svelte'; @@ -35,8 +35,7 @@ let historyRefresh = $state(0); let loading = $state(true); let error = $state(''); - let housePartyOpen = $state(false); - let housePartyNotice = $state(''); + let fleetNotice = $state(''); let tab = $state('active'); let limit = $state(25); let offset = $state(0); @@ -75,7 +74,6 @@ if (!await confirmAction({ title: bulk ? `Cancel all ${ids.length} active operations?` : 'Cancel operation?', message: `${bulk ? `This affects all ${ids.length} cancellable active operations across the fleet, regardless of the current filters.` : `Cancel ${actionLabels[rows[0].action].toLowerCase()} on ${serverTitle(rows[0].server)}.`} Cancelling stops remaining work and health checks. Changes already made are not undone; commands already delivered to a host may still finish.`, confirmLabel: bulk ? 'Cancel all operations' : 'Cancel operation', danger: true })) return; - fleetActionsOpen = false; cancelling = true; cancellationNotice = ''; let cancelled = 0; const unavailable: string[] = []; for (const id of ids) { try { const response = await fetch(`/api/provisioning/${encodeURIComponent(id)}/cancel`, { method: 'POST' }); if (response.ok) cancelled++; else unavailable.push(id); } @@ -168,10 +166,7 @@ {#snippet actions()} -
Fleet actions
- {#if active.some(row => row.canCancel)}{/if} - -
+ {/snippet}
{#if $page.url.searchParams.has('returnTo')}{/if} @@ -184,8 +179,8 @@ {#if filtered}{/if} - { housePartyNotice = `Removal queued for ${count} server${count === 1 ? '' : 's'}.`; tab = 'active'; clearFilters(); void load(); }} /> - {#if housePartyNotice}

{housePartyNotice}

{/if} + { fleetNotice = `${actionLabels[action]} queued for ${count} server${count === 1 ? '' : 's'}.`; tab = 'active'; clearFilters(); void load(); }} /> + {#if fleetNotice}

{fleetNotice}

{/if} {#if cancellationNotice}

{cancellationNotice}

{/if}
{#if error && tab === 'active'}{/if} @@ -193,6 +188,7 @@ {#if tab === 'active'}

{filtered ? `${visibleActive.length} of ${active.length}` : active.length} active{#if queueSummary} · {queueSummary}{/if}

{:else}

History {historyTotal}

{/if}
+ {#if tab === 'active' && active.some(row => row.canCancel)}{/if} {#if needsReview}{/if} {#if tab === 'active'} void load()} retryDisabled={refreshing} /> {:else} historyRefresh++} retryDisabled={historyLoading} />{/if} @@ -263,6 +259,5 @@ .detail-row { background:var(--surface); }.operation-details { display:grid; gap:16px; line-height:1.6; overflow-wrap:anywhere; } dl { display:flex; flex-wrap:wrap; gap:12px 32px; margin:0; }dt { color:var(--text-tertiary); font-size:var(--text-xs); }dd { margin:0; }.mono { font-family:var(--font-mono); } .detail-links { display:flex; align-items:center; flex-wrap:wrap; gap:16px; }.error { color:var(--state-danger); }.warning { color:var(--state-warning); } - .fleet-actions { position:relative; font-size:var(--text-sm); }.fleet-actions summary { padding:10px; cursor:pointer; }.fleet-menu { position:absolute; right:0; top:100%; z-index:10; display:grid; gap:8px; padding:12px; background:var(--surface-overlay); border:1px solid var(--border-strong); border-radius:var(--radius-md); min-width:250px; box-shadow:var(--shadow-raised); } - @media(max-width:720px) { table { min-width:820px; }.search { flex-basis:100%; }th,td { padding:12px 8px; }.fleet-menu { right:auto; left:0; } } + @media(max-width:720px) { table { min-width:820px; }.search { flex-basis:100%; }th,td { padding:12px 8px; }} diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index ab51be4..94bd8ae 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -231,7 +231,7 @@ {#snippet actions()} {#if canOperate} {#if primaryAction}{/if} -
More actions
{#each availableActions.filter(action => action !== primaryAction) as action}{/each}
+
More actions
{#each availableActions.filter(action => action !== primaryAction) as action}{/each}
{/if} {/snippet} diff --git a/tests/unit/ui-fleet-actions.test.ts b/tests/unit/ui-fleet-actions.test.ts new file mode 100644 index 0000000..e1ac740 --- /dev/null +++ b/tests/unit/ui-fleet-actions.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render } from 'svelte/server'; +import type { FleetServer } from '../../src/lib/fleet-overview.js'; +import { fleetActionAvailable, queueFleetAction } from '../../src/lib/ui/fleet-actions.js'; +import FleetActionsDialog from '../../src/lib/ui/components/FleetActionsDialog.svelte'; + +const now = Date.parse('2026-10-07T12:00:00Z'); +const at = new Date(now - 1000).toISOString(); +const server: FleetServer = { + name: 't5_zombie_theater_1', game: 't5', map: 'zombie_theater', last_map: 'zombie_theater', + last_gametype: 'zclassic', last_players: 4, last_seen_at: new Date(now - 10_000).toISOString(), last_error: null, + container_state: 'running', container_checked_at: at, last_checked_at: at, + roster: [], health: 'online', held: [], active_since: null, inactive_since: null, + game_started_at: null, game_ended_at: null, host_id: 'host-1', + host: { id: 'host-1', name: 'Dallas 1', connected: true, connectionState: 'online', lastHeartbeat: at } +}; + +describe('fleet actions', () => { + it('uses the single-server eligibility rules and permits queued destruction with unknown status', () => { + expect(fleetActionAvailable(server, 'restart', now)).toBe(true); + expect(fleetActionAvailable(server, 'start', now)).toBe(false); + const stopped = { ...server, health: 'offline' as const, container_state: 'stopped' }; + expect(fleetActionAvailable(stopped, 'start', now)).toBe(true); + expect(fleetActionAvailable(stopped, 'restart', now)).toBe(false); + expect(fleetActionAvailable(server, 'stop', now + 300_000)).toBe(false); + expect(fleetActionAvailable({ ...server, host: null }, 'rebuild', now)).toBe(false); + expect(fleetActionAvailable({ ...server, host: null }, 'destroy', now)).toBe(true); + }); + + it.each(['restart', 'start', 'stop', 'rebuild'] as const)('queues %s only for selected servers and deduplicates selections', async action => { + const request = vi.fn().mockImplementation(async () => Response.json({ deploymentId: 'job-1' }, { status: 202 })); + const result = await queueFleetAction(action, [server.name, 'second', server.name], request); + expect(request.mock.calls.map(([url, options]) => [url, options.method, JSON.parse(options.body)])).toEqual([ + ['/api/provisioning', 'POST', { server: server.name, action }], + ['/api/provisioning', 'POST', { server: 'second', action }] + ]); + expect(result.queued.map(item => item.server)).toEqual([server.name, 'second']); + expect(result.failed).toEqual([]); + }); + + it('destroys only the chosen servers through the existing per-server endpoint', async () => { + const request = vi.fn().mockResolvedValue(Response.json({ deploymentId: 'destroy-1' }, { status: 202 })); + await queueFleetAction('destroy', ['server/a'], request); + expect(request).toHaveBeenCalledTimes(1); + expect(request).toHaveBeenCalledWith('/api/servers/server%2Fa', { method: 'DELETE' }); + }); + + it('reports partial failures and continues without retrying successful requests', async () => { + const request = vi.fn() + .mockResolvedValueOnce(Response.json({ error: 'Another job is pending.' }, { status: 409 })) + .mockResolvedValueOnce(Response.json({ deploymentId: 'job-2' }, { status: 202 })) + .mockRejectedValueOnce(new Error('offline')); + const result = await queueFleetAction('restart', ['blocked', 'ready', 'uncertain'], request); + expect(result.queued).toEqual([{ server: 'ready', deploymentId: 'job-2' }]); + expect(result.failed).toEqual([ + { server: 'blocked', error: 'Another job is pending.' }, + { server: 'uncertain', error: expect.stringContaining('Check Provisioning before trying again') } + ]); + expect(request).toHaveBeenCalledTimes(3); + }); + + it('does not report missing job IDs as successfully queued or submit an empty selection', async () => { + const request = vi.fn().mockResolvedValue(Response.json({}, { status: 202 })); + expect((await queueFleetAction('stop', ['one'], request)).queued).toEqual([]); + request.mockClear(); + expect(await queueFleetAction('stop', [], request)).toEqual({ queued: [], failed: [] }); + expect(request).not.toHaveBeenCalled(); + }); + + it('renders a named dialog with all five actions and a disabled empty selection', () => { + const body = render(FleetActionsDialog, { props: { open: true, onqueued: vi.fn() } }).body; + for (const action of ['Restart', 'Start', 'Stop', 'Rebuild', 'Destroy']) expect(body).toContain(`>${action}`); + expect(body).toContain('Fleet actions'); + expect(body).toContain('Select all eligible'); + expect(body).toContain('0 selected'); + const submit = [...body.matchAll(/]*>[\s\S]*?<\/button>/g)].find(match => match[0].includes('Restart 0 servers'))?.[0]; + expect(submit).toContain('disabled'); + }); +}); diff --git a/tests/unit/ui-lifecycle-actions.test.ts b/tests/unit/ui-lifecycle-actions.test.ts index 691c987..595cb65 100644 --- a/tests/unit/ui-lifecycle-actions.test.ts +++ b/tests/unit/ui-lifecycle-actions.test.ts @@ -59,7 +59,7 @@ describe('lifecycle impact copy', () => { it('does not confuse unknown or invalid player counts with an empty lobby', () => { for (const players of [null, -1, NaN, 1.5]) expect(lifecycleImpact('destroy', players, 30)) - .toBe('Player count is unknown. Removal waits for a live check confirming the lobby is empty.'); + .toBe('Player count is unknown. Destruction waits for a live check confirming the lobby is empty.'); expect(lifecycleImpact('rebuild', 0, 30)).toBe('The lobby is currently empty. A live check will confirm this before rebuild begins.'); }); @@ -89,14 +89,14 @@ describe('contextual lifecycle confirmation', () => { expect(body).not.toContain('Start the existing container'); }); - it('uses Remove server consistently and preserves recorded history in the explanation', () => { + it('uses Destroy server consistently and preserves recorded history in the explanation', () => { const body = render(DestroyServerModal, { props: { open: true, name: server.name, players: 4, round: 30 } }).body; - expect(body).toContain('Remove Kino Der Toten · Lobby 1?'); + expect(body).toContain('Destroy Kino Der Toten · Lobby 1?'); expect(body).toContain('4 players are in round 30.'); - expect(body).toContain('Remove server when empty'); + expect(body).toContain('Destroy server when empty'); expect(body).toContain('recorded history'); expect(body).not.toContain('campaign details'); - expect(actionLabels.destroy).toBe('Remove'); - expect(actionCompleted.destroy).toBe('Server removed'); + expect(actionLabels.destroy).toBe('Destroy'); + expect(actionCompleted.destroy).toBe('Server destroyed'); }); });