Trim provisioning job details (Refs #88)
This commit is contained in:
@@ -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 @@
|
||||
<section class="outcome" class:attention={['failed', 'interrupted', 'warning'].includes(deployment.state)} aria-labelledby="outcome-title">
|
||||
<div class="badges"><StatusBadge state={deployment.state} />{#if deployment.server_deleted_at}<Badge>Deleted identity</Badge>{/if}{#if detail.progress?.recovery?.state === 'held'}<Badge variant="warning">Recovery capacity held</Badge>{/if}</div>
|
||||
<div class="outcome-message" aria-live="polite" aria-atomic="true">
|
||||
<h2 id="outcome-title">{outcome.headline}</h2><p>{outcome.description}</p>
|
||||
<h2 id="outcome-title">{outcome.headline}</h2>{#if outcome.description}<p>{outcome.description}</p>{/if}
|
||||
{#if outcome.checksSummary}<p><span aria-hidden="true">✓</span>{' '}{outcome.checksSummary}</p>{/if}
|
||||
</div>
|
||||
{#if deployment.error}<div class="failure" role="alert"><strong>Recorded error</strong><p>{deployment.error}</p></div>{/if}
|
||||
@@ -135,13 +160,13 @@
|
||||
{#if detail.safetyOverride}<div class="warnings"><h3>Admin override enabled</h3><p>Player safety checks bypassed for this operation. Confirmed by {detail.safetyOverride.actorName} · {@render dateTime(detail.safetyOverride.confirmedAt)}.</p></div>{/if}
|
||||
{#if detail.canResumeVerification || detail.canResumeCleanup || detail.canResumeRebuild || detail.canRetryWithRebuild || detail.canCancel || detail.canCancelCreation || canOverride}
|
||||
<div class="recovery-actions">
|
||||
{#if canOverride}<div><Button variant="danger" disabled={!!view.busy || !!detail.safetyOverrideUnavailableReason} loading={view.busy === 'override'} onclick={overrideSafety}>Admin override…</Button><p class="action-help">{detail.safetyOverrideUnavailableReason ?? 'Confirm to bypass player safety checks for this operation.'}</p></div>{/if}
|
||||
{#if canOverride}<div><Button variant="danger" disabled={!!view.busy || !!detail.safetyOverrideUnavailableReason} loading={view.busy === 'override'} onclick={overrideSafety}>Admin override…</Button>{#if detail.safetyOverrideUnavailableReason}<p class="action-help">{detail.safetyOverrideUnavailableReason}</p>{/if}</div>{/if}
|
||||
{#if detail.canResumeVerification}<Button variant="primary" loading={view.busy === 'resume'} disabled={!!view.busy} onclick={() => controller.act('resume', 'Health verification requested. An unhealthy container may continue through this operation’s remaining recovery steps.')}>Resume health verification</Button>{/if}
|
||||
{#if detail.canRetryWithRebuild}<div><Button variant="primary" loading={view.busy === 'retry-rebuild'} disabled={!!view.busy} onclick={retryWithRebuild}>Retry with rebuild</Button><p class="action-help">Starts a new rebuild for the same lobby and port. Deletes instance files and uses the latest promoted setup.</p></div>{/if}
|
||||
{#if detail.canRetryWithRebuild}<Button variant="primary" loading={view.busy === 'retry-rebuild'} disabled={!!view.busy} onclick={retryWithRebuild}>Retry with rebuild</Button>{/if}
|
||||
{#if detail.canResumeRebuild}<Button variant="primary" loading={view.busy === 'resume'} disabled={!!view.busy} onclick={() => controller.act('resume', 'Rebuild cleanup requested. Fresh creation waits for local cleanup to finish.')}>Resume rebuild cleanup</Button>{/if}
|
||||
{#if detail.canResumeCleanup}<Button variant="primary" loading={view.busy === 'resume'} disabled={!!view.busy} onclick={() => controller.act('resume', 'Local cleanup requested. Remote container deletion will not be repeated.')}>Resume cleanup</Button>{/if}
|
||||
{#if detail.canCancel}<div><Button disabled={!!view.busy} loading={view.busy === 'cancel'} onclick={() => controller.act('cancel', 'Operation cancelled. Already-submitted commands may finish; no further steps will be started.')}>Cancel operation</Button><p class="action-help">Stops remaining work and health checks. Changes already made are not undone.</p></div>{/if}
|
||||
{#if detail.canCancelCreation}<div><Button variant="danger" disabled={!!view.busy} loading={view.busy === 'cancel-creation'} onclick={() => controller.act('cancel-creation', 'Creation cancelled. Follow cleanup below until removal is confirmed.')}>Cancel creation and remove server</Button><p class="action-help">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.</p></div>{/if}
|
||||
{#if detail.canCancel}<Button disabled={!!view.busy} loading={view.busy === 'cancel'} onclick={cancelOperation}>Cancel operation</Button>{/if}
|
||||
{#if detail.canCancelCreation}<Button variant="danger" disabled={!!view.busy} loading={view.busy === 'cancel-creation'} onclick={cancelCreation}>Cancel creation and remove server</Button>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if detail.cancellation}<div class="recovery-note"><strong>{detail.cancellation.state === 'done' ? 'Cancellation cleanup completed' : 'Cancellation cleanup'}</strong><p>{detail.cancellation.error ?? detail.cancellation.stage}</p><a href={`/provisioning/${encodeURIComponent(detail.cancellation.cleanupId)}`}>View cleanup progress →</a></div>{/if}
|
||||
@@ -150,10 +175,12 @@
|
||||
{#if detail.progress?.blocker?.href}<a class="dependency" href={detail.progress.blocker.href}>{detail.progress.blocker.label ?? 'View blocking work'} →</a>{/if}
|
||||
{#if detail.progress?.recovery}<div class="recovery-note"><strong>{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'}</strong><p>{detail.progress.recovery.message}</p><span>Recorded {@render dateTime(detail.progress.recovery.at)}</span>{#if detail.progress.recovery.operationId && detail.progress.recovery.operationId !== deployment.id}<a href={`/provisioning/${encodeURIComponent(detail.progress.recovery.operationId)}`}>View recovery operation →</a>{/if}</div>{/if}
|
||||
{#if serverHref && !['done', 'cancelled'].includes(deployment.state)}<div class="diagnostics"><a href={`${serverHref}?tab=logs`}>Open server logs</a><a href={`${serverHref}?tab=diagnostics`}>Diagnose connection</a>{#if detail.progress?.phase === 'verify-map' || detail.progress?.checks.some(check => check.key === 'map' && check.result !== 'pass')}<a href={`${serverHref}?tab=config`}>Review map configuration</a>{/if}</div>{/if}
|
||||
<div class="timing">
|
||||
{#if outcome.active}<div><span>Time in phase</span><strong>{elapsedTime(timestampISO(detail.progress?.phaseStartedAt), now)}</strong></div>{/if}
|
||||
<div><span>Last evaluated</span>{@render dateTime(detail.progress?.observedAt)}</div>
|
||||
</div>
|
||||
{#if (active && phaseStartedAt) || observedAt}
|
||||
<div class="timing">
|
||||
{#if active && phaseStartedAt}<div><span>Time in phase</span><strong>{elapsedTime(phaseStartedAt, now)}</strong></div>{/if}
|
||||
{#if observedAt}<div><span>Last evaluated</span>{@render dateTime(observedAt)}</div>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#key deployment.id}
|
||||
@@ -166,37 +193,36 @@
|
||||
{#if outcome.active && detail.workerActivity}
|
||||
<Card as="section" padding="md" ariaLabel="Controller activity"><div class="context-section">
|
||||
<h2>Controller activity</h2>
|
||||
<p>Current work and elapsed time. Background scans run independently of provisioning.</p>
|
||||
{#if detail.workerActivity.length}<ul class="related">
|
||||
{#each detail.workerActivity as work (work.key)}<li><strong>{work.label}</strong><span>{elapsedTime(work.startedAt, Date.parse(detail.generatedAt))}</span></li>{/each}
|
||||
</ul>{:else}<p>Waiting for the next provisioning pass.</p>{/if}
|
||||
</div></Card>
|
||||
{/if}
|
||||
{#if detail.sources.length || detail.collection}<Card as="section" padding="md" ariaLabel="Related work"><div class="context-section"><h2>Related work</h2><p>The requesting work may have further checks after this operation finishes.</p><ul class="related">
|
||||
{#if detail.sources.length || detail.collection}<Card as="section" padding="md" ariaLabel="Related work"><div class="context-section"><h2>Related work</h2><ul class="related">
|
||||
{#if detail.collection}<li><a href={`/collections/launches/${encodeURIComponent(detail.collection.id)}`}>{detail.collection.name}</a><span>Collection launch</span></li>{/if}
|
||||
{#each detail.sources as source (`${source.kind}:${source.source_id}`)}<li>{#if source.href}<a href={source.href}>{source.label}</a>{:else}<strong>{source.label}</strong><span>Source record no longer available</span>{/if}</li>{/each}
|
||||
</ul></div></Card>{/if}
|
||||
{#if detail.capacity.pools.length}<Card as="section" padding="md" ariaLabel="Capacity"><div class="context-section"><h2>Capacity</h2>
|
||||
{#each detail.capacity.pools as pool (pool.kind)}<div class="capacity-pool"><div class="capacity-heading"><strong>{pool.kind === 'startup' ? 'Startup slots' : 'Restart recovery slots'}</strong><span>{pool.used} / {pool.limit} in use</span></div>
|
||||
{#if pool.ownsSlot}<p>This operation holds a slot.</p>{/if}
|
||||
{#if pool.requestOrder !== null}<p>Request order: {pool.requestOrder}. Eligible requests may pass blocked work; this is not a guaranteed start order.</p>{/if}
|
||||
{#if pool.requestOrder !== null}<p title="Eligible requests may pass blocked work; this is not a guaranteed start order.">Request order: {pool.requestOrder}</p>{/if}
|
||||
{#if pool.holders.length}<details><summary>Current slot holders</summary><ul>{#each pool.holders as holder}<li>{#if holder.id}<a href={`/provisioning/${encodeURIComponent(holder.id)}`}>{serverTitle(holder.server)}</a>{:else}<span>{serverTitle(holder.server)} · operation not recorded</span>{/if}</li>{/each}</ul></details>{/if}
|
||||
</div>{/each}<p class="small">Capacity snapshot <RelativeTime value={detail.capacity.evaluatedAt} missing="Not recorded" /></p>
|
||||
</div></Card>{/if}
|
||||
<Card as="section" padding="md" ariaLabel="Launch files"><div class="context-section recipe"><h2>Launch files</h2>
|
||||
{#if detail.recipe || ['create', 'rebuild'].includes(deployment.action)}<Card as="section" padding="md" ariaLabel="Launch files"><div class="context-section recipe"><h2>Launch files</h2>
|
||||
{#if detail.recipe}
|
||||
{#if detail.blueprintName}<p>From {detail.blueprintName}</p>{/if}
|
||||
<p class="small">{recipeSelection} · <RelativeTime value={detail.recipe.capturedAt} missing="Capture time not recorded" /></p>
|
||||
<LaunchRecipe recipe={detail.recipe} historical />
|
||||
{:else}<p>{['create', 'rebuild'].includes(deployment.action) ? 'No launch recipe was recorded for this operation.' : 'This operation uses the existing instance. No new launch files were selected.'}</p>{/if}
|
||||
</div></Card>
|
||||
{:else}<p>No launch recipe was recorded for this operation.</p>{/if}
|
||||
</div></Card>{/if}
|
||||
<details class="technical-details">
|
||||
<summary>Technical details</summary>
|
||||
<div class="technical-content">
|
||||
<dl class="metadata">
|
||||
<div><dt>Operation ID</dt><dd><CopyValue value={deployment.id} label="operation ID" /></dd></div>
|
||||
{#if !active || finishedAt}<div><dt>Finished</dt><dd>{#if finishedAt}<time datetime={finishedAt}>{$timeFormat.dateTime(finishedAt)}</time>{:else}Not recorded{/if}</dd></div>{/if}
|
||||
<div><dt>Submission attempted</dt><dd>{@render dateTime(deployment.submitted_at)}</dd></div>
|
||||
{#if finishedAt}<div><dt>Finished</dt><dd>{@render dateTime(finishedAt)}</dd></div>{/if}
|
||||
{#if submittedAt}<div><dt>Submission attempted</dt><dd>{@render dateTime(submittedAt)}</dd></div>{/if}
|
||||
{#if deployment.server_deleted_at}<div><dt>Identity deleted</dt><dd>{@render dateTime(deployment.server_deleted_at)}</dd></div>{/if}
|
||||
{#if detail.agentOperation}<div><dt>Agent command ID</dt><dd><CopyValue value={detail.agentOperation.id} label="agent command ID" /></dd></div>{/if}
|
||||
{#if detail.collection}<div><dt>Collection launch ID</dt><dd><CopyValue value={detail.collection.id} label="collection launch ID" /></dd></div>{/if}
|
||||
|
||||
Reference in New Issue
Block a user