From 967b2a1c8a77ae055bb4a609669c9637ba2fdaa9 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Fri, 9 Oct 2026 14:55:24 -0400 Subject: [PATCH] Trim provisioning job details (Refs #88) --- docs/guides/provisioning.md | 13 +++++ src/lib/ui/provisioning-presentation.ts | 8 +-- src/routes/provisioning/[id]/+page.svelte | 60 ++++++++++++++------ tests/unit/provisioning-presentation.test.ts | 5 +- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/docs/guides/provisioning.md b/docs/guides/provisioning.md index 43d50ed..03a27d3 100644 --- a/docs/guides/provisioning.md +++ b/docs/guides/provisioning.md @@ -170,6 +170,13 @@ Related work links back to the requesting campaign, beta, maintenance item, or collection. A completed provisioning job does not imply that its requester has finished its own verification. +Optional timestamps appear only when recorded. **Launch files** appears for Create +and Rebuild, or when an older operation has a recorded recipe. Start, Restart, +and Stop use the existing instance without selecting new launch files. + +While a job is active, **Controller activity** lists current work and elapsed +time. Background scans run independently of provisioning. + ### Verification evidence New operations record structured verification evidence alongside their activity: @@ -201,6 +208,12 @@ the replacement in its reserved slot. Resuming the same operation preserves its earlier failure and resume events. Uncertain commands have no generic Retry action. Cancelling an operation does not stop a command already delivered to the host. +The confirmation explains that remaining work and health checks stop, while +changes already made remain. **Cancel creation and remove server** also deletes +the launch's container and instance files, even if players have joined or their +status is unknown. Shared game files are kept, and the key returns to its game +pool after confirmed removal. + A later rebuild waits for that command to settle. Its waiting message includes the host command's state and last reported error; **View cancelled operation** opens the original command details. If an interrupted rebuild removed its old diff --git a/src/lib/ui/provisioning-presentation.ts b/src/lib/ui/provisioning-presentation.ts index 65acaca..bb6bf29 100644 --- a/src/lib/ui/provisioning-presentation.ts +++ b/src/lib/ui/provisioning-presentation.ts @@ -138,7 +138,7 @@ export function provisioningOutcome(detail: ProvisioningDetail) { : failure ? `${actionLabels[operation.action]} ${operation.state === 'interrupted' ? 'interrupted' : 'failed'}` : operation.state === 'warning' ? `${actionLabels[operation.action]} completed with warnings` : progress?.blocker?.message ?? operation.stage; - let description: string; + let description: string | null; if (detail.canResumeCleanup) description = 'The remote deployment was deleted. Resume the remaining local cleanup without repeating container removal.'; else if (detail.canResumeRebuild) description = 'The remote deployment was deleted. Resume local cleanup, then create a fresh instance in the reserved server slot.'; else if (detail.canResumeVerification) description = 'Resume readiness checks for the existing instance. An unhealthy container may continue through this operation’s remaining restart and rebuild recovery steps.'; @@ -152,11 +152,7 @@ export function provisioningOutcome(detail: ProvisioningDetail) { ? 'The remote response was uncertain. Inspect the recorded result and server before requesting another operation.' : 'Review the recorded failure and execution evidence before submitting another operation.'; else if (operation.state === 'warning') description = 'The operation finished. Review the recorded warnings below for any remaining issue.'; - else if (operation.state === 'done') description = !progress - ? 'This operation was recorded as completed. Detailed verification evidence was not recorded.' - : operation.action === 'destroy' ? 'The instance removal completed. This record preserves its results after deletion.' - : operation.action === 'stop' ? 'The container was confirmed stopped. Its files and registration were retained.' - : 'The operation completed its readiness checks. These recorded results describe that attempt; current health is available on the server page.'; + else if (operation.state === 'done') description = null; else if (active && progress?.phase === 'cleanup' && !progress.blocker) description = operation.action === 'rebuild' ? 'Remote deployment deletion was confirmed. Local cleanup must finish before a fresh instance is created in the reserved slot.' : 'Remote deployment deletion was confirmed. The remaining local files and registration cleanup will continue without repeating container removal.'; diff --git a/src/routes/provisioning/[id]/+page.svelte b/src/routes/provisioning/[id]/+page.svelte index 1dd4bd6..11f51ca 100644 --- a/src/routes/provisioning/[id]/+page.svelte +++ b/src/routes/provisioning/[id]/+page.svelte @@ -38,6 +38,9 @@ const deployment = $derived(detail?.deployment); const startedAt = $derived(timestampISO(deployment?.started_at)); const finishedAt = $derived(timestampISO(deployment?.finished_at)); + const submittedAt = $derived(timestampISO(deployment?.submitted_at)); + const observedAt = $derived(timestampISO(detail?.progress?.observedAt)); + const phaseStartedAt = $derived(timestampISO(detail?.progress?.phaseStartedAt)); const canOverride = $derived(data.user?.role === 'admin' && detail?.canOverrideSafety); const outcome = $derived(detail ? provisioningOutcome(detail) : null); const active = $derived(outcome?.active ?? false); @@ -83,6 +86,28 @@ if (confirmed && view.id === id && canOverride) await controller.act('override', 'Admin override enabled for this operation. Provisioning will proceed without player safety checks when the remaining requirements are met.', { confirmed: true }); } + async function cancelOperation() { + if (!deployment || !detail?.canCancel || view.busy) return; + const id = deployment.id; + const confirmed = await confirmAction({ + title: 'Cancel operation?', + message: `Stop remaining work and health checks for ${serverTitle(deployment.server)} (${deployment.server}). Changes already made are not undone. Commands already sent to the host may still finish.`, + confirmLabel: 'Cancel operation', danger: true + }); + if (confirmed && view.id === id && detail?.canCancel) await controller.act('cancel', + 'Operation cancelled. Already-submitted commands may finish; no further steps will be started.'); + } + async function cancelCreation() { + if (!deployment || !detail?.canCancelCreation || view.busy) return; + const id = deployment.id; + const confirmed = await confirmAction({ + title: 'Cancel creation and remove server?', + message: `Stop this launch and delete the container and instance files for ${serverTitle(deployment.server)} (${deployment.server}), even if player status is unknown or players have joined. Shared game files are kept. The key returns to its game pool after removal is confirmed.`, + confirmLabel: 'Cancel creation and remove server', danger: true + }); + if (confirmed && view.id === id && detail?.canCancelCreation) await controller.act('cancel-creation', + 'Creation cancelled. Follow cleanup below until removal is confirmed.'); + } async function copyLink() { copyStatus = ''; try { @@ -125,7 +150,7 @@
{#if deployment.server_deleted_at}Deleted identity{/if}{#if detail.progress?.recovery?.state === 'held'}Recovery capacity held{/if}
-

{outcome.headline}

{outcome.description}

+

{outcome.headline}

{#if outcome.description}

{outcome.description}

{/if} {#if outcome.checksSummary}

{' '}{outcome.checksSummary}

{/if}
{#if deployment.error}{/if} @@ -135,13 +160,13 @@ {#if detail.safetyOverride}

Admin override enabled

Player safety checks bypassed for this operation. Confirmed by {detail.safetyOverride.actorName} · {@render dateTime(detail.safetyOverride.confirmedAt)}.

{/if} {#if detail.canResumeVerification || detail.canResumeCleanup || detail.canResumeRebuild || detail.canRetryWithRebuild || detail.canCancel || detail.canCancelCreation || canOverride}
- {#if canOverride}

{detail.safetyOverrideUnavailableReason ?? 'Confirm to bypass player safety checks for this operation.'}

{/if} + {#if canOverride}
{#if detail.safetyOverrideUnavailableReason}

{detail.safetyOverrideUnavailableReason}

{/if}
{/if} {#if detail.canResumeVerification}{/if} - {#if detail.canRetryWithRebuild}

Starts a new rebuild for the same lobby and port. Deletes instance files and uses the latest promoted setup.

{/if} + {#if detail.canRetryWithRebuild}{/if} {#if detail.canResumeRebuild}{/if} {#if detail.canResumeCleanup}{/if} - {#if detail.canCancel}

Stops remaining work and health checks. Changes already made are not undone.

{/if} - {#if detail.canCancelCreation}

Stops this launch and deletes its container and instance files, even if player status is unknown or players have joined. Shared game files are kept. The key returns to its game pool after removal is confirmed.

{/if} + {#if detail.canCancel}{/if} + {#if detail.canCancelCreation}{/if}
{/if} {#if detail.cancellation}
{detail.cancellation.state === 'done' ? 'Cancellation cleanup completed' : 'Cancellation cleanup'}

{detail.cancellation.error ?? detail.cancellation.stage}

View cleanup progress →
{/if} @@ -150,10 +175,12 @@ {#if detail.progress?.blocker?.href}{detail.progress.blocker.label ?? 'View blocking work'} →{/if} {#if detail.progress?.recovery}
{detail.progress.recovery.state === 'held' ? 'Recovery remains in progress' : detail.progress.recovery.state === 'released' ? 'Recovery capacity released' : detail.progress.recovery.state === 'transferred' ? 'Recovery transferred' : 'Recovery state not confirmed'}

{detail.progress.recovery.message}

Recorded {@render dateTime(detail.progress.recovery.at)}{#if detail.progress.recovery.operationId && detail.progress.recovery.operationId !== deployment.id}View recovery operation →{/if}
{/if} {#if serverHref && !['done', 'cancelled'].includes(deployment.state)}
Open server logsDiagnose connection{#if detail.progress?.phase === 'verify-map' || detail.progress?.checks.some(check => check.key === 'map' && check.result !== 'pass')}Review map configuration{/if}
{/if} -
- {#if outcome.active}
Time in phase{elapsedTime(timestampISO(detail.progress?.phaseStartedAt), now)}
{/if} -
Last evaluated{@render dateTime(detail.progress?.observedAt)}
-
+ {#if (active && phaseStartedAt) || observedAt} +
+ {#if active && phaseStartedAt}
Time in phase{elapsedTime(phaseStartedAt, now)}
{/if} + {#if observedAt}
Last evaluated{@render dateTime(observedAt)}
{/if} +
+ {/if}
{#key deployment.id} @@ -166,37 +193,36 @@ {#if outcome.active && detail.workerActivity}

Controller activity

-

Current work and elapsed time. Background scans run independently of provisioning.

{#if detail.workerActivity.length}{:else}

Waiting for the next provisioning pass.

{/if}
{/if} - {#if detail.sources.length || detail.collection}

Related work

The requesting work may have further checks after this operation finishes.