# Task 03 — The shared page (anonymous read-only) **Phase:** `51_share_chat` · **Source:** `TODO.md:6` — "Need a way to share a chat with a link so others can see it anonymously." **Story:** n/a (TODO-derived) ## Objective `/shared/` renders the shared conversation for **anyone** — read-only, zero controls, nothing the share shouldn't expose — through the same record shape the chat uses. ## Work 1. `frontend/shared.html` (new) — the page scaffold as `history.html` does it: the head (charset, viewport, `assets/styles.css`), the skip link, the **identical** shared header (the nav with all the admin-only links hidden — a guest never sees them — and the auth pair with `?next=/` on the Sign-in link: a guest signing in from a shared page returns to the app root; note this in a comment), then `
` carrying the `#steering-panel` section + `#steering-announcer` (the shared panel ships on every page — phase 34) and the page content: - an `

` (JS-filled with the shared chat's title; static fallback text "Shared conversation"); - a `.shared-note` line — "Shared via Brain of Reese — read-only." (the brand resolves through the `window.BOR_BRAND` convention like the other pages); - the messages section — `
` using the **same** `.msg`/`.bubble`/`.thinking`/`.tool-calls` structure as the chat page, so the existing CSS applies unchanged (the shell maps to the 46rem centered chat column — reuse the `.chat-shell` class or a `.shared-shell` that maps to the same width rule, per the PLAN §7 column contract); - the invalid-state block `#shared-invalid` (hidden by default): "This share link is invalid or was revoked." - the app-footer (version span, like the other pages). - Scripts: `` (classic, first) + `` (the classic renderer, as `index.html` loads it) + ``. **No** `document-modal.js`, **no** composer, **no** Save/Share/Retry/Tune markup at all (owner-locked: zero controls). No-CDN rule holds (local assets only). 2. `frontend/assets/shared.js` (new) — a module: - read the token from `location.pathname` (the last path segment of `/shared/`; a malformed/missing token → show `#shared-invalid` immediately, **no fetch**). - `await initSharedHeader()` (the header works for guests — whoami anonymous, the admin links stay hidden), then `GET /api/shared/`: - 200 → set the `h1` to the title; render every message through a local `renderSharedMessage(m)` reusing the chat's record shape: user → the `.msg.user` bubble; brain → the `.msg.brain` bubble with the optional thinking block (**collapsed** — the phase-17 restore convention), the tool lines, the `is-deflected` class, the stopped note (the ~8-line `appendStoppedNote` markup duplicated locally — the per-page duplication house style), the deflection's "Maybe try" chips as **plain ``** text (not buttons — a guest tapping a chip has nowhere to go; owner-locked zero controls), and the source chips as **plain text ``** (owner-locked: guests cannot open documents — the documents API is admin-only; no `href`, no modal wiring). - 404/other → show `#shared-invalid` (the title keeps its fallback), no data rendered, no error banner. - markdown through the global `renderMarkdown` (escape-first — the stored payloads are raw text, so the renderer's XSS safety applies unchanged). 3. `frontend/assets/styles.css` — `.shared-note` (the muted meta line under the h1); the static-chip treatment scoped to the shared page (e.g. `.shared-shell .suggestion-chip { pointer-events: none; cursor: default; }` — or a distinct `.chip-static` class if cleaner; the interactive chips' styles elsewhere stay untouched); the invalid-state styling (a centered muted block); the shared shell's 46rem column mapping; the ≤640px responsive behavior (the phase-07 contract). 4. Frontend source pins (house pattern): the token parse (a malformed path → no fetch, the invalid state shows); the 404 → invalid state (no data render); **zero interactive controls** (pin: `renderSharedMessage` never calls `renderChips`/`appendTuneButton`/`appendRetryButton`, and the rendered shared messages contain no `90%** on `app/` (unchanged — frontend-only task). ## Completion Criteria - [ ] `GET /shared/` (the task-01 route) serves the page; a fresh anonymous browser renders the conversation read-only. - [ ] A wrong/revoked token shows the invalid state; no control exists anywhere on the page. - [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.