186 lines
7.7 KiB
Markdown
186 lines
7.7 KiB
Markdown
|
|
# 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)
|