feat(web/m7-coverage-gauge): coverage API client + query
Adds CoverageRollup type, getCoverageRollup() against /api/admin/library/coverage, and createCoverageQuery() with the same 3s/30s pacing used by scan-status. New qk.coverage() key follows the existing zero-arg tuple pattern. Vitest covers the GET path and the empty-library zero-state plus the with_art + pending + settled = total invariant.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getCoverageRollup, type CoverageRollup } from './admin';
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
api: { get: vi.fn(), post: vi.fn() }
|
||||
}));
|
||||
|
||||
import { api } from './client';
|
||||
|
||||
describe('admin coverage API', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('getCoverageRollup GETs the correct path', async () => {
|
||||
const sample: CoverageRollup = {
|
||||
total: 18026,
|
||||
with_art: 12400,
|
||||
pending: 5400,
|
||||
settled: 226,
|
||||
pending_no_mbid: 320
|
||||
};
|
||||
(api.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(sample);
|
||||
const got = await getCoverageRollup();
|
||||
expect(api.get).toHaveBeenCalledWith('/api/admin/library/coverage');
|
||||
expect(got).toEqual(sample);
|
||||
});
|
||||
|
||||
it('returns zeros on an empty library', async () => {
|
||||
const empty: CoverageRollup = {
|
||||
total: 0,
|
||||
with_art: 0,
|
||||
pending: 0,
|
||||
settled: 0,
|
||||
pending_no_mbid: 0
|
||||
};
|
||||
(api.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(empty);
|
||||
const got = await getCoverageRollup();
|
||||
expect(got.total).toBe(0);
|
||||
expect(got.with_art + got.pending + got.settled).toBe(got.total);
|
||||
});
|
||||
});
|
||||
@@ -236,3 +236,29 @@ export function createScanStatusQuery() {
|
||||
refetchInterval: 3_000
|
||||
});
|
||||
}
|
||||
|
||||
// Library coverage --------------------------------------------------------
|
||||
|
||||
export type CoverageRollup = {
|
||||
total: number;
|
||||
with_art: number;
|
||||
pending: number;
|
||||
settled: number;
|
||||
pending_no_mbid: number;
|
||||
};
|
||||
|
||||
export async function getCoverageRollup(): Promise<CoverageRollup> {
|
||||
return api.get<CoverageRollup>('/api/admin/library/coverage');
|
||||
}
|
||||
|
||||
// Same pacing as scan-status — 3s while polling, 30s stale-time. The
|
||||
// query is cheap server-side (one aggregate per call) and we want
|
||||
// the gauge to tick during a scan.
|
||||
export function createCoverageQuery() {
|
||||
return createQuery({
|
||||
queryKey: qk.coverage(),
|
||||
queryFn: getCoverageRollup,
|
||||
staleTime: 30_000,
|
||||
refetchInterval: 3_000
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export const qk = {
|
||||
adminQuarantineActions: (limit?: number) =>
|
||||
['adminQuarantineActions', { limit: limit ?? 50 }] as const,
|
||||
scanStatus: () => ['scanStatus'] as const,
|
||||
coverage: () => ['coverage'] as const,
|
||||
suggestions: (limit?: number) =>
|
||||
['suggestions', { limit: limit ?? 12 }] as const,
|
||||
home: () => ['home'] as const,
|
||||
|
||||
Reference in New Issue
Block a user