Limit provisioning step timing to progress and evidence

This commit is contained in:
2026-10-09 14:27:51 -04:00
parent deb2d21f29
commit da623e8409
2 changed files with 17 additions and 14 deletions
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import OperationTime from '$ui/components/OperationTime.svelte'; import { timestampISO } from '$lib/time-zone.js';
import { timeFormat } from '$ui/stores/time-zone.js';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import type { ProvisioningDetail, ProvisioningEvent } from '$lib/provisioning-detail.js'; import type { ProvisioningDetail, ProvisioningEvent } from '$lib/provisioning-detail.js';
import { countLabel } from '$ui/plural.js'; import { countLabel } from '$ui/plural.js';
@@ -86,7 +87,7 @@
{#if loading && !loaded}<p class="muted" role="status">Loading activity…</p> {#if loading && !loaded}<p class="muted" role="status">Loading activity…</p>
{:else if loaded && !entries.length}<p class="empty">{appliedQuery ? 'No activity matches this search.' : 'No detailed activity recorded.'}</p> {:else if loaded && !entries.length}<p class="empty">{appliedQuery ? 'No activity matches this search.' : 'No detailed activity recorded.'}</p>
{:else}<ol class="timeline"> {:else}<ol class="timeline">
{#each entries as event (event.id)}<li><span class="time"><OperationTime start={detail.deployment.started_at} value={event.created_at} /></span><p>{event.message}</p></li>{/each} {#each entries as event (event.id)}<li><time class="time" datetime={timestampISO(event.created_at)}>{$timeFormat.dateTime(event.created_at)}</time><p>{event.message}</p></li>{/each}
</ol>{/if} </ol>{/if}
{#if loaded}<div class="footer"><span>Showing {entries.length} of {total} matching events</span>{#if cursor !== null}<Button size="sm" loading={loading} disabled={loading} onclick={() => load(true)}>Show more activity</Button>{/if}</div>{/if} {#if loaded}<div class="footer"><span>Showing {entries.length} of {total} matching events</span>{#if cursor !== null}<Button size="sm" loading={loading} disabled={loading} onclick={() => load(true)}>Show more activity</Button>{/if}</div>{/if}
</div> </div>
+14 -12
View File
@@ -23,8 +23,7 @@
import DeployServerDialog from '$ui/components/DeployServerDialog.svelte'; import DeployServerDialog from '$ui/components/DeployServerDialog.svelte';
import Freshness from '$ui/components/Freshness.svelte'; import Freshness from '$ui/components/Freshness.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte'; import RelativeTime from '$ui/components/RelativeTime.svelte';
import OperationTime from '$ui/components/OperationTime.svelte'; import { timestampISO, type Timestamp } from '$lib/time-zone.js';
import { timestampISO } from '$lib/time-zone.js';
import { timeFormat } from '$ui/stores/time-zone.js'; import { timeFormat } from '$ui/stores/time-zone.js';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -98,6 +97,11 @@
} }
</script> </script>
{#snippet dateTime(value: Timestamp)}
{@const iso = timestampISO(value)}
{#if iso}<time datetime={iso}>{$timeFormat.dateTime(value)}</time>{:else}<span>Not recorded</span>{/if}
{/snippet}
<svelte:head><title>{deployment ? `${actionLabels[deployment.action]} ${deployment.server}` : 'Provisioning'} | Electric Cherry</title></svelte:head> <svelte:head><title>{deployment ? `${actionLabels[deployment.action]} ${deployment.server}` : 'Provisioning'} | Electric Cherry</title></svelte:head>
<div class="page"> <div class="page">
@@ -128,10 +132,10 @@
{#if outcome.checksSummary}<p><span aria-hidden="true">✓</span>{' '}{outcome.checksSummary}</p>{/if} {#if outcome.checksSummary}<p><span aria-hidden="true">✓</span>{' '}{outcome.checksSummary}</p>{/if}
</div> </div>
{#if deployment.error}<div class="failure" role="alert"><strong>Recorded error</strong><p>{deployment.error}</p></div>{/if} {#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 · <OperationTime start={deployment.started_at} 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 · {@render dateTime(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.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} · <OperationTime start={deployment.started_at} 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} · {@render dateTime(detail.safetyOverride.confirmedAt)}.</p></div>{/if}
{#if detail.canResumeVerification || detail.canResumeCleanup || detail.canResumeRebuild || detail.canRetryWithRebuild || detail.canCancel || detail.canCancelCreation || canOverride} {#if detail.canResumeVerification || detail.canResumeCleanup || detail.canResumeRebuild || detail.canRetryWithRebuild || detail.canCancel || detail.canCancelCreation || canOverride}
<div class="recovery-actions"> <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><p class="action-help">{detail.safetyOverrideUnavailableReason ?? 'Confirm to bypass player safety checks for this operation.'}</p></div>{/if}
@@ -147,18 +151,18 @@
{#if view.actionError}<div class="action-error" role="alert"><strong>Request not confirmed</strong><p>{view.actionError}</p></div>{/if} {#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 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?.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 <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 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} {#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 class="timing">
{#if outcome.active}<div><span>Time in phase</span><strong>{elapsedTime(timestampISO(detail.progress?.phaseStartedAt), now)}</strong></div>{/if} {#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><span>Last evaluated</span>{@render dateTime(detail.progress?.observedAt)}</div>
</div> </div>
</section> </section>
{#key deployment.id} {#key deployment.id}
<div class="detail-grid"> <div class="detail-grid">
<div class="execution-column"> <div class="execution-column">
<div class="execution-progress"><p class="timing-note">Offsets below show time from the operation start (minutes:seconds or hours:minutes:seconds).</p><ProvisioningProgress {detail} hostHistoryHref={contextHref(`/hosts/operations?server=${encodeURIComponent(deployment.server)}`, $page.url)} /></div> <ProvisioningProgress {detail} hostHistoryHref={contextHref(`/hosts/operations?server=${encodeURIComponent(deployment.server)}`, $page.url)} />
<ProvisioningActivity {detail} /> <ProvisioningActivity {detail} />
</div> </div>
<aside class="context-column" aria-label="Operation context"> <aside class="context-column" aria-label="Operation context">
@@ -186,7 +190,7 @@
{#if detail.recipe} {#if detail.recipe}
{#if detail.blueprintName}<p>From {detail.blueprintName}</p>{/if} {#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>{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 <OperationTime start={deployment.started_at} value={detail.recipe.capturedAt} /></p> <p class="small">Captured {@render dateTime(detail.recipe.capturedAt)}</p>
<LaunchRecipe recipe={detail.recipe} historical selectionStatus={recipeStatus} /> <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> <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} {: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}
@@ -197,8 +201,8 @@
<dl class="metadata"> <dl class="metadata">
<div><dt>Operation ID</dt><dd><CopyValue value={deployment.id} label="operation ID" /></dd></div> <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} {#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><OperationTime start={deployment.started_at} value={deployment.submitted_at} missing="Not recorded" /></dd></div> <div><dt>Submission attempted</dt><dd>{@render dateTime(deployment.submitted_at)}</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} {#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.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} {#if detail.collection}<div><dt>Collection launch ID</dt><dd><CopyValue value={detail.collection.id} label="collection launch ID" /></dd></div>{/if}
{#each detail.sources as source (`${source.kind}:${source.source_id}`)}<div><dt>{source.label} · Source ID</dt><dd><CopyValue value={source.source_id} label={`${source.label} source ID`} /></dd></div>{/each} {#each detail.sources as source (`${source.kind}:${source.source_id}`)}<div><dt>{source.label} · Source ID</dt><dd><CopyValue value={source.source_id} label={`${source.label} source ID`} /></dd></div>{/each}
@@ -222,8 +226,6 @@
p { line-height: 1.65; overflow-wrap: anywhere; } p { line-height: 1.65; overflow-wrap: anywhere; }
.operation-header { display: grid; gap: var(--space-3); } .operation-header { display: grid; gap: var(--space-3); }
.operation-summary { color: var(--text-secondary); font-size: var(--text-sm); font-variant-numeric: tabular-nums; } .operation-summary { color: var(--text-secondary); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.execution-progress { display: grid; gap: var(--space-4); }
.timing-note { font-size: var(--text-xs); color: var(--text-tertiary); }
.badges, .diagnostics, .recovery-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; } .badges, .diagnostics, .recovery-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.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); } .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); }
.outcome.attention { border-color: color-mix(in srgb, var(--state-warning) 35%, var(--border-strong)); }.outcome h2 { font-size: var(--text-xl); line-height: 1.35; letter-spacing: var(--tracking-tight); overflow-wrap: anywhere; } .outcome.attention { border-color: color-mix(in srgb, var(--state-warning) 35%, var(--border-strong)); }.outcome h2 { font-size: var(--text-xl); line-height: 1.35; letter-spacing: var(--tracking-tight); overflow-wrap: anywhere; }