Limit provisioning step timing to progress and evidence
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user