Files
brain-of-reese/.agent/phases/complete/51_share_chat/03_shared_page.md
T
ducoterra 4971e2859d
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s
chore(agent): track .agent/ planning tree in git
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.
2026-09-01 10:18:22 -04:00

5.2 KiB

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/<token> 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 <main id="main"> carrying the #steering-panel section + #steering-announcer (the shared panel ships on every page — phase 34) and the page content:
    • an <h1 id="shared-title"> (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 — <section class="messages" id="messages" aria-label="Shared conversation"> 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: <script src="assets/brand.js"></script> (classic, first) + <script src="assets/markdown.js"></script> (the classic renderer, as index.html loads it) + <script type="module" src="/assets/shared.js"></script>. 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/<token>; 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/<token>:
      • 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 <span class="suggestion-chip"> text (not buttons — a guest tapping a chip has nowhere to go; owner-locked zero controls), and the source chips as plain text <span> (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 <button/<form — i.e. chips are spans, source chips carry no href); shared.html does not reference document-modal.js or a composer.
  • ASSUMPTION (owner-locked 2026-08-29): the shared page shows the full conversation (thinking collapsed) read-only; zero interactive controls; the "Maybe try" chips are plain text; the source chips are plain text (no document access for guests).

Testing & Quality

  • Unit: source pins as above; full suite green.
  • Coverage: >90% on app/ (unchanged — frontend-only task).

Completion Criteria

  • GET /shared/<token> (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.