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:
@@ -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}
|
||||
|
||||
@@ -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({}),
|
||||
|
||||
Reference in New Issue
Block a user