diff --git a/web/src/lib/api/admin.ts b/web/src/lib/api/admin.ts index e7012071..ece4d1ed 100644 --- a/web/src/lib/api/admin.ts +++ b/web/src/lib/api/admin.ts @@ -116,10 +116,22 @@ export function createRootFoldersQuery(enabled: boolean = true) { export function createAdminRequestsQuery(status?: LidarrRequestStatus) { return createQuery({ queryKey: qk.adminRequests(status), - queryFn: () => listAdminRequests(status) + queryFn: () => listAdminRequests(status), + // Only the 'approved' tab needs polling — that's where in-flight + // ingests live. Other tabs (pending/rejected/completed) are static + // until the operator acts on them. + refetchInterval: (query) => { + if (status !== 'approved') return false; + const rows = query.state.data as LidarrRequest[] | undefined; + return hasInFlightRequest(rows) ? 12_000 : false; + } }); } +function hasInFlightRequest(rows: readonly LidarrRequest[] | undefined): boolean { + return rows?.some((r) => r.status === 'approved') ?? false; +} + // Admin quarantine -------------------------------------------------------- export async function listAdminQuarantine(): Promise { diff --git a/web/src/lib/api/requests.test.ts b/web/src/lib/api/requests.test.ts index 94db17fd..38877fd6 100644 --- a/web/src/lib/api/requests.test.ts +++ b/web/src/lib/api/requests.test.ts @@ -14,7 +14,8 @@ import { createRequest, listMyRequests, getRequest, - cancelRequest + cancelRequest, + hasInFlightRequest } from './requests'; import { api, apiFetch } from './client'; import { qk } from './queries'; @@ -155,3 +156,30 @@ describe('qk.myRequests', () => { expect(qk.myRequests()).toEqual(['myRequests']); }); }); + +describe('hasInFlightRequest', () => { + test('false for undefined / empty', () => { + expect(hasInFlightRequest(undefined)).toBe(false); + expect(hasInFlightRequest([])).toBe(false); + }); + + test('false when no row is approved', () => { + expect( + hasInFlightRequest([ + { ...mockRow, status: 'pending' }, + { ...mockRow, status: 'completed' }, + { ...mockRow, status: 'rejected' } + ]) + ).toBe(false); + }); + + test('true when at least one row is approved (mid-ingest)', () => { + expect( + hasInFlightRequest([ + { ...mockRow, status: 'pending' }, + { ...mockRow, status: 'approved' }, + { ...mockRow, status: 'completed' } + ]) + ).toBe(true); + }); +}); diff --git a/web/src/lib/api/requests.ts b/web/src/lib/api/requests.ts index 0e601753..be716af2 100644 --- a/web/src/lib/api/requests.ts +++ b/web/src/lib/api/requests.ts @@ -3,6 +3,18 @@ import { api, apiFetch } from './client'; import { qk } from './queries'; import type { LidarrRequest, LidarrRequestKind } from './types'; +/** Cadence for in-flight request polling. Tuned to match the operator's + "fast enough to feel live, slow enough to not hammer the server" + threshold from #369. */ +const POLL_INTERVAL_MS = 12_000; + +/** True if any row is mid-ingest (status === 'approved'). The status flips + to 'completed' / 'rejected' when Lidarr finishes, so this predicate + lets the page poll exactly while there's something interesting to watch. */ +export function hasInFlightRequest(rows: readonly LidarrRequest[] | undefined): boolean { + return rows?.some((r) => r.status === 'approved') ?? false; +} + // CreateRequestParams is the shape callers pass; unused MBID fields are // optional, and we omit them from the wire body rather than sending empty // strings (the backend tolerates either, but this keeps test expectations @@ -63,6 +75,14 @@ export function createMyRequestsQuery() { return createQuery({ queryKey: qk.myRequests(), queryFn: listMyRequests, - staleTime: 60_000 + staleTime: 60_000, + // Auto-poll while at least one user request is mid-ingest. Stops + // automatically when all rows are pending/completed/rejected. + // refetchIntervalInBackground defaults to false → polling pauses + // while the tab is hidden and resumes on focus. + refetchInterval: (query) => + hasInFlightRequest(query.state.data as LidarrRequest[] | undefined) + ? POLL_INTERVAL_MS + : false }); }