chore(agent): track .agent/ planning tree in git
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s

Remove the blanket .agent/ gitignore so the phase roadmap, user
stories, reports, and PLAN.md are versioned with the code. Only
runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log)
remain ignored. Update AGENTS.md git protocol rule to match.
This commit is contained in:
2026-09-01 10:18:22 -04:00
parent 5fa620fde5
commit 4971e2859d
818 changed files with 23964 additions and 4 deletions
@@ -0,0 +1,48 @@
# 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=<id>` 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=<id>` 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 `.agent/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=<id>` 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 .agent/ 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"
```