feat(appview,web): server-side offset/limit pagination for GET /api/topics/:id (ATB-33) (#57)
* test(appview): add failing pagination tests for server-side topic pagination (ATB-33)
Add describe.sequential block to topics.test.ts covering GET /api/topics/:id
server-side pagination behavior that does not exist yet. Tests verify total,
offset, limit fields in response, default/explicit pagination, offset/limit
clamping, and empty result sets. All 8 tests fail as expected — implementation
pending.
* test(appview): clarify total semantics in pagination test name (ATB-33)
* feat(appview): add offset/limit pagination to GET /api/topics/:id (ATB-33)
- Parse offset/limit query params (default 25, max 100)
- Run COUNT + paginated SELECT in parallel (matching boards pattern)
- Return total, offset, limit in response alongside paginated replies
- Removes 1000-reply defensive limit in favour of server-controlled pagination
* test(appview): document approximate total semantic and add companion test (ATB-33)
* feat(web): use server-side pagination for topic replies (ATB-33)
- Pass offset/limit to AppView instead of slicing locally
- HTMX partial: forwards ?offset=N&limit=25 to AppView
- Full page: requests ?offset=0&limit=(offset+25) for bookmark support
- Removes TODO(ATB-33) comment
* fix(web): remove duplicate total property in makeTopicResponse test helper (ATB-33)
* chore(web): add clarifying comments for pagination edge cases (ATB-33)
* fix(web): remove invalid JSX comment between attributes (ATB-33)
* docs(bruno): update Get Topic collection with pagination params (ATB-33)
* fix(appview,web): address code review feedback on ATB-33 pagination
Critical fixes:
- Split single try-catch into two: topic query and reply query now have
distinct error messages ("Failed to retrieve topic" vs "Failed to
retrieve replies for topic") per CLAUDE.md try-block granularity rule
- HTMX partial error now returns a retry fragment instead of silently
replacing the Load More button with empty content
- Fix hasMore infinite loop: use `replies.length >= limit` (page-fullness
heuristic) instead of `nextOffset < total`; total is pre-filter and can
cause an infinite loop when in-memory filters zero out all SQL results
- Raise AppView limit cap from 100 to 250 so bookmark displayLimit
(offset + REPLIES_PER_PAGE) no longer gets silently clamped for deep links
- Fix Bruno docs: total is filtered for bannedByMod=false at SQL level,
not "unfiltered"; update description to match inline code comment
Important fixes:
- Remove total from ReplyFragment props (no longer used after hasMore fix)
- Change `total === 0` guard to `initialReplies.length === 0` so EmptyState
renders when all page-1 replies are filtered in-memory; update message to
"No replies to show."
- Add test: bannedByMod=true directly reduces total (proves COUNT query
applies the SQL-level filter)
- Add test: non-numeric offset/limit params default to 0/25
- Strengthen clamps limit=0 test to assert replies are returned, not just
metadata; rename limit cap test to reflect new max of 250
- Add AppView URL assertions to bookmark and HTMX partial web tests
- Update HTMX error test to assert retry fragment content
* fix(atb-33): clean up HTMX retry element and stale pagination comment
- Replace <p>/<button> retry fragment with bare <button> so hx-swap="outerHTML"
replaces the entire error element on retry success (no orphan text node)
- Update stale comment: topics cap is 250 (not 100 like boards) to support bookmarks