Files
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

3.3 KiB

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:.