# 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: <count> }`
- **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)
