# Phase 51 — Share a Chat by Link (Anonymous View) **Source:** `TODO.md` L6 — "Need a way to share a chat with a link so others can see it anonymously." **Story:** n/a (TODO-derived — owner roadmap confirmation 2026-08-29) **Context:** Builds on phase 50's `saved_chats` rows: sharing is a token on a saved chat. The public surface is a **new anonymous page** `/shared/` (a real route — the static mount cannot serve a dynamic path) rendering a read-only copy of the conversation through the same record shape (thinking block, tool lines, source chips, stopped note) — no composer, no controls. The documents API is admin-only (phase 16), so a guest's source chips are plain text (owner-locked). The cache-busting middleware treats known HTML paths (the `HTML_PAGES` tuple in `app/core/caching.py`) as revalidate + `?v=`-rewrite pages; the shared page joins that contract by path prefix. ## Objective The owner can turn a saved chat into a public link (`/shared/`); anyone with the link sees the conversation read-only, anonymously; unsharing revokes it. ## Dependencies - `50_chat_history` (todo) — the `saved_chats` row, the Save flow, and the History table the share actions extend. ## Tasks 1. `01_share_token.md` — migration `0009_saved_chat_share_token` + the share/unshare/public-read API + the `/shared/` page route + the middleware prefix. 2. `02_share_ui.md` — the Share button (chat page, save-then-share in one action) + the History table's share column (create/copy/unshare). 3. `03_shared_page.md` — `shared.html` + `shared.js`: the anonymous read-only rendering. 4. `04_e2e_share_chat.md` — the story Playwright suite + regressions + commit. ## Testing & Quality - Integration: share/unshare/public-read contract (token shape, idempotent share, unshare revokes, a wrong token 404s, no admin needed to read, `updated_at` untouched by share/unshare) + `tests/integration/test_migration_0009.py`. - Coverage: **>90%** on `app/`. - E2E (mandatory, A16): `tests/e2e/test_share_chat.py`, run in isolation. ## Completion Criteria - [ ] Admin: the Share button on the chat page (saved or unsaved conversation) and the History row both produce/copy the `/shared/` link; an unsaved conversation is saved + shared in one action. - [ ] A fresh anonymous context opening `/shared/` sees the full conversation read-only (thinking collapsed, tool lines, the stopped note where present, source chips as plain text) with **no** composer, Save, Share, Tune, or Retry anywhere; a wrong/revoked token shows the "invalid or revoked" state. - [ ] Unshare revokes: the same URL shows the invalid state afterwards; the served shared page carries the cache-busting contract (no-cache + `?v=` rewrite). - [ ] `uv run pytest` green; coverage TOTAL >90%. - [ ] `uv run pytest tests/e2e/test_share_chat.py -v --no-cov` green in isolation (DB up). - [ ] Regression E2E suites green in isolation: `test_chat_history.py`, `test_chat_persistence.py`, `test_smoke.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 (2026-08-29, roadmap confirmation):** share links are public by design (128-bit `uuid4` token; unshare revokes); the shared page renders the full conversation read-only (thinking collapsed) with **zero** interactive controls; source chips are plain text (guests cannot open documents — the documents API is admin-only); clipboard copy with an inline-link fallback (a homelab http origin may not be a secure context). - **The A10 extension (phase 50, owner permission 2026-08-29) unchanged** — sharing reuses the already-stored row; no new storage beyond the token column. - **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): share a chat by link — anonymous read-only /shared/ page, share/unshare" ```