diff --git a/src/lib/ui/components/ProvisioningProgress.svelte b/src/lib/ui/components/ProvisioningProgress.svelte
index 4af5c53..78ded5c 100644
--- a/src/lib/ui/components/ProvisioningProgress.svelte
+++ b/src/lib/ui/components/ProvisioningProgress.svelte
@@ -20,14 +20,15 @@
{#if groups.length}
{#each groups as group (group.key)}
+ {@const defaultOpen = detail.deployment.state !== 'done' || group.steps.some(step => step.state === 'failed' || step.checks.some(check => check.result !== 'pass'))}
-
-
+
{ event.preventDefault(); expanded[group.key] = !(expanded[group.key] ?? true); }}
+ onclick={event => { event.preventDefault(); expanded[group.key] = !(expanded[group.key] ?? defaultOpen); }}
onkeydown={event => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
- if (!event.repeat) expanded[group.key] = !(expanded[group.key] ?? true);
+ if (!event.repeat) expanded[group.key] = !(expanded[group.key] ?? defaultOpen);
}}
>
{progressIcons[group.state]}
diff --git a/src/lib/ui/provisioning-presentation.ts b/src/lib/ui/provisioning-presentation.ts
index ac6ba75..a376662 100644
--- a/src/lib/ui/provisioning-presentation.ts
+++ b/src/lib/ui/provisioning-presentation.ts
@@ -125,6 +125,8 @@ export function provisioningOutcome(detail: ProvisioningDetail) {
const { deployment: operation, progress } = detail;
const active = activeProvisioningStates.includes(operation.state);
const failure = ['failed', 'interrupted'].includes(operation.state);
+ const checksSummary = operation.state === 'done' && progress?.checks.length && progress.checks.every(check => check.result === 'pass')
+ ? `All ${countLabel(progress.checks.length, 'check')} passed` : null;
const headline = operation.state === 'done' ? actionCompleted[operation.action]
: operation.state === 'cancelled' ? `${actionLabels[operation.action]} request cancelled`
: failure ? `${actionLabels[operation.action]} ${operation.state === 'interrupted' ? 'interrupted' : 'failed'}`
@@ -160,7 +162,7 @@ export function provisioningOutcome(detail: ProvisioningDetail) {
else description = operation.action === 'destroy' ? 'Removal is in progress. Confirmed checkpoints and remaining cleanup are recorded below.'
: operation.action === 'rebuild' ? 'The old instance is removed before a fresh instance is created in its reserved slot. Follow each checkpoint below.'
: 'The worker is executing this request and checking its resulting state.';
- return { active, headline, description };
+ return { active, headline, description, checksSummary };
}
/** Accept return context only from the local operation surfaces we expose. */
diff --git a/src/routes/provisioning/[id]/+page.svelte b/src/routes/provisioning/[id]/+page.svelte
index 5b7dd5c..515b5df 100644
--- a/src/routes/provisioning/[id]/+page.svelte
+++ b/src/routes/provisioning/[id]/+page.svelte
@@ -127,7 +127,10 @@
{#if deployment && detail && outcome}
{#if deployment.server_deleted_at}Deleted identity{/if}{#if detail.progress?.recovery?.state === 'held'}Recovery capacity held{/if}
Operation outcome
- {outcome.headline}
{outcome.description}
+
+
{outcome.headline}
{outcome.description}
+ {#if outcome.checksSummary}
✓{' '}{outcome.checksSummary}
{/if}
+
{#if deployment.error}Recorded error{deployment.error}
{/if}
{#if detail.activity.failureExcerpt && detail.activity.failureExcerpt.message !== deployment.error && ['failed', 'interrupted'].includes(deployment.state)}Last failure event · {detail.activity.failureExcerpt.message}
{/if}
{#if detail.warningMessages.length}Recorded warnings
{#each detail.warningMessages as warning}- {warning}
{/each}
{/if}
diff --git a/tests/unit/provisioning-presentation.test.ts b/tests/unit/provisioning-presentation.test.ts
index 67760ca..70781b2 100644
--- a/tests/unit/provisioning-presentation.test.ts
+++ b/tests/unit/provisioning-presentation.test.ts
@@ -20,8 +20,57 @@ function fixture(): ProvisioningDetail {
collection: null, progress: progress(), capacity: { evaluatedAt: at, pools: [] }, warningMessages: []
};
}
+function completedFixture(): ProvisioningDetail {
+ const detail = fixture();
+ detail.deployment.state = 'done';
+ detail.deployment.finished_at = at;
+ detail.progress = progress({ checks: (['container', 'game', 'map', 'plutonium-release', 'plutonium-version'] as const).map(key => ({
+ key, label: key, result: 'pass', source: 'agent', checkedAt: at, receivedAt: at,
+ expected: `Expected ${key}`, actual: `Observed ${key}`, message: null
+ })) });
+ return detail;
+}
describe('provisioning outcome and evidence presentation', () => {
+ it('summarizes recorded passed checks and retains their details in closed timeline groups', () => {
+ const detail = completedFixture();
+ expect(provisioningOutcome(detail).checksSummary).toBe('All 5 checks passed');
+ const body = render(ProvisioningProgressView, { props: { detail } }).body;
+ expect(body.match(/]*\bopen\b/);
+ for (const check of detail.progress!.checks) {
+ expect(body).toContain(check.expected);
+ expect(body).toContain(check.actual);
+ }
+ expect(body).toContain('Host agent');
+ expect(body).toContain('Evidence received');
+ detail.progress!.checks = detail.progress!.checks.slice(0, 1);
+ expect(provisioningOutcome(detail).checksSummary).toBe('All 1 check passed');
+ detail.progress!.checks = [];
+ expect(provisioningOutcome(detail).checksSummary).toBeNull();
+ detail.progress = null;
+ expect(provisioningOutcome(detail).checksSummary).toBeNull();
+ });
+
+ it.each(['fail', 'waiting', 'unknown'] as const)('keeps %s evidence visible even with a recorded completed outcome', result => {
+ const detail = completedFixture();
+ detail.progress!.checks[2].result = result;
+ expect(provisioningOutcome(detail).checksSummary).toBeNull();
+ const body = render(ProvisioningProgressView, { props: { detail } }).body;
+ const openGroups = body.match(/]*\bopen\b[^>]*>[\s\S]*?<\/details>/g);
+ expect(openGroups).toHaveLength(1);
+ expect(openGroups![0]).toContain('Observed map');
+ });
+
+ it.each(['queued', 'waiting', 'running', 'awaiting-health', 'warning', 'failed', 'interrupted', 'cancelled'])(
+ 'keeps %s evidence expanded without claiming the operation passed all checks', state => {
+ const detail = completedFixture();
+ detail.deployment.state = state;
+ expect(provisioningOutcome(detail).checksSummary).toBeNull();
+ const body = render(ProvisioningProgressView, { props: { detail } }).body;
+ expect(body.match(/]*\bopen\b/g)).toHaveLength(3);
+ });
+
it('shows recorded usernames and preserves unknown manual requesters', () => {
expect(provisioningRequester({ triggered_by: 'ui', requested_by: 'alice' })).toBe('alice');
expect(provisioningRequester({ triggered_by: 'rebalance:run', requested_by: 'bob' })).toBe('bob');