Paginate the Review Queue with cursor-based infinite scroll

Fetching every needs_review activity's laps and per-second samples up front
gets expensive once there are many -- that work is now deferred to the
current page only. GET /api/review-queue/ takes limit/before and returns
{items, next_cursor, total}; sorting/cursor filtering still needs each
activity's cheap summary row, but only the requested page's items get their
laps/samples fetched.

Frontend loads 10 at a time and fetches the next page when the list's
bottom sentinel scrolls into view. Selecting a specific-kind filter (not
"All") loads the rest of the backlog up front, since filtering only the
items scrolled into view so far would hide matches further down.
This commit is contained in:
2026-07-19 16:31:24 +02:00
parent 9ec721b9fc
commit db4c76b558
6 changed files with 284 additions and 36 deletions

View File

@@ -6,7 +6,7 @@ import type {
Profile,
ProgressionMetric,
ProgressionPoint,
ReviewQueueItem,
ReviewQueuePage,
SyncRun,
SyncStatus,
WorkoutKind,
@@ -82,8 +82,17 @@ export const api = {
updateProfile: (profile: Profile) =>
request<Profile>("/api/profile", { method: "PUT", body: JSON.stringify(profile) }),
// Review queue
reviewQueue: () => request<ReviewQueueItem[]>("/api/review-queue/"),
// Review queue -- cursor-paginated: pass the previous page's next_cursor
// as `before` to fetch the next one. Fetching every activity's laps and
// per-second samples up front gets expensive once there are many, so the
// frontend loads it incrementally (infinite scroll) instead of all at once.
reviewQueue: (params?: { limit?: number; before?: string }) => {
const q = new URLSearchParams();
if (params?.limit) q.set("limit", String(params.limit));
if (params?.before) q.set("before", params.before);
const qs = q.toString();
return request<ReviewQueuePage>(`/api/review-queue/${qs ? `?${qs}` : ""}`);
},
resolveReview: (activityId: number, workoutKindId: number) =>
request<{ status: string }>(`/api/review-queue/${activityId}/resolve`, {
method: "POST",