# Phase 50 — Save & View Chat History **Source:** `TODO.md` L5 — "Need a way to save and view chat history in a new page, then return to that history with a click" **Story:** n/a (TODO-derived — owner roadmap confirmation 2026-08-29) **Context:** A10 (revised, phase 16) keeps the public **chat** API stateless, and phase 14 locked conversation persistence to browser-local `localStorage` (`bor.chat.v1`). This phase records an **owner-locked extension** (AGENTS.md rule 3, owner permission 2026-08-29): conversations the owner explicitly **Saves** are stored in a new Postgres `saved_chats` table — `/api/chat` itself stays stateless, and nothing is stored about a conversation that was not saved. The admin gate is `app/core/auth.require_admin` (phase 16, the `steering` router's pattern). The localStorage record shape (`{who, text, sources?, deflected?, suggestions?, thinking?, tools?, stopped?}`) is the stored `messages` payload, so a saved chat restores pixel-identical through the existing `renderStoredMessage` path. The new page follows the phase-34 shared-header contract (every page carries the identical nav block; admin-only links ship hidden and `header.js` reveals them) and AGENTS.md rule 5 (a **full-width table** — no skinny wasted-space list). ## Objective The owner can Save the current conversation, see every saved chat on a new **History** page (full-width table), click one to return to the chat with that conversation loaded, and delete a saved chat. ## Dependencies - `48_stop_generation` / `49_retry_answer` (todo, sequential) — no shared-file conflicts beyond `app.js`; ordering keeps the chat UI stable while the save/load plumbing lands. - `14_chat_persistence` (complete) — the record shape + the `renderStoredMessage` restore a saved chat reuses. - `16_admin_auth` + `34_consistent_navbar` (complete) — the `require_admin` gate + the one-bar nav contract. - **Owner permission (2026-08-29):** the A10 extension — a new `saved_chats` table for explicitly saved conversations (see Context). ## Tasks 1. `01_saved_chat_model.md` — the `SavedChat` model + migration `0008_saved_chats` (+ migration test). 2. `02_chats_api.md` — admin-only CRUD under `/api/chats` + schemas + integration tests. 3. `03_save_chat_ui.md` — the chat page: Save button, `?chat=` load, upsert semantics, live-region feedback, absent for anonymous. 4. `04_history_page.md` — `history.html` + `history.js` (full-width table, Open + two-step Delete), the `#nav-history` admin-only nav link on every page, the cache-busting page registration, CSS. 5. `05_e2e_chat_history.md` — the story Playwright suite + regressions + commit. ## Testing & Quality - Integration: `tests/integration/test_chats_api.py` (the CRUD contract: 403 anonymous, create auto-title, list order, get, put replacement, delete 404/204, message-shape validation). - Integration: `tests/integration/test_migration_0008.py` (the house migration-test pattern from `test_migration_0007.py`). - Coverage: **>90%** on `app/`. - E2E (mandatory, A16): `tests/e2e/test_chat_history.py`, run in isolation. ## Completion Criteria - [ ] Admin: Save on the chat page stores the conversation (auto-title = first question, 120-char cap); re-Save on the same conversation updates the same row; New chat unlinks. - [ ] `/history.html` (admin) lists saved chats in a **full-width** table (Title, Messages, Updated, Actions); a row's title opens `/?chat=` and the chat renders the stored conversation (sources, thinking, stopped notes, deflection chips — pixel-identical to the local restore); a subsequent Save updates that row. - [ ] Delete removes the row (inline two-step confirm, no `window.confirm`); an unknown id 404s; `GET /api/chats` + the served `/history.html` carry the cache-busting contract (no-cache + `?v=` rewrite). - [ ] Anonymous: no Save button, no History nav link, `/api/chats*` → 403, `/history.html` shows the gated state without fetching `/api/chats`. - [ ] `uv run pytest` green; coverage TOTAL >90%. - [ ] `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` green in isolation (DB up). - [ ] Regression E2E suites green in isolation: `test_chat_persistence.py`, `test_nav_consistency.py`, `test_shared_header.py`, `test_admin_auth.py`, `test_cache_busting.py`. - [ ] `uv run ruff check . && uv run pyright` clean. - [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`. ## Locked decisions - **Owner-locked extension (2026-08-29, recorded per AGENTS.md rule 3):** `saved_chats` in Postgres stores **only** conversations the owner explicitly saves; `/api/chat` stays stateless; phase 14's local persistence is unchanged (the localStorage session keeps working exactly as before — saving is an additional, explicit action). - **Owner-locked (2026-08-29):** Save/History is **admin-only** (no account system — anonymous rows would be unfindable); absent-not-hidden for anonymous (phase 16); auto-title, no rename UI in v1 (the schema still accepts an optional `title`); re-Save = upsert of the same row; `?chat=` replaces the local conversation and links it; inline two-step delete confirm (no `window.confirm`). - **A16/A17 honoured** — one story E2E suite, one atomic commit. ## Commit ```bash git add -A .agents/ app/ alembic/versions/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): save and view chat history — admin-only saved_chats, History page, open-a-chat return" ```