# Session Archive, Multi-Select, Sunday Badge, and Pagination — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add session archiving, multi-select bulk actions, a Sunday "Game Night" badge, and configurable pagination to the History page and Session Detail page. **Architecture:** Backend-first approach. Add the `archived` column, then modify the list endpoint with filter/limit/total-count support, then add archive and bulk endpoints. Frontend follows: utility helpers, History page rewrite (controls bar + cards + multi-select), then SessionDetail updates. **Tech Stack:** Express/better-sqlite3 backend, React frontend with Tailwind CSS. No new dependencies. **Spec:** `docs/superpowers/specs/2026-03-23-session-archive-multiselect-design.md` --- ### Task 1: Schema — Add `archived` Column **Files:** - Modify: `backend/database.js` (after existing session table creation, ~line 57) - [ ] **Step 1: Add archived column** In `backend/database.js`, add after the existing sessions table creation (after the closing `);` of `CREATE TABLE IF NOT EXISTS sessions`): ```js // Add archived column if it doesn't exist (for existing databases) try { db.exec(`ALTER TABLE sessions ADD COLUMN archived INTEGER DEFAULT 0`); } catch (err) { // Column already exists, ignore error } ``` This follows the exact same pattern used for `session_games.status`, `session_games.room_code`, etc. - [ ] **Step 2: Verify the column exists** Run: `cd backend && node -e "const db = require('./database'); console.log(db.prepare('PRAGMA table_info(sessions)').all().map(c => c.name))"` Expected: Array includes `'archived'` - [ ] **Step 3: Commit** ```bash git add backend/database.js git commit -m "feat: add archived column to sessions table" ``` --- ### Task 2: Backend — Modify `GET /api/sessions` List Endpoint **Files:** - Modify: `backend/routes/sessions.js:22-47` (the `GET /` handler) - Test: `tests/api/session-archive.test.js` (new file) - [ ] **Step 1: Write the failing tests** Create `tests/api/session-archive.test.js`: ```js const request = require('supertest'); const { app } = require('../../backend/server'); const { cleanDb, getAuthHeader, seedSession } = require('../helpers/test-utils'); describe('GET /api/sessions — filter and limit', () => { beforeEach(() => { cleanDb(); }); test('default filter excludes archived sessions', async () => { seedSession({ is_active: 0, notes: null }); const archived = seedSession({ is_active: 0, notes: null }); require('../helpers/test-utils').db.prepare( 'UPDATE sessions SET archived = 1 WHERE id = ?' ).run(archived.id); const res = await request(app).get('/api/sessions'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].archived).toBe(0); }); test('filter=archived returns only archived sessions', async () => { seedSession({ is_active: 0, notes: null }); const archived = seedSession({ is_active: 0, notes: null }); require('../helpers/test-utils').db.prepare( 'UPDATE sessions SET archived = 1 WHERE id = ?' ).run(archived.id); const res = await request(app).get('/api/sessions?filter=archived'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].archived).toBe(1); }); test('filter=all returns all sessions', async () => { seedSession({ is_active: 0, notes: null }); const archived = seedSession({ is_active: 0, notes: null }); require('../helpers/test-utils').db.prepare( 'UPDATE sessions SET archived = 1 WHERE id = ?' ).run(archived.id); const res = await request(app).get('/api/sessions?filter=all'); expect(res.status).toBe(200); expect(res.body).toHaveLength(2); }); test('limit restricts number of sessions returned', async () => { for (let i = 0; i < 10; i++) { seedSession({ is_active: 0, notes: null }); } const res = await request(app).get('/api/sessions?filter=all&limit=3'); expect(res.status).toBe(200); expect(res.body).toHaveLength(3); }); test('limit=all returns all sessions', async () => { for (let i = 0; i < 10; i++) { seedSession({ is_active: 0, notes: null }); } const res = await request(app).get('/api/sessions?filter=all&limit=all'); expect(res.status).toBe(200); expect(res.body).toHaveLength(10); }); test('X-Total-Count header reflects total matching sessions before limit', async () => { for (let i = 0; i < 10; i++) { seedSession({ is_active: 0, notes: null }); } const res = await request(app).get('/api/sessions?filter=all&limit=3'); expect(res.headers['x-total-count']).toBe('10'); expect(res.body).toHaveLength(3); }); test('response includes archived field on each session', async () => { seedSession({ is_active: 0, notes: null }); const res = await request(app).get('/api/sessions?filter=all'); expect(res.status).toBe(200); expect(res.body[0]).toHaveProperty('archived', 0); }); test('default limit is all when no limit param provided', async () => { for (let i = 0; i < 8; i++) { seedSession({ is_active: 0, notes: null }); } const res = await request(app).get('/api/sessions?filter=all'); expect(res.status).toBe(200); expect(res.body).toHaveLength(8); }); }); ``` Note: the default server-side limit is `all` (no limit) for backwards compatibility. The frontend controls the limit via query param. - [ ] **Step 2: Run tests to verify they fail** Run: `cd /Users/erikfredericks/dev-ai/HSO/jackboxpartypack-gamepicker && npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: Multiple failures — `archived` field missing from response, no filter/limit support. - [ ] **Step 3: Implement the modified GET / handler** Replace lines 22-47 of `backend/routes/sessions.js` (the entire `router.get('/', ...)` handler) with: ```js router.get('/', (req, res) => { try { const filter = req.query.filter || 'default'; const limitParam = req.query.limit || 'all'; let whereClause = ''; if (filter === 'default') { whereClause = 'WHERE s.archived = 0'; } else if (filter === 'archived') { whereClause = 'WHERE s.archived = 1'; } const countRow = db.prepare(` SELECT COUNT(DISTINCT s.id) as total FROM sessions s ${whereClause} `).get(); let limitClause = ''; if (limitParam !== 'all') { const limitNum = parseInt(limitParam, 10); if (!isNaN(limitNum) && limitNum > 0) { limitClause = `LIMIT ${limitNum}`; } } const sessions = db.prepare(` SELECT s.id, s.created_at, s.closed_at, s.is_active, s.archived, s.notes, COUNT(sg.id) as games_played FROM sessions s LEFT JOIN session_games sg ON s.id = sg.session_id ${whereClause} GROUP BY s.id ORDER BY s.created_at DESC ${limitClause} `).all(); const result = sessions.map(({ notes, ...session }) => { const { has_notes, notes_preview } = computeNotesPreview(notes); return { ...session, has_notes, notes_preview }; }); res.set('X-Total-Count', String(countRow.total)); res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } }); ``` - [ ] **Step 4: Run tests to verify they pass** Run: `cd /Users/erikfredericks/dev-ai/HSO/jackboxpartypack-gamepicker && npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: All 8 tests PASS - [ ] **Step 5: Run full test suite to verify no regressions** Run: `cd /Users/erikfredericks/dev-ai/HSO/jackboxpartypack-gamepicker && npx jest --verbose --forceExit` Expected: All tests pass (existing session-notes tests may need a small adjustment if they expect the exact old response shape — check for `archived` field expectations). - [ ] **Step 6: Commit** ```bash git add backend/routes/sessions.js tests/api/session-archive.test.js git commit -m "feat: add filter, limit, and X-Total-Count to session list endpoint" ``` --- ### Task 3: Backend — Archive/Unarchive Single Session Endpoints **Files:** - Modify: `backend/routes/sessions.js` (add two new routes after `DELETE /:id/notes` at line 288) - Test: `tests/api/session-archive.test.js` (append to existing file) - [ ] **Step 1: Write the failing tests** Append to `tests/api/session-archive.test.js`: ```js describe('POST /api/sessions/:id/archive', () => { beforeEach(() => { cleanDb(); }); test('archives a closed session', async () => { const session = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post(`/api/sessions/${session.id}/archive`) .set('Authorization', getAuthHeader()); expect(res.status).toBe(200); expect(res.body.success).toBe(true); const check = await request(app).get(`/api/sessions/${session.id}`); expect(check.body.archived).toBe(1); }); test('returns 400 for active session', async () => { const session = seedSession({ is_active: 1, notes: null }); const res = await request(app) .post(`/api/sessions/${session.id}/archive`) .set('Authorization', getAuthHeader()); expect(res.status).toBe(400); }); test('returns 404 for non-existent session', async () => { const res = await request(app) .post('/api/sessions/9999/archive') .set('Authorization', getAuthHeader()); expect(res.status).toBe(404); }); test('returns 401 without auth', async () => { const session = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post(`/api/sessions/${session.id}/archive`); expect(res.status).toBe(401); }); }); describe('POST /api/sessions/:id/unarchive', () => { beforeEach(() => { cleanDb(); }); test('unarchives an archived session', async () => { const session = seedSession({ is_active: 0, notes: null }); require('../helpers/test-utils').db.prepare( 'UPDATE sessions SET archived = 1 WHERE id = ?' ).run(session.id); const res = await request(app) .post(`/api/sessions/${session.id}/unarchive`) .set('Authorization', getAuthHeader()); expect(res.status).toBe(200); expect(res.body.success).toBe(true); const check = await request(app).get(`/api/sessions/${session.id}`); expect(check.body.archived).toBe(0); }); test('returns 404 for non-existent session', async () => { const res = await request(app) .post('/api/sessions/9999/unarchive') .set('Authorization', getAuthHeader()); expect(res.status).toBe(404); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: New tests fail with 404 (routes don't exist yet) - [ ] **Step 3: Implement the archive/unarchive routes** Add after the `DELETE /:id/notes` route (after line 288 in `sessions.js`): ```js // Archive a session (admin only) router.post('/:id/archive', authenticateToken, (req, res) => { try { const session = db.prepare('SELECT id, is_active FROM sessions WHERE id = ?').get(req.params.id); if (!session) { return res.status(404).json({ error: 'Session not found' }); } if (session.is_active === 1) { return res.status(400).json({ error: 'Cannot archive an active session. Please close it first.' }); } db.prepare('UPDATE sessions SET archived = 1 WHERE id = ?').run(req.params.id); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Unarchive a session (admin only) router.post('/:id/unarchive', authenticateToken, (req, res) => { try { const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(req.params.id); if (!session) { return res.status(404).json({ error: 'Session not found' }); } db.prepare('UPDATE sessions SET archived = 0 WHERE id = ?').run(req.params.id); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: All tests PASS - [ ] **Step 5: Commit** ```bash git add backend/routes/sessions.js tests/api/session-archive.test.js git commit -m "feat: add POST archive and unarchive endpoints for sessions" ``` --- ### Task 4: Backend — Bulk Endpoint **Files:** - Modify: `backend/routes/sessions.js` (add `POST /bulk` route — MUST be before any `/:id` POST routes) - Test: `tests/api/session-archive.test.js` (append) - [ ] **Step 1: Write the failing tests** Append to `tests/api/session-archive.test.js`: ```js describe('POST /api/sessions/bulk', () => { beforeEach(() => { cleanDb(); }); test('bulk archive multiple sessions', async () => { const s1 = seedSession({ is_active: 0, notes: null }); const s2 = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'archive', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.affected).toBe(2); const list = await request(app).get('/api/sessions?filter=archived'); expect(list.body).toHaveLength(2); }); test('bulk unarchive multiple sessions', async () => { const s1 = seedSession({ is_active: 0, notes: null }); const s2 = seedSession({ is_active: 0, notes: null }); const db = require('../helpers/test-utils').db; db.prepare('UPDATE sessions SET archived = 1 WHERE id IN (?, ?)').run(s1.id, s2.id); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'unarchive', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); expect(res.body.affected).toBe(2); const list = await request(app).get('/api/sessions?filter=all'); expect(list.body.every(s => s.archived === 0)).toBe(true); }); test('bulk delete multiple sessions', async () => { const s1 = seedSession({ is_active: 0, notes: null }); const s2 = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'delete', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); expect(res.body.affected).toBe(2); const list = await request(app).get('/api/sessions?filter=all'); expect(list.body).toHaveLength(0); }); test('rejects archive of active sessions', async () => { const active = seedSession({ is_active: 1, notes: null }); const closed = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'archive', ids: [active.id, closed.id] }); expect(res.status).toBe(400); expect(res.body.activeIds).toContain(active.id); const list = await request(app).get('/api/sessions?filter=all'); expect(list.body).toHaveLength(2); expect(list.body.every(s => s.archived === 0)).toBe(true); }); test('rejects delete of active sessions', async () => { const active = seedSession({ is_active: 1, notes: null }); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'delete', ids: [active.id] }); expect(res.status).toBe(400); }); test('returns 400 for empty ids array', async () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'archive', ids: [] }); expect(res.status).toBe(400); }); test('returns 400 for invalid action', async () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'nuke', ids: [1] }); expect(res.status).toBe(400); }); test('returns 400 for non-array ids', async () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'archive', ids: 'not-array' }); expect(res.status).toBe(400); }); test('returns 404 if any session ID does not exist', async () => { const s1 = seedSession({ is_active: 0, notes: null }); const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) .send({ action: 'archive', ids: [s1.id, 9999] }); expect(res.status).toBe(404); }); test('returns 401 without auth', async () => { const res = await request(app) .post('/api/sessions/bulk') .send({ action: 'archive', ids: [1] }); expect(res.status).toBe(401); }); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: `npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: Bulk tests fail (route doesn't exist — likely gets matched as `POST /:id` with `id=bulk`) - [ ] **Step 3: Implement the bulk route** This route MUST be registered BEFORE any `POST /:id/...` routes. Insert it after the `POST /` route (create session, line ~152) and before `POST /:id/close` (line ~155): ```js // Bulk session operations (admin only) router.post('/bulk', authenticateToken, (req, res) => { try { const { action, ids } = req.body; if (!Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ error: 'ids must be a non-empty array' }); } const validActions = ['archive', 'unarchive', 'delete']; if (!validActions.includes(action)) { return res.status(400).json({ error: `action must be one of: ${validActions.join(', ')}` }); } const placeholders = ids.map(() => '?').join(','); const sessions = db.prepare( `SELECT id, is_active FROM sessions WHERE id IN (${placeholders})` ).all(...ids); if (sessions.length !== ids.length) { const foundIds = sessions.map(s => s.id); const missingIds = ids.filter(id => !foundIds.includes(id)); return res.status(404).json({ error: 'Some sessions not found', missingIds }); } if (action === 'archive' || action === 'delete') { const activeIds = sessions.filter(s => s.is_active === 1).map(s => s.id); if (activeIds.length > 0) { return res.status(400).json({ error: `Cannot ${action} active sessions. Close them first.`, activeIds }); } } const bulkOperation = db.transaction(() => { if (action === 'archive') { db.prepare(`UPDATE sessions SET archived = 1 WHERE id IN (${placeholders})`).run(...ids); } else if (action === 'unarchive') { db.prepare(`UPDATE sessions SET archived = 0 WHERE id IN (${placeholders})`).run(...ids); } else if (action === 'delete') { db.prepare(`DELETE FROM chat_logs WHERE session_id IN (${placeholders})`).run(...ids); db.prepare(`DELETE FROM live_votes WHERE session_id IN (${placeholders})`).run(...ids); db.prepare(`DELETE FROM session_games WHERE session_id IN (${placeholders})`).run(...ids); db.prepare(`DELETE FROM sessions WHERE id IN (${placeholders})`).run(...ids); } }); bulkOperation(); res.json({ success: true, affected: ids.length }); } catch (error) { res.status(500).json({ error: error.message }); } }); ``` - [ ] **Step 4: Run tests to verify they pass** Run: `npx jest tests/api/session-archive.test.js --verbose --forceExit` Expected: All tests PASS - [ ] **Step 5: Run full test suite** Run: `npx jest --verbose --forceExit` Expected: All tests pass - [ ] **Step 6: Commit** ```bash git add backend/routes/sessions.js tests/api/session-archive.test.js git commit -m "feat: add POST /sessions/bulk endpoint for bulk archive, unarchive, and delete" ``` --- ### Task 5: Frontend — Add `isSunday` Helper **Files:** - Modify: `frontend/src/utils/dateUtils.js` - [ ] **Step 1: Add isSunday function** Append before the final empty line of `frontend/src/utils/dateUtils.js`: ```js /** * Check if a SQLite timestamp falls on a Sunday (in local timezone) * @param {string} sqliteTimestamp * @returns {boolean} */ export function isSunday(sqliteTimestamp) { return parseUTCTimestamp(sqliteTimestamp).getDay() === 0; } ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/utils/dateUtils.js git commit -m "feat: add isSunday helper to dateUtils" ``` --- ### Task 6: Frontend — History Page Rewrite (Controls Bar + Pagination + Filters + Cards) **Files:** - Rewrite: `frontend/src/pages/History.jsx` This is the largest frontend task. The History page gets a full rewrite with: - Controls bar (filter dropdown, show dropdown, session count, select button) - localStorage persistence for filter and limit - Session cards with Sunday badge, Archived badge, notes preview - Updated `loadSessions` to pass filter and limit query params + read X-Total-Count - [ ] **Step 1: Rewrite History.jsx** Replace the entire contents of `frontend/src/pages/History.jsx` with the new implementation. The key changes: **State management:** ```js const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const [closingSession, setClosingSession] = useState(null); const [totalCount, setTotalCount] = useState(0); const [filter, setFilter] = useState(() => localStorage.getItem('history-filter') || 'default'); const [limit, setLimit] = useState(() => localStorage.getItem('history-show-limit') || '5'); ``` **loadSessions with query params:** ```js const loadSessions = useCallback(async () => { try { const response = await api.get('/sessions', { params: { filter, limit } }); setSessions(response.data); setTotalCount(parseInt(response.headers['x-total-count'] || '0', 10)); } catch (err) { console.error('Failed to load sessions', err); } finally { setLoading(false); } }, [filter, limit]); ``` **localStorage persistence:** ```js const handleFilterChange = (newFilter) => { setFilter(newFilter); localStorage.setItem('history-filter', newFilter); }; const handleLimitChange = (newLimit) => { setLimit(newLimit); localStorage.setItem('history-show-limit', newLimit); }; ``` **Controls bar JSX:** - Filter dropdown: `` with 5/10/25/50/All options - Session count from `totalCount` state - Select button (admin only, placeholder for Task 7) **Session card badges:** - Import `isSunday` from dateUtils - Sunday badge: amber "☀ GAME NIGHT" `` when `isSunday(session.created_at)` - "· Sunday" text appended to date line - Archived badge: gray "Archived" `` when `session.archived === 1` and filter is `all` or `archived` - Active badge: green "Active" `` (existing) **EndSessionModal:** Keep the existing `EndSessionModal` component at the bottom of the file unchanged. - [ ] **Step 2: Verify the build compiles** Run: `cd frontend && npm run build` Expected: Build succeeds with no errors. - [ ] **Step 3: Commit** ```bash git add frontend/src/pages/History.jsx git commit -m "feat: rewrite History page with controls bar, filter, pagination, and session badges" ``` --- ### Task 7: Frontend — Multi-Select Mode **Files:** - Modify: `frontend/src/pages/History.jsx` (add multi-select state, checkboxes, action bar, long-press handler) This task adds to the History page from Task 6: - [ ] **Step 1: Add multi-select state and handlers** Add new state variables: ```js const [selectMode, setSelectMode] = useState(false); const [selectedIds, setSelectedIds] = useState(new Set()); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); ``` Add long-press refs and handler: ```js const longPressTimer = useRef(null); const handlePointerDown = (sessionId) => { if (!isAuthenticated || selectMode) return; longPressTimer.current = setTimeout(() => { setSelectMode(true); setSelectedIds(new Set([sessionId])); }, 500); }; const handlePointerUp = () => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }; ``` Add toggle selection handler: ```js const toggleSelection = (sessionId) => { setSelectedIds(prev => { const next = new Set(prev); if (next.has(sessionId)) { next.delete(sessionId); } else { next.add(sessionId); } return next; }); }; ``` Add bulk action handlers: ```js const handleBulkAction = async (action) => { try { await api.post('/sessions/bulk', { action, ids: Array.from(selectedIds) }); success(`${selectedIds.size} session${selectedIds.size !== 1 ? 's' : ''} ${action}d`); setSelectedIds(new Set()); setShowBulkDeleteConfirm(false); await loadSessions(); } catch (err) { error(err.response?.data?.error || `Failed to ${action} sessions`); } }; ``` Clear selections when filter or limit changes: ```js const handleFilterChange = (newFilter) => { setFilter(newFilter); localStorage.setItem('history-filter', newFilter); setSelectedIds(new Set()); }; const handleLimitChange = (newLimit) => { setLimit(newLimit); localStorage.setItem('history-show-limit', newLimit); setSelectedIds(new Set()); }; ``` Exit multi-select handler: ```js const exitSelectMode = () => { setSelectMode(false); setSelectedIds(new Set()); }; ``` - [ ] **Step 2: Update the controls bar with Select/Done button** In the controls bar, add the Select/Done button (admin only): ```jsx {isAuthenticated && ( )} ``` - [ ] **Step 3: Update session cards with checkboxes and selection state** Wrap each session card's `onClick` to be conditional: ```jsx onClick={() => { if (selectMode) { if (session.is_active !== 1) { toggleSelection(session.id); } } else { navigate(`/history/${session.id}`); } }} ``` Add long-press handlers on the card `
`: ```jsx onPointerDown={() => { if (session.is_active !== 1) handlePointerDown(session.id); }} onPointerUp={handlePointerUp} onPointerLeave={handlePointerUp} ``` When `selectMode` is true, show a checkbox at the start of each card: ```jsx {selectMode && (
{selectedIds.has(session.id) && ( )}
)} ``` Active sessions get `opacity-50` and `cursor-not-allowed` when in selectMode. Hide the "End Session" button when in selectMode. Selected cards get the indigo highlight: `border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20`. - [ ] **Step 4: Add the floating action bar** Below the session list, when `selectMode && selectedIds.size > 0`: ```jsx {selectMode && selectedIds.size > 0 && (
{selectedIds.size} selected
{filter !== 'archived' && ( )} {filter !== 'default' && ( )}
)} ``` - [ ] **Step 5: Add the bulk delete confirmation modal** ```jsx {showBulkDeleteConfirm && (

Delete {selectedIds.size} Session{selectedIds.size !== 1 ? 's' : ''}?

This will permanently delete {selectedIds.size} session{selectedIds.size !== 1 ? 's' : ''} and all associated games and chat logs. This action cannot be undone.

)} ``` - [ ] **Step 6: Verify build** Run: `cd frontend && npm run build` Expected: Build succeeds - [ ] **Step 7: Commit** ```bash git add frontend/src/pages/History.jsx git commit -m "feat: add multi-select mode with bulk archive, unarchive, and delete" ``` --- ### Task 8: Frontend — Session Detail Page Updates **Files:** - Modify: `frontend/src/pages/SessionDetail.jsx` - Modify: `frontend/src/utils/dateUtils.js` (already done in Task 5) - [ ] **Step 1: Add archive handler and state** In `SessionDetail`, add the archive/unarchive handler: ```js const handleArchive = async () => { const action = session.archived === 1 ? 'unarchive' : 'archive'; try { await api.post(`/sessions/${id}/${action}`); await loadSession(); success(`Session ${action}d`); } catch (err) { showError(err.response?.data?.error || `Failed to ${action} session`); } }; ``` - [ ] **Step 2: Add Sunday badge to the session header** Import `isSunday` from dateUtils at the top of the file: ```js import { formatLocalDateTime, formatLocalTime, isSunday } from '../utils/dateUtils'; ``` In the session header, after the Active badge, add: ```jsx {isSunday(session.created_at) && ( ☀ Game Night )} ``` In the date/time line, append " · Sunday" when applicable: ```jsx

{formatLocalDateTime(session.created_at)} {isSunday(session.created_at) && ( · Sunday )} {' • '} {session.games_played} game{session.games_played !== 1 ? 's' : ''} played

``` - [ ] **Step 3: Add archived banner** After the back link and before the main session header card, add: ```jsx {session.archived === 1 && (
This session is archived {isAuthenticated && ( )}
)} ``` - [ ] **Step 4: Add archive/unarchive button alongside delete** In the action buttons area (where the Delete Session button is), add the Archive/Unarchive button for closed sessions: ```jsx {isAuthenticated && session.is_active === 0 && ( <> )} ``` This replaces the existing Delete Session button block (lines 211-218). - [ ] **Step 5: Verify build** Run: `cd frontend && npm run build` Expected: Build succeeds - [ ] **Step 6: Commit** ```bash git add frontend/src/pages/SessionDetail.jsx git commit -m "feat: add archive button, archived banner, and Sunday badge to Session Detail page" ``` --- ### Task 9: Final Verification - [ ] **Step 1: Run full backend test suite** Run: `cd /Users/erikfredericks/dev-ai/HSO/jackboxpartypack-gamepicker && npx jest --verbose --forceExit` Expected: All tests pass - [ ] **Step 2: Build frontend** Run: `cd frontend && npm run build` Expected: Build succeeds - [ ] **Step 3: Manual smoke test checklist** Spin up the app and verify: - [ ] History page loads with controls bar (Filter + Show dropdowns) - [ ] Default filter shows non-archived sessions - [ ] Changing Show dropdown to 10/25/50/All works and persists on reload - [ ] Changing Filter dropdown to Archived/All works and persists on reload - [ ] Sunday sessions show "GAME NIGHT" badge and "· Sunday" text - [ ] Session Detail page shows Sunday badge and archive button (for closed sessions, as admin) - [ ] Archiving from detail page works, shows archived banner - [ ] Select button enters multi-select mode (admin only) - [ ] Long-press on a closed session enters multi-select - [ ] Active sessions are greyed out and non-selectable - [ ] Bulk archive works from action bar - [ ] Bulk delete shows confirmation modal and works - [ ] Archived filter shows archived sessions with Unarchive + Delete buttons - [ ] Changing filter/limit while in multi-select clears selections