Files
outline-sync/FILES_VIEWER_PRD.md
domverse 49ab2a1599
All checks were successful
Deploy / deploy (push) Successful in 12s
feat: add vault download button and markdown viewer PRD to /files page
Adds GET /files/download endpoint that streams all vault files (excl.
.git) as a deflate-compressed vault.zip. Adds Download All button to
the /files page header. Also adds FILES_VIEWER_PRD.md planning doc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 19:35:25 +01:00

7.5 KiB

PRD: Markdown Viewer for /files Page

Status: Draft Date: 2026-03-13 Scope: outline-sync webui — /files page enhancement


Problem

The /files page currently displays a static file tree showing document names and file sizes. Files are not interactive — there is no way to read document content within the web UI. Users who want to verify a document's content must access the vault directory directly (container shell, WebDAV mount, or Obsidian).

This breaks the self-contained nature of the web UI and is a missing piece in the read workflow: pull → verify content → push.


Goal

Make every .md file in the vault tree clickable, opening a clean rendered view of the document without leaving the web UI.


Library Assessment

The awesome-markdown-editors list is focused primarily on editors (WYSIWYG, Monaco-based, cloud tools), not standalone viewers/renderers. The relevant rendering engines mentioned are markdown-it, marked.js, highlight.js, KaTeX, and Mermaid.

For this project's architecture — a pure FastAPI app with all HTML/CSS/JS inline in webui.py, no npm, no build system, no CDN currently in use — the viable options are:

Approach Library Pro Con
Server-side (Python) mistune No CDN, consistent with existing diff rendering, works offline/air-gapped New pip dependency
Server-side (Python) markdown2 Same as above, supports fenced code + tables via extras New pip dependency
Client-side (CDN) marked.js Zero Python deps, ~50 KB CDN call from container; breaks if offline
Client-side (CDN) markdown-it Plugin ecosystem Same CDN concern + more complex

Recommendation: mistune (server-side) Reason: The existing diff renderer (difflib.HtmlDiff) is already server-side. mistune is a lightweight pure-Python CommonMark-compatible parser (~25 KB, no transitive deps). It keeps the architecture consistent, avoids CDN latency from inside Docker, and doesn't require any frontend changes to _SCRIPT.

markdown2 is an equally valid alternative with a nearly identical API.


User Stories

  1. As a user, I can click any .md file in the vault tree to open a rendered view of its contents.
  2. As a user, I can see the document's markdown rendered as formatted HTML (headings, lists, code blocks, bold/italic, tables).
  3. As a user, the YAML frontmatter block (--- ... ---) is either hidden or rendered as a collapsible metadata section, not shown as raw YAML in the document body.
  4. As a user, I can navigate back to the file tree with a single click (back link or breadcrumb).
  5. As a user, non-markdown files (e.g. .json, .txt) show a plain text fallback, not an error.
  6. As a user, attempting to view a file outside the vault directory results in a 403, not a path traversal.

Scope

In scope

  • New route GET /files/view?path=<relative-path> that serves a rendered HTML view of the file
  • Frontmatter stripped from rendered output; optionally shown as a collapsible <details> block with the sync metadata fields
  • Clickable file links in _build_tree_html() for .md files (and other text files)
  • Back-navigation link to /files
  • Path traversal protection: resolve the path and assert it is within VAULT_DIR
  • mistune added to Dockerfile pip install line

Out of scope

  • Editing files within the viewer (separate feature)
  • Syntax highlighting for code blocks (can be added later with pygments)
  • Full-text search across vault files
  • Non-text binary files (images, PDFs)
  • Live refresh / watch mode

Technical Design

New route

GET /files/view?path=<url-encoded-relative-path>
  • Decode and resolve path relative to VAULT_DIR
  • Assert resolved path is inside VAULT_DIR (403 if not)
  • Assert file exists and is a regular file (404 if not)
  • Read content as UTF-8
  • Strip frontmatter using existing parse_frontmatter() from outline_sync.py
  • Render body with mistune.create_markdown()
  • Wrap in _page() with a back-link header card and the rendered content card

File tree changes

In _build_tree_html(), wrap .md file names in anchor tags:

<a href="/files/view?path=<encoded-relative-path>">{item.name}</a>

Non-.md files remain as plain <code> labels (or optionally link to a raw text view).

Frontmatter handling

Use the already-existing parse_frontmatter() helper to split metadata from body before rendering. Optionally render frontmatter fields in a collapsed <details> block styled consistently with the existing card/badge UI.

Path traversal protection

resolved = (VAULT_DIR / rel_path).resolve()
if not str(resolved).startswith(str(VAULT_DIR.resolve())):
    raise HTTPException(status_code=403)

Dependency change

Dockerfile pip install line gains mistune:

pip install --no-cache-dir requests fastapi "uvicorn[standard]" pydantic mistune

UI Design

File tree.md files become blue underlined links. Non-markdown files stay as grey <code> labels.

Viewer page layout:

┌──────────────────────────────────────────────────────┐
│  ← Back to Files          Collection/Document.md     │
├──────────────────────────────────────────────────────┤
│  [▶ Sync metadata]  (collapsed <details>)            │
├──────────────────────────────────────────────────────┤
│                                                      │
│  # Document Title                                    │
│                                                      │
│  Rendered markdown body...                           │
│                                                      │
└──────────────────────────────────────────────────────┘

Styling reuses existing .card and _BASE_CSS variables — no new CSS classes needed beyond a .md-body prose wrapper (max-width, line-height, heading sizes).


Acceptance Criteria

  • Clicking a .md file in the vault tree navigates to /files/view?path=...
  • Rendered page shows formatted headings, paragraphs, lists, bold/italic, code blocks, and tables
  • YAML frontmatter is not visible in the rendered body
  • Requesting /files/view?path=../../etc/passwd returns 403
  • Requesting a non-existent path returns 404
  • Back link returns to /files and preserves the tree state
  • mistune is installed in the Docker image (Dockerfile updated)
  • No new test files required for MVP, but the new route is covered by a basic integration test asserting 200, 403, 404

Risks & Mitigations

Risk Mitigation
Path traversal to read secrets outside vault Strict resolve() + prefix check; test explicitly
mistune API changes (v2 vs v3) Pin version in Dockerfile; use mistune.create_markdown() stable API
Very large files causing slow response Enforce a max file size read limit (e.g. 2 MB) with a warning
Malformed/binary .md files Wrap read in try/except, fall back to plain <pre> block