feat: add vault download button and markdown viewer PRD to /files page
All checks were successful
Deploy / deploy (push) Successful in 12s

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>
This commit is contained in:
2026-03-13 19:35:25 +01:00
parent 9e143a8156
commit 49ab2a1599
2 changed files with 185 additions and 1 deletions

162
FILES_VIEWER_PRD.md Normal file
View File

@@ -0,0 +1,162 @@
# 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](https://github.com/mundimark/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
```python
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:
```html
<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
```python
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 |