Files
brain-of-reese/.agents/user_stories/document-viewer.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

65 lines
3.3 KiB
Markdown

# Story: Clickable Document Viewer
**Phase:** `10_story_document_viewer.md` · **E2E:** `tests/e2e/test_document_viewer.py`
## Narrative
As **a user**, I want to click any document Brain cites (the source
chips under an answer) or any row in the Sources table and have the
**actual document open in the browser** — in whatever format it is — so I
can read the full context and verify the answer against my real notes.
- **Given** an answer with source chips (`Homelab/active/container_gitlab/
gitlab.md`) or the Sources page table
- **When** I click a chip or a path
- **Then** a new tab opens the viewer: title, source/format/path meta, and
the full content — markdown rendered with the existing local
escape-first renderer, other formats as escaped monospace text.
## Acceptance criteria
1. `GET /api/documents/content?source=…&path=…` → 200
`{source, path, title, format, content, indexed_at, chunks}`; 404
`{detail}` when the pair is not indexed. Served **from the database
only** — no filesystem access, hence no path-traversal surface.
2. `GET /document.html?source=…&path=…` renders the document: back link,
title, meta row (source, format badge, mono path, indexed date, chunk
count), content — md via the shared local renderer; other formats as
escaped `<pre>` (mono, horizontal scroll).
3. Chat source chips link to the viewer **in a new tab**
(`target="_blank" rel="noopener"`), accessible name intact.
4. Sources table: the path cell is a link to the viewer (new tab).
5. XSS-safe: document content is never injected as raw HTML (renderer
escapes first; raw formats use text nodes).
6. The viewer inherits the dark tech theme; the no-CDN invariant is
extended to `/document.html`; landmarks/labels/`aria-live` on the new
page.
7. Missing doc → designed not-found state with a link to the Sources
page.
8. Unit + integration green, `app/` coverage >90%, story E2E green in
isolation, one `--no-gpg-sign` commit.
## UI Visualization & Structure
- **Viewer page:** sticky header (back link + title + meta badges) +
`<main>`: markdown column centered at ≤46rem; raw content in a
full-width `pre` with `overflow-x: auto`. Dark tokens from Phase 08.
- **Chips/links:** source chips keep the mono pill; path links get
hover/focus underline; `:focus-visible` ring; ≥44px touch targets on
mobile.
- **Not-found state:** centered card (no emoji — SVG or plain text),
message + "Open Sources" link.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_document_viewer.py`** (mock LLM, seeded KB):
1. `test_source_chip_opens_document` — on-topic question → click the
`.source-chip` → a **new tab** at `/document.html?…` shows the fixture
doc's title + a known content string + the format badge.
2. `test_sources_row_links_to_viewer` — `/sources.html` → click a path
link for a `.yaml` fixture → viewer shows its raw content in a `pre`.
3. `test_markdown_renders_and_stays_xss_safe` — an md fixture containing
`<script>alert(1)</script>` renders as visible escaped text (no
execution).
4. `test_missing_doc_shows_not_found` — unknown path → not-found state +
Sources link; no console crash.
5. `test_viewer_theme_and_no_cdn` — dark background + every
`script[src]` / `link[href]` local or `data:`.