feat(web/m7-scan-progress): StageBadge + 4-card layout updates

Each of the 4 stage cards in the admin Library Scan section gains a
StageBadge in its header. The badge derives state via stageState()
from the existing ScanStatus payload — no new query, no schema
change. Active stage shows spinner + "Processing" in moss green;
done shows check + "Done" in moss green; pending and skipped show
muted text.

Card body now shows either the (possibly partial) numbers OR a
"Waiting for previous stage…" placeholder when the badge is
pending. The previous bottom-of-card "Pending…" / "Skipped."
fallback text disappears (the badge replaces it).

StageBadge.test.ts covers the four state renderings.
admin.test.ts adds one new case asserting the Processing badge
shows when scan.library is populated and in_flight: true. Existing
admin.test.ts assertions still pass (the default mock data has no
stage tallies + in_flight: false → all stages skipped, no Loader/
Check icons rendered).
This commit is contained in:
2026-05-06 21:19:53 -04:00
parent a368c74a9a
commit 3b2d1009cd
4 changed files with 104 additions and 14 deletions
+26 -12
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { pageTitle } from '$lib/branding';
import { Disc3, Album, Music2, Check, X, RotateCcw, Trash2, Cloud, ChevronRight } from 'lucide-svelte';
import StageBadge from '$lib/components/StageBadge.svelte';
import { stageState } from './stage-state';
import { useQueryClient } from '@tanstack/svelte-query';
import {
createAdminRequestsQuery,
@@ -346,7 +348,10 @@
<div class="mt-3 grid gap-3 sm:grid-cols-4">
<!-- Library stage -->
<div class="rounded border border-border bg-bg p-3">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Library walk</div>
<div class="flex items-baseline justify-between gap-2">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Library walk</div>
<StageBadge state={stageState(scan, 'library')} />
</div>
{#if scan.library}
<dl class="mt-2 space-y-0.5 text-sm">
<div class="flex justify-between"><dt class="text-text-secondary">Scanned</dt><dd>{scan.library.scanned}</dd></div>
@@ -355,14 +360,17 @@
<div class="flex justify-between"><dt class="text-text-secondary">Skipped</dt><dd>{scan.library.skipped}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Errored</dt><dd>{scan.library.errored}</dd></div>
</dl>
{:else}
<p class="mt-2 text-sm text-text-muted">{scanInFlight ? 'Pending…' : 'Skipped.'}</p>
{:else if stageState(scan, 'library') === 'pending'}
<p class="mt-2 text-sm text-text-muted">Waiting for previous stage…</p>
{/if}
</div>
<!-- MBID backfill stage -->
<div class="rounded border border-border bg-bg p-3">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">MBID backfill</div>
<div class="flex items-baseline justify-between gap-2">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">MBID backfill</div>
<StageBadge state={stageState(scan, 'mbid_backfill')} />
</div>
{#if scan.mbid_backfill}
<dl class="mt-2 space-y-0.5 text-sm">
<div class="flex justify-between"><dt class="text-text-secondary">Processed</dt><dd>{scan.mbid_backfill.processed}</dd></div>
@@ -370,36 +378,42 @@
<div class="flex justify-between"><dt class="text-text-secondary">Skipped</dt><dd>{scan.mbid_backfill.skipped}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Duplicates</dt><dd>{scan.mbid_backfill.duplicates ?? 0}</dd></div>
</dl>
{:else}
<p class="mt-2 text-sm text-text-muted">{scanInFlight ? 'Pending…' : 'Skipped.'}</p>
{:else if stageState(scan, 'mbid_backfill') === 'pending'}
<p class="mt-2 text-sm text-text-muted">Waiting for previous stage…</p>
{/if}
</div>
<!-- Cover enrichment stage -->
<div class="rounded border border-border bg-bg p-3">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Cover enrichment</div>
<div class="flex items-baseline justify-between gap-2">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Cover enrichment</div>
<StageBadge state={stageState(scan, 'cover_enrich')} />
</div>
{#if scan.cover_enrich}
<dl class="mt-2 space-y-0.5 text-sm">
<div class="flex justify-between"><dt class="text-text-secondary">Processed</dt><dd>{scan.cover_enrich.processed}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Succeeded</dt><dd>{scan.cover_enrich.succeeded}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Failed</dt><dd>{scan.cover_enrich.failed}</dd></div>
</dl>
{:else}
<p class="mt-2 text-sm text-text-muted">{scanInFlight ? 'Pending…' : 'Skipped.'}</p>
{:else if stageState(scan, 'cover_enrich') === 'pending'}
<p class="mt-2 text-sm text-text-muted">Waiting for previous stage…</p>
{/if}
</div>
<!-- Artist art enrichment stage -->
<div class="rounded border border-border bg-bg p-3">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Artist art</div>
<div class="flex items-baseline justify-between gap-2">
<div class="text-xs font-medium uppercase tracking-wide text-text-muted">Artist art</div>
<StageBadge state={stageState(scan, 'artist_art_enrich')} />
</div>
{#if scan.artist_art_enrich}
<dl class="mt-2 space-y-0.5 text-sm">
<div class="flex justify-between"><dt class="text-text-secondary">Processed</dt><dd>{scan.artist_art_enrich.processed}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Succeeded</dt><dd>{scan.artist_art_enrich.succeeded}</dd></div>
<div class="flex justify-between"><dt class="text-text-secondary">Failed</dt><dd>{scan.artist_art_enrich.failed}</dd></div>
</dl>
{:else}
<p class="mt-2 text-sm text-text-muted">{scanInFlight ? 'Pending…' : 'Skipped.'}</p>
{:else if stageState(scan, 'artist_art_enrich') === 'pending'}
<p class="mt-2 text-sm text-text-muted">Waiting for previous stage…</p>
{/if}
</div>
</div>