Files
outline-sync/tests/USER_STORIES.md
2026-03-07 20:54:59 +01:00

211 lines
12 KiB
Markdown

# User Stories — Outline Sync Web UI
**Derived from:** WEBUI_PRD.md v2.0
**Date:** 2026-03-07
Each story follows the format: **As a [user], I want [goal] so that [benefit].**
Acceptance criteria map directly to automated test IDs in the corresponding `test_phase_*.py` files.
---
## Phase A — WebDAV Container
**US-A1** — As a user with Obsidian running locally, I want my vault files to sync automatically to the VPS via WebDAV, so that I do not need terminal access for file transfer.
- AC: A file created in the vault directory is retrievable via WebDAV GET within the sync interval.
- AC: A file updated locally appears updated on the WebDAV server after sync.
- AC: Deleted files are removed from the WebDAV share.
- Tests: `test_phase_a_webdav.py::TestWebDAVFileOps`
**US-A2** — As a system administrator, I want the WebDAV endpoint protected by basic auth and Tailscale network isolation, so that the vault is not publicly accessible.
- AC: Unauthenticated requests return 401.
- AC: Requests with valid credentials return 200.
- AC: The WebDAV port is not bound to the public interface.
- Tests: `test_phase_a_webdav.py::TestWebDAVAuth`
**US-A3** — As a user, I want the `.git/` directory excluded from WebDAV access, so that git internals are not exposed or corrupted by Obsidian.
- AC: GET request to `/.git/` returns 403 or 404.
- AC: Obsidian plugin cannot overwrite `.git/` files via WebDAV.
- Tests: `test_phase_a_webdav.py::TestWebDAVGitProtection`
**US-A4** — As an Obsidian user, I want WebDAV to support bidirectional file sync (upload, download, delete), so that both push and pull directions work without manual steps.
- AC: PROPFIND returns correct file listing.
- AC: PUT creates/updates files.
- AC: DELETE removes files.
- Tests: `test_phase_a_webdav.py::TestWebDAVMethods`
---
## Phase B — Read-Only Dashboard
**US-B1** — As a user, I want to open `https://sync.domverse.de` and immediately see the current vault state, so that I know if anything needs attention before syncing.
- AC: GET `/` returns HTTP 200 with HTML content.
- AC: Page contains vault status badge (Clean / Dirty / Conflicts).
- AC: Page shows count of pending local changes.
- Tests: `test_phase_b_dashboard.py::TestDashboardPage`
**US-B2** — As a user, I want to see when the vault was last pulled from Outline and last pushed to Outline, so that I can judge how stale my local state is.
- AC: GET `/status` returns JSON with `last_pull`, `last_push` timestamps.
- AC: Dashboard renders these timestamps in human-readable form.
- Tests: `test_phase_b_dashboard.py::TestStatusEndpoint`
**US-B3** — As a user, I want the dashboard to show a conflict warning badge when git merge conflicts are present, so that I do not accidentally push broken files.
- AC: When conflict files exist, `/status` includes `conflicts: N` where N > 0.
- AC: Dashboard shows warning banner with link to `/conflicts`.
- Tests: `test_phase_b_dashboard.py::TestConflictBadge`
**US-B4** — As a user, I want the dashboard to show how many files Obsidian has written via WebDAV that are not yet pushed to Outline, so that I know the push button's scope.
- AC: Pending count = `git diff outline..main --name-only | wc -l`.
- AC: Count updates on each page load without manual refresh.
- Tests: `test_phase_b_dashboard.py::TestPendingCount`
---
## Phase C — Pull with Live Output
**US-C1** — As a user, I want to click "Get from Outline" and see live streaming output in the browser, so that I can monitor progress without terminal access.
- AC: POST `/pull` responds immediately with a `job_id`.
- AC: GET `/stream/{job_id}` returns `text/event-stream` content type.
- AC: SSE stream emits at least one `data:` event per document processed.
- AC: Stream ends with a `done` event containing a summary.
- Tests: `test_phase_c_pull.py::TestPullStreaming`
**US-C2** — As a user, I want the pull to fetch new Outline documents and update them in the vault, so that Obsidian shows the latest wiki content.
- AC: After pull, new documents appear as `.md` files in the vault.
- AC: Modified documents have updated content.
- AC: The `outline` branch is advanced to reflect the new Outline state.
- Tests: `test_phase_c_pull.py::TestPullContent`
**US-C3** — As a user, I want the pull operation to be idempotent when nothing changed in Outline, so that repeated pulls are safe and fast.
- AC: Pull with no Outline changes returns success with "0 changes" summary.
- AC: No git commits are made when there are no changes.
- Tests: `test_phase_c_pull.py::TestPullIdempotent`
**US-C4** — As a user, I want only one sync job running at a time, so that concurrent pull/push operations do not corrupt the vault.
- AC: Starting a pull while one is in progress returns HTTP 409 Conflict.
- AC: Job lock is released when the stream closes (done or error).
- Tests: `test_phase_c_pull.py::TestJobLock`
---
## Phase D — Pending Changes View
**US-D1** — As a user, I want to see a structured list of pending changes before pushing, so that I can review what will be sent to Outline.
- AC: GET `/changes` returns 200 with a list of change objects.
- AC: Each item has a `path`, `status` (modified/added/renamed/deleted) and `action` (what the sync engine will do).
- Tests: `test_phase_d_changes.py::TestChangesEndpoint`
**US-D2** — As a user, I want modified files listed separately from new files and deleted files, so that I understand the scope of each change type.
- AC: `status=modified` for files changed since last outline branch commit.
- AC: `status=added` for files not on the outline branch at all.
- AC: `status=deleted` for files removed from main but still on outline.
- AC: `status=renamed` with both `from` and `to` paths.
- Tests: `test_phase_d_changes.py::TestChangeCategories`
**US-D3** — As a user, I want to preview the diff for a modified file inline, so that I can confirm the content before pushing.
- AC: GET `/diff/{encoded_path}` returns an HTML fragment with two columns.
- AC: Left column shows the outline branch version, right shows main branch version.
- AC: Added lines are highlighted green, removed lines red.
- Tests: `test_phase_d_changes.py::TestDiffPreview`
**US-D4** — As a user, I want deleted files shown as "skipped" when deletions are disabled in settings, so that I know why they are not being removed from Outline.
- AC: When `allow_deletions=false`, deleted files appear with `action=skip`.
- AC: Reason text explains deletions are disabled.
- Tests: `test_phase_d_changes.py::TestDeletedFilesSkipped`
---
## Phase E — Push with Live Output
**US-E1** — As a user, I want to click "Send to Outline" and see live streaming output, so that I can monitor progress for each file.
- AC: POST `/push` returns a `job_id`.
- AC: SSE stream emits one event per file with status (created/updated/skipped/error).
- AC: Final event contains summary counts (created, updated, skipped, errors).
- Tests: `test_phase_e_push.py::TestPushStreaming`
**US-E2** — As a user, I want new Obsidian files to appear in Outline under the correct collection and parent, so that the hierarchy is preserved.
- AC: A file `Projekte/NewNote.md` (no frontmatter) is created in Outline under the "Projekte" collection.
- AC: After push, the file receives frontmatter with `outline_id`.
- AC: The updated file is committed and becomes readable via WebDAV.
- Tests: `test_phase_e_push.py::TestNewFileCreation`
**US-E3** — As a user, I want push to be blocked when there are unresolved conflicts, so that I cannot push broken files with conflict markers.
- AC: When `git ls-files -u` returns conflict files, POST `/push` returns HTTP 409.
- AC: Response body includes list of conflicting paths.
- Tests: `test_phase_e_push.py::TestPushBlockedByConflicts`
**US-E4** — As a user, I want a new top-level folder in Obsidian to create a new Outline collection automatically, so that new categories do not require manual Outline setup.
- AC: Folder `NewCollection/` not mapped to any existing collection → `collections.create` called.
- AC: Documents inside the new folder are created under the new collection.
- Tests: `test_phase_e_push.py::TestNewCollectionCreation`
**US-E5** — As a user, I want the push to handle renames (file moved/title changed) without deleting and recreating the document, so that Outline document history and URL are preserved.
- AC: Renamed file detected via git rename detection.
- AC: Outline `documents.update` called (not delete+create).
- Tests: `test_phase_e_push.py::TestRenameHandling`
---
## Phase F — Conflict Resolution
**US-F1** — As a user, I want to see all version conflicts listed in the browser, so that I can resolve them without using git on the command line.
- AC: GET `/conflicts` returns list of conflict file paths.
- AC: Each item includes local timestamp and Outline edit timestamp.
- Tests: `test_phase_f_conflicts.py::TestConflictsList`
**US-F2** — As a user, I want a side-by-side diff view per conflicting file, so that I can compare my Obsidian edit with the Outline edit before choosing.
- AC: GET `/diff/{encoded_path}` for a conflict file returns two-column HTML diff.
- AC: Diff is rendered using Python `difflib` or equivalent.
- Tests: `test_phase_f_conflicts.py::TestConflictDiff`
**US-F3** — As a user, I want to click "Keep mine" to accept my local Obsidian edit, so that my changes win.
- AC: POST `/resolve` with `{file: "path", accept: "local"}` resolves conflict in favour of local.
- AC: Conflict markers are removed from the file.
- AC: File is committed to main branch.
- Tests: `test_phase_f_conflicts.py::TestResolveLocal`
**US-F4** — As a user, I want to click "Keep Outline's" to accept the Outline version, so that the wiki state wins.
- AC: POST `/resolve` with `{file: "path", accept: "remote"}` resolves in favour of outline branch.
- AC: Conflict markers are removed.
- AC: File is committed to main branch.
- Tests: `test_phase_f_conflicts.py::TestResolveRemote`
**US-F5** — As a user, I want the system to reject invalid file paths in resolve requests, so that an attacker cannot trigger arbitrary git operations via the UI.
- AC: `/resolve` with a path not in the conflict list returns HTTP 422.
- AC: Path traversal attempts (`../`) return HTTP 422.
- Tests: `test_phase_f_conflicts.py::TestResolveValidation`
**US-F6** — As a user, I want to be redirected to the dashboard after resolving all conflicts, with "Push now available" displayed, so that the workflow continues naturally.
- AC: After last conflict resolved, GET `/conflicts` returns empty list.
- AC: Dashboard status badge updates to show clean/push-ready state.
- Tests: `test_phase_f_conflicts.py::TestAllConflictsResolved`
---
## Phase G — Sync History
**US-G1** — As a user, I want to view a chronological history of all sync operations, so that I can audit what changed and when.
- AC: GET `/history` returns HTTP 200 with HTML content.
- AC: Sync entries are shown in reverse chronological order.
- AC: Each entry shows: timestamp, direction (pull/push), files affected, status.
- Tests: `test_phase_g_history.py::TestHistoryPage`
**US-G2** — As a user, I want the history sourced from `_sync_log.md`, so that it remains readable as a plain Obsidian note.
- AC: `_sync_log.md` in vault root is parsed into structured records.
- AC: Entries are displayed as an HTML table, not raw markdown.
- Tests: `test_phase_g_history.py::TestSyncLogParsing`
---
## End-to-End Flows
**US-E2E1** — As a user, I want the full Obsidian → Outline flow to work without terminal access.
- Tests: `test_e2e.py::TestObsidianToOutlineFlow`
**US-E2E2** — As a user, I want the full Outline → Obsidian flow to work without terminal access.
- Tests: `test_e2e.py::TestOutlineToObsidianFlow`
**US-E2E3** — As a user, I want conflicts detected and resolvable end-to-end through the browser.
- Tests: `test_e2e.py::TestConflictResolutionFlow`
**US-E2E4** — As a user, I want a new file created in Obsidian to reach Outline with correct hierarchy, frontmatter written back, and the ID visible in Obsidian on the next sync.
- Tests: `test_e2e.py::TestNewFileRoundTrip`