|
|
|
@@ -23,6 +23,9 @@
|
|
|
|
|
import DeployServerDialog from '$ui/components/DeployServerDialog.svelte';
|
|
|
|
|
import Freshness from '$ui/components/Freshness.svelte';
|
|
|
|
|
import RelativeTime from '$ui/components/RelativeTime.svelte';
|
|
|
|
|
import OperationTime from '$ui/components/OperationTime.svelte';
|
|
|
|
|
import { timestampISO } from '$lib/time-zone.js';
|
|
|
|
|
import { timeFormat } from '$ui/stores/time-zone.js';
|
|
|
|
|
|
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
|
const initial = untrack(() => data.detail);
|
|
|
|
@@ -36,6 +39,8 @@
|
|
|
|
|
const controller = createProvisioningRefresh<ProvisioningDetail>({ validate: isProvisioningDetail, onChange: value => { view = value; } });
|
|
|
|
|
const detail = $derived(view.detail);
|
|
|
|
|
const deployment = $derived(detail?.deployment);
|
|
|
|
|
const startedAt = $derived(timestampISO(deployment?.started_at));
|
|
|
|
|
const finishedAt = $derived(timestampISO(deployment?.finished_at));
|
|
|
|
|
const canOverride = $derived(data.user?.role === 'admin' && detail?.canOverrideSafety);
|
|
|
|
|
const outcome = $derived(detail ? provisioningOutcome(detail) : null);
|
|
|
|
|
const returnTo = $derived.by(() => {
|
|
|
|
@@ -101,6 +106,16 @@
|
|
|
|
|
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Activity', href: '/activity' }, { label: 'Operation' }]} />{/snippet}
|
|
|
|
|
{#snippet actions()}{#if serverHref}<Button href={serverHref}>View server</Button>{/if}{/snippet}
|
|
|
|
|
</SectionHeader>
|
|
|
|
|
{#if deployment && outcome}
|
|
|
|
|
<div class="operation-summary">
|
|
|
|
|
<p>
|
|
|
|
|
<span>Requested by {provisioningRequester(deployment)}</span>{' · '}
|
|
|
|
|
<span>Started {#if startedAt}<time datetime={startedAt}>{$timeFormat.dateTime(startedAt)}</time>{:else}Not recorded{/if}</span>{' · '}
|
|
|
|
|
<span>{#if finishedAt}Finished <time datetime={finishedAt}>{$timeFormat.dateTime(finishedAt)}</time>{:else}{outcome.active ? 'In progress' : 'Finished: Not recorded'}{/if}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p class="timing-note">Offsets below show time from the operation start (minutes:seconds or hours:minutes:seconds).</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="identity-row">
|
|
|
|
|
{#if deployment}<div class="operation-id"><span>Operation</span><CopyValue value={deployment.id} label="operation ID" /></div>{/if}
|
|
|
|
|
<div class="identity-links"><a href={returnTo.href}>← {returnTo.label}</a><Button size="sm" variant="ghost" onclick={copyLink}>Copy link</Button>{#if copyStatus}<span role="status">{copyStatus}</span>{/if}</div>
|
|
|
|
@@ -114,10 +129,10 @@
|
|
|
|
|
<div class="outcome-top"><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><span class="record-label">Operation outcome</span></div>
|
|
|
|
|
<div class="outcome-message" aria-live="polite" aria-atomic="true"><h2 id="outcome-title">{outcome.headline}</h2><p>{outcome.description}</p></div>
|
|
|
|
|
{#if deployment.error}<div class="failure" role="alert"><strong>Recorded error</strong><p>{deployment.error}</p></div>{/if}
|
|
|
|
|
{#if detail.activity.failureExcerpt && detail.activity.failureExcerpt.message !== deployment.error && ['failed', 'interrupted'].includes(deployment.state)}<p class="failure-excerpt"><span>Last failure event · <RelativeTime value={detail.activity.failureExcerpt.created_at} /></span>{detail.activity.failureExcerpt.message}</p>{/if}
|
|
|
|
|
{#if detail.activity.failureExcerpt && detail.activity.failureExcerpt.message !== deployment.error && ['failed', 'interrupted'].includes(deployment.state)}<p class="failure-excerpt"><span>Last failure event · <OperationTime start={deployment.started_at} value={detail.activity.failureExcerpt.created_at} /></span>{detail.activity.failureExcerpt.message}</p>{/if}
|
|
|
|
|
{#if detail.warningMessages.length}<div class="warnings"><h3>Recorded warnings</h3><ul>{#each detail.warningMessages as warning}<li>{warning}</li>{/each}</ul></div>{/if}
|
|
|
|
|
|
|
|
|
|
{#if detail.safetyOverride}<div class="warnings"><h3>Admin override enabled</h3><p>Player safety checks bypassed for this operation. Confirmed by {detail.safetyOverride.actorName} · <RelativeTime value={detail.safetyOverride.confirmedAt} />.</p></div>{/if}
|
|
|
|
|
{#if detail.safetyOverride}<div class="warnings"><h3>Admin override enabled</h3><p>Player safety checks bypassed for this operation. Confirmed by {detail.safetyOverride.actorName} · <OperationTime start={deployment.started_at} value={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}
|
|
|
|
@@ -133,12 +148,12 @@
|
|
|
|
|
{#if view.actionError}<div class="action-error" role="alert"><strong>Request not confirmed</strong><p>{view.actionError}</p></div>{/if}
|
|
|
|
|
{#if view.notice}<p class="notice" role="status">{view.notice}</p>{/if}
|
|
|
|
|
{#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 <RelativeTime value={detail.progress.recovery.at} missing="Not recorded" /></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 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 <OperationTime start={deployment.started_at} value={detail.progress.recovery.at} missing="Not recorded" /></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">
|
|
|
|
|
<div><span>{outcome.active ? 'Total elapsed' : 'Recorded duration'}</span><strong>{elapsedTime(deployment.started_at, deployment.finished_at ? Date.parse(deployment.finished_at) : now)}</strong></div>
|
|
|
|
|
{#if outcome.active}<div><span>Time in phase</span><strong>{elapsedTime(detail.progress?.phaseStartedAt, now)}</strong></div>{/if}
|
|
|
|
|
<div><span>Last evaluated</span><RelativeTime value={detail.progress?.observedAt} missing="Not recorded" /></div>
|
|
|
|
|
<div><span>{outcome.active ? 'Total elapsed' : 'Recorded duration'}</span><strong>{finishedAt || outcome.active ? elapsedTime(startedAt, finishedAt ? Date.parse(finishedAt) : now) : 'Not recorded'}</strong></div>
|
|
|
|
|
{#if outcome.active}<div><span>Time in phase</span><strong>{elapsedTime(timestampISO(detail.progress?.phaseStartedAt), now)}</strong></div>{/if}
|
|
|
|
|
<div><span>Last evaluated</span><OperationTime start={deployment.started_at} value={detail.progress?.observedAt} missing="Not recorded" /></div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
@@ -151,8 +166,8 @@
|
|
|
|
|
<h2>Host agent execution</h2>
|
|
|
|
|
<dl class="metadata">
|
|
|
|
|
<div><dt>Agent state</dt><dd>{detail.agentOperation.state}</dd></div>
|
|
|
|
|
<div><dt>Command queued</dt><dd><RelativeTime value={detail.agentOperation.createdAt} missing="Not recorded" /></dd></div>
|
|
|
|
|
<div><dt>Latest agent report</dt><dd><RelativeTime value={detail.agentOperation.updatedAt} missing="Not recorded" /></dd></div>
|
|
|
|
|
<div><dt>Command queued</dt><dd><OperationTime start={deployment.started_at} value={detail.agentOperation.createdAt} missing="Not recorded" /></dd></div>
|
|
|
|
|
<div><dt>Latest agent report</dt><dd><OperationTime start={deployment.started_at} value={detail.agentOperation.updatedAt} missing="Not recorded" /></dd></div>
|
|
|
|
|
<div><dt>Command ID</dt><dd><code>{detail.agentOperation.id}</code></dd></div>
|
|
|
|
|
</dl>
|
|
|
|
|
{#if detail.agentOperation.error}<p>{detail.agentOperation.error}</p>{/if}
|
|
|
|
@@ -171,11 +186,8 @@
|
|
|
|
|
{/if}
|
|
|
|
|
<Card as="section" padding="md" ariaLabel="Operation details">
|
|
|
|
|
<div class="context-section"><h2>Operation details</h2><dl class="metadata">
|
|
|
|
|
<div><dt>Requester</dt><dd>{provisioningRequester(deployment)}</dd></div>
|
|
|
|
|
<div><dt>Requested at</dt><dd><RelativeTime value={deployment.started_at} /></dd></div>
|
|
|
|
|
<div><dt>Submission attempted</dt><dd><RelativeTime value={deployment.submitted_at} missing="Not recorded" /></dd></div>
|
|
|
|
|
<div><dt>Finished</dt><dd><RelativeTime value={deployment.finished_at} missing={outcome.active ? 'In progress' : 'Not recorded'} /></dd></div>
|
|
|
|
|
{#if deployment.server_deleted_at}<div><dt>Identity deleted</dt><dd><RelativeTime value={deployment.server_deleted_at} /></dd></div>{/if}
|
|
|
|
|
<div><dt>Submission attempted</dt><dd><OperationTime start={deployment.started_at} value={deployment.submitted_at} missing="Not recorded" /></dd></div>
|
|
|
|
|
{#if deployment.server_deleted_at}<div><dt>Identity deleted</dt><dd><OperationTime start={deployment.started_at} value={deployment.server_deleted_at} /></dd></div>{/if}
|
|
|
|
|
</dl></div>
|
|
|
|
|
</Card>
|
|
|
|
|
{#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">
|
|
|
|
@@ -193,7 +205,7 @@
|
|
|
|
|
{#if detail.recipe}
|
|
|
|
|
{#if detail.blueprintName}<p>From {detail.blueprintName}</p>{/if}
|
|
|
|
|
<p>{recipeStatus === 'queued' ? 'This capture does not pin versions. The latest promoted setup is selected again when creation begins.' : recipeStatus === 'attempted' ? 'These files were selected when creation began. Selection alone does not confirm successful installation.' : 'This is the saved recipe for this operation. The point at which it was selected was not recorded.'}</p>
|
|
|
|
|
<p class="small">Captured <RelativeTime value={detail.recipe.capturedAt} /></p>
|
|
|
|
|
<p class="small">Captured <OperationTime start={deployment.started_at} value={detail.recipe.capturedAt} /></p>
|
|
|
|
|
<LaunchRecipe recipe={detail.recipe} historical selectionStatus={recipeStatus} />
|
|
|
|
|
<div class="launch-another"><Button variant="ghost" onclick={() => launchOpen = true}>Launch another server →</Button><p>Creates a separate server with a fresh ID and port using the latest promoted files. It does not repair this instance.</p></div>
|
|
|
|
|
{: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}
|
|
|
|
@@ -212,6 +224,8 @@
|
|
|
|
|
h2, h3, p { margin: 0; }a { color: var(--accent-primary-hover); text-underline-offset: 3px; }
|
|
|
|
|
a:focus-visible, summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 3px; border-radius: 2px; }
|
|
|
|
|
p { line-height: 1.65; overflow-wrap: anywhere; }
|
|
|
|
|
.operation-summary { color: var(--text-secondary); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
|
|
|
|
|
.operation-summary .timing-note { margin-top: var(--space-2); font-size: var(--text-xs); }
|
|
|
|
|
.identity-row, .operation-id, .identity-links, .badges, .outcome-top, .diagnostics, .recovery-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
|
|
|
|
|
.identity-row { justify-content: space-between; margin-top: calc(-1 * var(--space-3)); gap: var(--space-2) var(--space-5); }.operation-id { gap: var(--space-2); min-width: 0; }.operation-id > span, .identity-links { font-size: var(--text-xs); color: var(--text-tertiary); }.identity-links { gap: var(--space-4); }
|
|
|
|
|
.outcome { display: grid; gap: var(--space-5); padding: var(--space-6); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface); }
|
|
|
|
|