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
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import { Loader2, Check } from 'lucide-svelte';
type Props = { state: 'pending' | 'in_progress' | 'done' | 'skipped' };
let { state }: Props = $props();
</script>
{#if state === 'in_progress'}
<span class="inline-flex items-center gap-1 text-xs text-action-primary">
<Loader2 size={12} class="animate-spin" />
Processing
</span>
{:else if state === 'done'}
<span class="inline-flex items-center gap-1 text-xs text-action-primary">
<Check size={12} />
Done
</span>
{:else if state === 'pending'}
<span class="text-xs text-text-muted">Pending</span>
{:else}
<span class="text-xs text-text-muted">Skipped</span>
{/if}
+31
View File
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import StageBadge from './StageBadge.svelte';
describe('StageBadge', () => {
it('renders Processing + spinner when state is in_progress', () => {
render(StageBadge, { props: { state: 'in_progress' } });
expect(screen.getByText('Processing')).toBeInTheDocument();
// The spinner is a lucide icon — just confirm a child SVG exists.
expect(document.querySelector('svg')).toBeTruthy();
});
it('renders Done + check when state is done', () => {
render(StageBadge, { props: { state: 'done' } });
expect(screen.getByText('Done')).toBeInTheDocument();
expect(document.querySelector('svg')).toBeTruthy();
});
it('renders Pending muted when state is pending', () => {
render(StageBadge, { props: { state: 'pending' } });
expect(screen.getByText('Pending')).toBeInTheDocument();
// No icon for the muted states.
expect(document.querySelector('svg')).toBeFalsy();
});
it('renders Skipped muted when state is skipped', () => {
render(StageBadge, { props: { state: 'skipped' } });
expect(screen.getByText('Skipped')).toBeInTheDocument();
expect(document.querySelector('svg')).toBeFalsy();
});
});