# Design: Session Archive, Sunday Badge, Multi-Select, and Pagination ## Overview Four enhancements to the History page and Session Detail page: 1. **Archive/Unarchive sessions** — hide sessions from the default history list, with a filter to view archived sessions 2. **Sunday "Game Night" badge** — visual indicator on session cards when a session took place on a Sunday 3. **Multi-select mode** — bulk archive and delete operations on the History page (admin only) 4. **Pagination options** — configurable number of sessions shown (5, 10, 25, 50, All) ## Backend ### Schema Change Add `archived INTEGER DEFAULT 0` to the `sessions` table using the existing defensive `ALTER TABLE` pattern in `database.js`: ```js try { db.exec(`ALTER TABLE sessions ADD COLUMN archived INTEGER DEFAULT 0`); } catch (err) { // Column already exists } ``` No new tables. No migration file. ### New Endpoints #### `POST /api/sessions/:id/archive` - **Auth:** Required - **Action:** Sets `archived = 1` on the session - **Constraints:** Returns 400 if the session is still active (`is_active = 1`). Returns 404 if session not found. - **Response:** `{ success: true }` #### `POST /api/sessions/:id/unarchive` - **Auth:** Required - **Action:** Sets `archived = 0` on the session - **Constraints:** Returns 404 if session not found. - **Response:** `{ success: true }` #### `POST /api/sessions/bulk` - **Auth:** Required - **Action:** Performs a bulk operation on multiple sessions - **Body:** `{ "action": "archive" | "unarchive" | "delete", "ids": [1, 2, 3] }` - **Constraints:** - For `archive` and `delete`: rejects request with 400 if any session ID in the list is still active, returning the offending IDs in the error response - All IDs must exist (404 if any are not found) - Runs inside a database transaction — all-or-nothing - **Validation:** Returns 400 if `ids` is empty, if `action` is not one of the three valid values, or if `ids` is not an array - **Response:** `{ success: true, affected: }` - **Route registration:** Must be registered before `/:id` routes to avoid Express matching `"bulk"` as an `:id` parameter ### Modified Endpoint #### `GET /api/sessions` Add two query parameters: - **`filter`**: `"default"` | `"archived"` | `"all"` - `"default"` (when omitted): returns sessions where `archived = 0` - `"archived"`: returns sessions where `archived = 1` - `"all"`: returns all sessions regardless of archived status - **`limit`**: `"5"` | `"10"` | `"25"` | `"50"` | `"all"` - `"5"` (when omitted): returns the first 5 sessions (ordered by `created_at DESC`) - `"all"`: no limit applied - Any other value: applied as SQL `LIMIT` The response shape stays the same (array of session objects). Each session now includes the `archived` field (0 or 1). The existing `has_notes` and `notes_preview` fields continue as-is. **Total count:** The response should also include a way for the frontend to know how many sessions match the current filter (for the "N sessions total" display). Two options: a response header, or wrapping the response. To avoid breaking the existing array response shape, add a custom response header `X-Total-Count` with the total matching count before limit is applied. ### Unchanged Endpoints - `GET /api/sessions/:id` — already returns the full session object; will naturally include `archived` once the column exists - `POST /api/sessions` — unchanged (new sessions default to `archived = 0`) - `POST /api/sessions/:id/close` — unchanged - `DELETE /api/sessions/:id` — unchanged (still works, used by detail page) - `PUT /api/sessions/:id/notes`, `DELETE /api/sessions/:id/notes` — unchanged ## Frontend ### History Page — Controls Bar Replace the existing "Show All / Show Recent" toggle with a cohesive controls bar containing: 1. **Filter dropdown:** "Sessions" (default, `filter=default`), "Archived" (`filter=archived`), "All" (`filter=all`) 2. **Show dropdown:** 5 (default), 10, 25, 50, All 3. **Session count:** "N sessions total" — derived from the `X-Total-Count` response header 4. **Select button** (admin only): toggles multi-select mode on/off Both the Filter and Show selections are persisted in `localStorage`: - `history-filter` — stores the selected filter value (`"default"`, `"archived"`, `"all"`) - `history-show-limit` — stores the selected limit value (`"5"`, `"10"`, `"25"`, `"50"`, `"all"`) Values are read on mount and written on change. The frontend should always pass both `filter` and `limit` query params explicitly when calling `GET /api/sessions`, including in the 3-second polling interval. ### History Page — Session Cards #### Sunday Badge Sessions whose `created_at` falls on a Sunday (in the user's local timezone) display: - An amber "GAME NIGHT" badge (with sun icon) next to the session number - "· Sunday" appended to the date line in muted text Determination is client-side: `parseUTCTimestamp(session.created_at).getDay() === 0` using the existing `dateUtils.js` helper. #### Archived Badge When viewing the "All" or "Archived" filter, archived sessions show a gray "Archived" badge next to the session number. #### Notes Preview Continues as-is — indigo left-border teaser when `has_notes` is true. ### History Page — Multi-Select Mode **Entering multi-select:** - Click the "Select" toggle button in the controls bar (admin only) - Long-press (500ms+) on a closed session card (admin only) — enters multi-select and selects that card **While in multi-select:** - Checkboxes appear on each session card - Active sessions (`is_active = 1`) are greyed out with a disabled checkbox — not selectable - Clicking a card toggles its selection (instead of navigating to detail page) - The "End Session" button on active session cards is hidden - A floating action bar appears at the bottom with: - Left: "N selected" count - Right: context-aware action buttons: - **"Sessions" filter:** Archive + Delete - **"Archived" filter:** Unarchive + Delete - **"All" filter:** Archive + Unarchive + Delete - The action bar only appears when at least 1 session is selected - **Delete** always shows a confirmation modal: "Delete N sessions? This cannot be undone." - **Archive/Unarchive** execute immediately (non-destructive, reversible) with a toast confirmation **Changing filter or limit while in multi-select:** - Clears all selections (since the visible session list changes) - Stays in multi-select mode **Exiting multi-select:** - Click the "Done" / Select toggle button - Clears all selections and hides checkboxes + action bar ### Session Detail Page #### Archive/Unarchive Button - Placed alongside the existing Delete Session button at the bottom of the page - Only visible to authenticated admins, only for closed sessions - Shows "Archive" if `session.archived === 0`, "Unarchive" if `session.archived === 1` - Executes immediately with toast confirmation (no modal — it's reversible) #### Archived Banner - When viewing an archived session, a subtle banner appears at the top of the detail content: "This session is archived" - Includes an inline "Unarchive" button (admin only) #### Sunday Badge - Same amber "GAME NIGHT" badge shown next to "Session #N" in the page header - "· Sunday" in the date/time display ### Frontend Utilities Add a helper to `dateUtils.js`: ```js export function isSunday(sqliteTimestamp) { return parseUTCTimestamp(sqliteTimestamp).getDay() === 0; } ``` ## What's NOT Changing - **Database:** No new tables, just one column addition - **Session notes** (view/edit/delete) — untouched - **EndSessionModal** — untouched - **WebSocket events** — no archive-related real-time updates - **Other routes/pages** (Home, Picker, Manager, Login) — untouched - **`POST /sessions/:id/close`** — untouched - **New dependencies** — none required (no new npm packages)