feat(web): auto-poll request progress when ingests are in flight (#369)

Half of #369 — the auto-poll piece. Inbox deferred (separate task).

Both /requests (user) and /admin/requests (admin, 'approved' tab only)
now refetch every 12s while at least one row has status='approved'.
Stops automatically when all rows settle to pending/completed/rejected.
TanStack Query's default refetchIntervalInBackground=false handles
the visibility behavior — polling pauses when the tab is hidden and
resumes on focus.

Predicate hasInFlightRequest() is exported + tested so the polling
logic is auditable independent of TanStack Query's internals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 17:59:38 -04:00
parent 7bc254e17e
commit 861dd8ef17
3 changed files with 63 additions and 3 deletions
+21 -1
View File
@@ -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
});
}