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>
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
- As a user, I can click any
.mdfile in the vault tree to open a rendered view of its contents. - As a user, I can see the document's markdown rendered as formatted HTML (headings, lists, code blocks, bold/italic, tables).
- 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. - As a user, I can navigate back to the file tree with a single click (back link or breadcrumb).
- As a user, non-markdown files (e.g.
.json,.txt) show a plain text fallback, not an error. - 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.mdfiles (and other text files) - Back-navigation link to
/files - Path traversal protection: resolve the path and assert it is within
VAULT_DIR mistuneadded toDockerfilepip 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()fromoutline_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
.mdfile 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/passwdreturns 403 - Requesting a non-existent path returns 404
- Back link returns to
/filesand preserves the tree state mistuneis 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 |