You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GET /api/v1/books/ returns up to 100 books (limit: int = Query(100, ge=1, le=100)), and the dashboard renders every one as a card. There is no pagination control in the UI, so a user past 100 books simply cannot reach the rest.
Measured on staging, where the E2E account holds 2,702 books:
100-book payload: ~195 KB (~1.9 KB per book)
100 cards rendered in one list
First paint exceeded 15 s late in a suite run — an E2E spec waiting 15 s for its book to appear captured a snapshot with zero cards rendered
That spec has been moved to 20 s to match its siblings (#488), which treats the symptom. The underlying slowness is this issue.
Why it is worth fixing
The remaining 2,602 books are unreachable.skip exists on the endpoint but nothing in the UI drives it.
A ~200 KB payload plus 100 card renders is a poor first-paint budget for the app's primary landing screen.
Ordering only recently became correct (fix(books): show newest books first on the dashboard #491 added sort(updated_at, -1); before that the page showed the oldest 100 forever). With ordering fixed, the missing pagination is now the binding limit.
Notes for whoever picks this up
owner_updated_idx (owner_id, updated_at DESC) already exists and backs the current sort — explain() shows LIMIT → FETCH → IXSCAN. Keep any paging change on that index; adding an _id tiebreaker measured as a blocking in-memory SORT stage.
Cursor-based paging (updated_at + _id as a cursor) avoids the skip/limit drift problem on a list that changes while you page through it, but note the index caveat above.
The list endpoint already projects out per-chapter content, so the payload is metadata only — the size is card count, not fat rows.
Priority
P3.22 — degraded UX and an unreachable tail, not a correctness or security problem, and it only bites accounts with large libraries. The staging E2E account is the extreme case; real users are unlikely to be near 100 yet.
Surfaced while triaging #488.
What happens
GET /api/v1/books/returns up to 100 books (limit: int = Query(100, ge=1, le=100)), and the dashboard renders every one as a card. There is no pagination control in the UI, so a user past 100 books simply cannot reach the rest.Measured on staging, where the E2E account holds 2,702 books:
That spec has been moved to 20 s to match its siblings (#488), which treats the symptom. The underlying slowness is this issue.
Why it is worth fixing
skipexists on the endpoint but nothing in the UI drives it.sort(updated_at, -1); before that the page showed the oldest 100 forever). With ordering fixed, the missing pagination is now the binding limit.Notes for whoever picks this up
owner_updated_idx (owner_id, updated_at DESC)already exists and backs the current sort —explain()showsLIMIT → FETCH → IXSCAN. Keep any paging change on that index; adding an_idtiebreaker measured as a blocking in-memorySORTstage.updated_at+_idas a cursor) avoids the skip/limit drift problem on a list that changes while you page through it, but note the index caveat above.content, so the payload is metadata only — the size is card count, not fat rows.Priority
P3.22 — degraded UX and an unreachable tail, not a correctness or security problem, and it only bites accounts with large libraries. The staging E2E account is the extreme case; real users are unlikely to be near 100 yet.