From e2e9ab36f778bdc2d773243578bc8d5ef0085e2c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 6 May 2026 10:18:59 -0400 Subject: [PATCH] feat(web/m7-coverage-gauge): inline coverage gauge in admin overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/src/routes/admin/+page.svelte | 47 +++++++++++++++++++++++++----- web/src/routes/admin/admin.test.ts | 6 ++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 09460146..129237c7 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -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(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 @@

Refetch cover art for albums that are missing one or where the previous attempt failed.

- +
+ + + {#if coverage} + {#if coverage.total === 0} + Library is empty. + {:else} +
+ {coverage.with_art.toLocaleString()} with art + · + 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 + + · + {coverage.settled.toLocaleString()} settled +
+ {/if} + {/if} +
{#if bulkResult}

{bulkResult}

{/if} diff --git a/web/src/routes/admin/admin.test.ts b/web/src/routes/admin/admin.test.ts index 90f34997..ea7a2949 100644 --- a/web/src/routes/admin/admin.test.ts +++ b/web/src/routes/admin/admin.test.ts @@ -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({}),