diff --git a/docs/superpowers/specs/2026-03-23-session-archive-multiselect-design.md b/docs/superpowers/specs/2026-03-23-session-archive-multiselect-design.md new file mode 100644 index 0000000..7793c9b --- /dev/null +++ b/docs/superpowers/specs/2026-03-23-session-archive-multiselect-design.md @@ -0,0 +1,185 @@ +# 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)