124 lines
11 KiB
Svelte
124 lines
11 KiB
Svelte
<script lang="ts">
|
||
import type { ProvisioningDetail } from '$lib/provisioning-detail.js';
|
||
import { provisioningGroups, progressIcons, progressStateLabels } from '$ui/provisioning-presentation.js';
|
||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||
import Badge from './primitives/Badge.svelte';
|
||
|
||
let { detail }: { detail: ProvisioningDetail } = $props();
|
||
const groups = $derived(provisioningGroups(detail));
|
||
const progress = $derived(detail.progress);
|
||
let expanded = $state<Record<string, boolean>>({});
|
||
const evidenceLabels = { pass: 'Passed', waiting: 'Waiting', fail: 'Failed', unknown: 'Unknown' };
|
||
const sources = { agent: 'Host agent', rcon: 'RCON', telemetry: 'Telemetry', combined: 'RCON and telemetry', manifest: 'Plutonium manifest' };
|
||
</script>
|
||
|
||
<section class="execution" aria-labelledby="execution-title">
|
||
<div class="section-heading"><div><span class="eyebrow">Execution</span><h2 id="execution-title">Progress and evidence</h2></div></div>
|
||
{#if !progress}
|
||
<div class="legacy"><h3>{detail.deployment.stage}</h3><p>Detailed checkpoints were not recorded for this operation. Its recorded outcome and original activity remain available below.</p></div>
|
||
{:else}
|
||
<ol class="phases">
|
||
{#each groups as group (group.key)}
|
||
<li class:current={group.state === 'current' || group.state === 'waiting'} class:failed={group.state === 'failed'}>
|
||
<details open={expanded[group.key] ?? ['current', 'waiting', 'failed'].includes(group.state)}>
|
||
<summary
|
||
onclick={event => { event.preventDefault(); expanded[group.key] = !(expanded[group.key] ?? ['current', 'waiting', 'failed'].includes(group.state)); }}
|
||
onkeydown={event => {
|
||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||
event.preventDefault();
|
||
if (!event.repeat) expanded[group.key] = !(expanded[group.key] ?? ['current', 'waiting', 'failed'].includes(group.state));
|
||
}}
|
||
>
|
||
<span class="phase-icon state-{group.state}" aria-hidden="true">{progressIcons[group.state]}</span>
|
||
<span class="phase-title">{group.label}</span>
|
||
<span class="phase-state state-{group.state}">{progressStateLabels[group.state]}</span>
|
||
</summary>
|
||
<ol class="steps">
|
||
{#each group.steps as step (step.key)}
|
||
<li aria-current={step.state === 'current' || step.state === 'waiting' ? 'step' : undefined}>
|
||
<span class="step-icon state-{step.state}" aria-hidden="true">{progressIcons[step.state]}</span>
|
||
<div><strong>{step.label}</strong><span class="step-meta state-{step.state}">{progressStateLabels[step.state]}{#if step.at} · <time datetime={step.at}>{$timeFormat.dateTime(step.at)}</time>{/if}</span></div>
|
||
</li>
|
||
{/each}
|
||
</ol>
|
||
</details>
|
||
</li>
|
||
{/each}
|
||
</ol>
|
||
{#if progress.command}
|
||
<div class="command" class:uncertain={progress.command.state === 'uncertain'}>
|
||
<h3>{progress.command.state === 'attempted' ? 'Command submission attempted' : progress.command.state === 'accepted' ? 'Remote request accepted' : progress.command.state === 'rejected' ? 'Remote request rejected' : 'Remote response uncertain'}</h3>
|
||
<p>{progress.command.message}</p>
|
||
<time datetime={progress.command.at}>{$timeFormat.dateTime(progress.command.at)}</time>
|
||
</div>
|
||
{/if}
|
||
{#if progress.checkpoints.some(checkpoint => !groups.some(group => group.steps.some(step => step.key === checkpoint.key)))}
|
||
<details class="milestones"><summary>Recorded checkpoints</summary><ul>
|
||
{#each progress.checkpoints.filter(checkpoint => !groups.some(group => group.steps.some(step => step.key === checkpoint.key))) as checkpoint (checkpoint.key)}
|
||
<li><strong>{checkpoint.label}</strong><time datetime={checkpoint.at}>{$timeFormat.dateTime(checkpoint.at)}</time></li>
|
||
{/each}
|
||
</ul></details>
|
||
{/if}
|
||
<div class="evidence-heading"><h3>Latest recorded checks</h3>{#if progress.observedAt}<span>Evaluated <time datetime={progress.observedAt}>{$timeFormat.dateTime(progress.observedAt)}</time></span>{/if}</div>
|
||
{#if progress.checks.length}
|
||
<div class="checks">
|
||
{#each progress.checks as check (check.key)}
|
||
<article class="check">
|
||
<div class="check-heading"><strong>{check.label}</strong><Badge variant={check.result === 'pass' ? 'online' : check.result === 'fail' ? 'danger' : check.result === 'waiting' ? 'warning' : 'neutral'}>{evidenceLabels[check.result]}</Badge></div>
|
||
{#if check.expected !== null || check.actual !== null}<dl>
|
||
{#if check.expected !== null}<div><dt>Expected</dt><dd>{check.expected}</dd></div>{/if}
|
||
<div><dt>Observed</dt><dd>{check.actual ?? 'Unknown'}</dd></div>
|
||
</dl>{/if}
|
||
{#if check.message}<p>{check.message}</p>{/if}
|
||
<div class="evidence-meta"><span>{check.source ? sources[check.source] : 'Source not recorded'}</span><span>Checked <time datetime={check.checkedAt}>{$timeFormat.dateTime(check.checkedAt)}</time></span>{#if check.receivedAt}<span>Evidence received <time datetime={check.receivedAt}>{$timeFormat.dateTime(check.receivedAt)}</time></span>{/if}</div>
|
||
</article>
|
||
{/each}
|
||
</div>
|
||
{:else}<p class="empty">No check evidence has been recorded for this operation yet.</p>{/if}
|
||
{#if detail.deployment.expected_map}<p class="map-note">Selected engine map: <code>{detail.deployment.expected_map}</code>. Engine-map verification does not independently verify a variant or start location.</p>{/if}
|
||
{/if}
|
||
</section>
|
||
|
||
<style>
|
||
.execution { display: grid; gap: var(--space-5); min-width: 0; }
|
||
h2, h3, p { margin: 0; }
|
||
h2 { font-size: var(--text-lg); letter-spacing: var(--tracking-tight); }
|
||
h3 { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
|
||
.eyebrow { display: block; margin-bottom: var(--space-2); font-size: var(--text-xs); color: var(--text-tertiary); }
|
||
.phases, .steps, .milestones ul { list-style: none; padding: 0; margin: 0; }
|
||
.phases { display: grid; gap: var(--space-3); }
|
||
.phases > li { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
|
||
.phases > li.current { border-color: color-mix(in srgb, var(--accent-primary) 60%, var(--border-subtle)); }
|
||
.phases > li.failed { border-color: color-mix(in srgb, var(--state-danger) 60%, var(--border-subtle)); }
|
||
summary { cursor: pointer; }
|
||
.phases summary { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-5); list-style: none; }
|
||
.phases summary::-webkit-details-marker { display: none; }
|
||
.phases summary::after { content: '+'; color: var(--text-tertiary); font-size: 18px; }
|
||
.phases details[open] > summary::after { content: '−'; }
|
||
summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: -3px; border-radius: var(--radius-sm); }
|
||
.phase-icon { flex: 0 0 26px; display: grid; place-items: center; width: 26px; height: 26px; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); font-size: var(--text-sm); }
|
||
.phase-title { flex: 1; font-size: var(--text-sm); font-weight: var(--weight-semibold); }
|
||
.phase-state { font-size: var(--text-xs); }
|
||
.steps { border-top: 1px solid var(--border-subtle); padding: var(--space-4) var(--space-5); display: grid; gap: var(--space-4); }
|
||
.steps li { display: flex; align-items: flex-start; gap: var(--space-3); }
|
||
.step-icon { flex: 0 0 26px; text-align: center; line-height: 1.5; }
|
||
.steps strong { display: block; font-size: var(--text-sm); font-weight: var(--weight-medium); line-height: 1.5; }
|
||
.step-meta { display: block; font-size: var(--text-xs); margin-top: var(--space-1); line-height: 1.6; }
|
||
.state-current { color: var(--accent-primary-hover); }.state-waiting { color: var(--state-warning); }.state-failed { color: var(--state-danger); }.state-done { color: var(--state-online); }
|
||
.state-unknown, .state-pending, .state-cancelled, time { color: var(--text-secondary); }
|
||
.command, .legacy { display: grid; gap: var(--space-2); padding: var(--space-4); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
|
||
.command.uncertain { border-color: color-mix(in srgb, var(--state-warning) 45%, var(--border-subtle)); }
|
||
p { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.65; overflow-wrap: anywhere; }
|
||
time, .evidence-meta, .evidence-heading span, .milestones { font-size: var(--text-xs); }
|
||
.evidence-heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2); padding-top: var(--space-2); }
|
||
.evidence-heading span { color: var(--text-secondary); }
|
||
.checks { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
|
||
.check { padding: var(--space-5); display: grid; gap: var(--space-3); }.check + .check { border-top: 1px solid var(--border-subtle); }
|
||
.check-heading { display: flex; gap: var(--space-3); align-items: center; justify-content: space-between; flex-wrap: wrap; }.check-heading strong { font-size: var(--text-sm); }
|
||
dl { display: flex; gap: var(--space-4) var(--space-8); margin: 0; flex-wrap: wrap; }dt { font-size: var(--text-xs); color: var(--text-tertiary); }dd { margin: var(--space-1) 0 0; font-size: var(--text-sm); overflow-wrap: anywhere; }
|
||
.evidence-meta { display: flex; gap: var(--space-1) var(--space-4); flex-wrap: wrap; color: var(--text-tertiary); line-height: 1.6; }
|
||
.milestones { border-block: 1px solid var(--border-subtle); padding: var(--space-3) 0; }.milestones summary { color: var(--text-secondary); }.milestones ul { display: grid; gap: var(--space-3); padding-top: var(--space-4); }.milestones li { display: grid; gap: var(--space-1); }.milestones strong { font-weight: var(--weight-medium); }
|
||
.map-note { font-size: var(--text-xs); }code { font-family: var(--font-mono); overflow-wrap: anywhere; }
|
||
@media (max-width: 600px) { .phases summary { padding: var(--space-4); gap: var(--space-2); flex-wrap: wrap; }.phase-title { flex-basis: calc(100% - 70px); }.phase-state { padding-left: 34px; order: 1; flex-basis: 100%; }.phases summary::after { order: 0; }.steps, .check { padding: var(--space-4); } }
|
||
</style>
|