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

12 KiB

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