4.7 KiB
Pagination & Day Grouping — Design Spec
Date: 2026-03-23 Status: Approved
Overview
Two enhancements to the session History page:
- Pagination — When "Show X" is set to a value other than "All", add Prev/Next navigation to access older sessions.
- Day Grouping — Group sessions that occurred on the same calendar day under a shared header bar.
Backend Changes
GET /api/sessions — New offset parameter
Add an offset query parameter (default 0) to the existing endpoint. Works with the existing limit, filter, X-Total-Count, and X-Absolute-Total headers.
GET /api/sessions?filter=default&limit=5&offset=10
Offset validation: Non-numeric or negative values are clamped to 0. An offset past the end returns an empty array (the pagination bar will show "Page X of Y" and the user can navigate back).
New response header:
X-Prev-Last-Date— Whenoffset > 0, the raw SQLitecreated_attimestamp (same format ascreated_atin response body, e.g."2026-03-23 19:30:00") of the session immediately before the current page (the session at positionoffset - 1). Used by the frontend to detect whether the first day group on the current page is a continuation from the previous page. Omitted whenoffsetis 0. The frontend parses this with the existingparseUTCTimestamputility.
SQL changes: Add OFFSET clause to the existing query. For X-Prev-Last-Date, run a small secondary query to fetch the created_at of the session at position offset - 1 (same filter/ordering).
No other backend changes required.
Frontend Changes
State
page(number, default1) — current page number. Derived from offset:offset = (page - 1) * limit. Not persisted in localStorage; resets to 1 on navigation.prevLastDate(string|null) — fromX-Prev-Last-Dateheader. Used for "(continued)" detection.
Page math
totalPages = Math.ceil(totalCount / limitNum)
offset = (page - 1) * limitNum
When limit is "all", pagination is disabled (no offset, no pagination bar).
loadSessions changes
Pass offset as a query parameter alongside filter and limit. Read X-Prev-Last-Date from response headers.
Page reset triggers
Changing filter, limit, or entering/exiting selectMode resets page to 1.
Day Grouping (render-time only)
Group the flat session array by local calendar date at render time. For each group:
- Day header bar — Styled with
bg-[#1e2a3a](dark) /bg-gray-100(light), left border accent (border-l-[3px] border-indigo-500), contains:- Full date: "Sunday, Mar 23, 2026"
- Right side: session count ("2 sessions") and, if Sunday, "🎲 Game Night"
- Session cards — Indented slightly (
ml-3) beneath their day header. Display time only (e.g., "7:30 PM") since the full date is in the header. Remove the per-card "· Sunday" text and per-card "🎲 Game Night" badge since that information is now on the day header.
"(continued)" detection
When page > 1 and prevLastDate is set:
- Parse the previous page's last session date to a local calendar date string
- If it matches the first day group's date, append an italic "(continued)" tag to that day header (no session count shown for continued groups since the count would be incomplete)
Pagination bar
Rendered below the session list, above the multi-select action bar (if active). Only shown when limit !== "all" and totalPages > 1.
Layout: ← Prev button | "Page X of Y" text | Next → button
- Prev button disabled (grayed out) on page 1
- Next button disabled on last page
- Active buttons use indigo (
bg-indigo-600) - Disabled buttons use gray (
bg-gray-600/700withcursor-not-allowed)
Multi-select interaction
Day header bars are not selectable. Only session cards participate in multi-select. Checkboxes render inside the indented card area as they do today. Changing pages clears selected IDs but keeps select mode active.
Polling behavior
The existing 3-second polling interval refetches the current page (same offset/limit/filter). If sessions are deleted while the user is on a later page and the page becomes empty, the next poll cycle detects sessions.length === 0 && page > 1 and resets to page 1.
"Visible" count update
The existing "X visible (Y total)" label continues to work as-is. sessions.length reflects the current page's sessions.
Scope
- No changes to
SessionDetail.jsx - No changes to bulk endpoints
- No new dependencies
- The
dateUtils.jsgains aformatDayHeaderhelper (e.g., "Sunday, Mar 23, 2026") and agetLocalDateKeyhelper for grouping - Existing tests for
GET /sessionsupdated to coveroffsetparameter; new tests forX-Prev-Last-Dateheader