feat(web/m7-coverage-gauge): inline coverage gauge in admin overview

Adds a library-wide cover-art coverage gauge inline to the right of
the "Refetch missing covers" button. Three buckets: with_art ·
pending · settled. Native HTML title= tooltip on "pending" surfaces
the pending_no_mbid sub-count when > 0, telling the operator how
many "pending" rows are blocked on missing MBID and won't be moved
by another scan.

Run-scan and Refetch-missing handlers extend their TanStack
invalidation to also clear qk.coverage(), so the gauge ticks
immediately after the operator clicks instead of waiting up to 3s
for the next refetch interval.

flex-wrap on the row so the gauge drops below the button on narrow
viewports instead of overflowing.

The existing admin page vitest mock for $lib/api/admin gains a
createCoverageQuery stub returning data: undefined so the new
import doesn't break the existing page tests (which don't touch
the gauge).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 10:18:59 -04:00
parent 16527cb509
commit e2e9ab36f7
2 changed files with 45 additions and 8 deletions
+39 -8
View File
@@ -7,6 +7,7 @@
createLidarrConfigQuery,
createAdminQuarantineQuery,
createScanStatusQuery,
createCoverageQuery,
approveRequest,
rejectRequest,
resolveQuarantine,
@@ -186,6 +187,11 @@
const scan = $derived(scanStatusQ.data);
const scanInFlight = $derived(scan?.in_flight === true);
// ---- Cover-art coverage gauge ----
const coverageStore = $derived(createCoverageQuery());
const coverageQ = $derived($coverageStore);
const coverage = $derived(coverageQ.data);
let triggering = $state(false);
let triggerResult = $state<string | null>(null);
@@ -197,6 +203,7 @@
await triggerScan();
triggerResult = 'Scan started.';
await client.invalidateQueries({ queryKey: qk.scanStatus() });
await client.invalidateQueries({ queryKey: qk.coverage() });
} catch (e) {
const code = (e as { code?: string; status?: number })?.code ?? 'unknown';
const status = (e as { status?: number })?.status;
@@ -226,6 +233,7 @@
try {
const { queued } = await refetchMissingCovers();
bulkResult = `Queued ${queued} albums for cover refetch.`;
await client.invalidateQueries({ queryKey: qk.coverage() });
} catch (e) {
bulkResult = `Failed: ${(e as { code?: string })?.code ?? 'unknown'}`;
} finally {
@@ -400,14 +408,37 @@
<p class="mt-1 text-sm text-text-secondary">
Refetch cover art for albums that are missing one or where the previous attempt failed.
</p>
<button
type="button"
onclick={onBulkRefetch}
disabled={bulkBusy}
class="mt-3 flex h-8 items-center gap-1 rounded-md bg-action-primary px-4 text-sm text-action-fg hover:opacity-90 disabled:opacity-50"
>
{bulkBusy ? 'Queueing…' : 'Refetch missing covers'}
</button>
<div class="mt-3 flex flex-wrap items-center gap-4">
<button
type="button"
onclick={onBulkRefetch}
disabled={bulkBusy}
class="flex h-8 items-center gap-1 rounded-md bg-action-primary px-4 text-sm text-action-fg hover:opacity-90 disabled:opacity-50"
>
{bulkBusy ? 'Queueing…' : 'Refetch missing covers'}
</button>
{#if coverage}
{#if coverage.total === 0}
<span class="text-sm text-text-muted">Library is empty.</span>
{:else}
<div class="flex items-center gap-3 text-sm">
<span>{coverage.with_art.toLocaleString()} with art</span>
<span class="text-text-muted">·</span>
<span
class={coverage.pending_no_mbid > 0 ? 'cursor-help' : ''}
title={coverage.pending_no_mbid > 0
? `${coverage.pending_no_mbid.toLocaleString()} of these have no MBID and won't be helped by another scan fix tags or merge duplicate albums.`
: undefined}
>
{coverage.pending.toLocaleString()} pending
</span>
<span class="text-text-muted">·</span>
<span>{coverage.settled.toLocaleString()} settled</span>
</div>
{/if}
{/if}
</div>
{#if bulkResult}
<p class="mt-2 text-sm">{bulkResult}</p>
{/if}
+6
View File
@@ -28,6 +28,12 @@ vi.mock('$lib/api/admin', async () => {
isPending: false,
isError: false
}),
createCoverageQuery: () =>
readable({
data: undefined,
isPending: false,
isError: false
}),
approveRequest: vi.fn().mockResolvedValue({}),
rejectRequest: vi.fn().mockResolvedValue({}),
resolveQuarantine: vi.fn().mockResolvedValue({}),