Files
solopm-server/plans/08-collaboration.md

3.7 KiB

08 — Collaboration: Comments, Attachments, Links, Wiki, Markdown, Watchers

Comments

  • Flat list per issue, chronological (no threading); markdown bodies
  • Edit: author only; sets updated_at; UI shows "(edited)"
  • Delete: soft; author or project owner; UI shows a "comment deleted" tombstone (author + timestamp, no body)
  • Resolvable: any member can resolve/unresolve (resolved_at, resolved_by); resolved comments render collapsed with a "resolved by X" header; issue detail shows resolved count
  • Emit comment.created/updated/resolved/unresolved/deleted; comment.created payload carries parsed mentions

Markdown pipeline

  • Client: markdown-it + DOMPurify sanitization; single shared MarkdownView component; supported: CommonMark + tables, strikethrough, task lists, fenced code with highlight.js, autolink
  • Server (on save of any markdown field — issue/project/epic descriptions, comments, wiki bodies):
    • Parse @username mentions (word-boundary, against existing usernames) → into the event payload for notification fan-out; mentions inside code blocks/spans are ignored
    • Parse #N references (this project's issues) → validated list into event payload; used for cross-link rendering and future relation hints
    • Server does NOT render HTML (client renders); server only extracts entities
  • Client renders @username as a profile-ish chip and #N as a router-link to the issue (resolve via a lightweight GET /projects/{id}/issues/{num} title lookup, cached)
  • Paste/drag an image into MarkdownEditor → uploads as attachment → inserts ![name](/files/{path})

Attachments

  • Upload targets: project, issue, comment (multipart; ≤ MAX_UPLOAD_MB; any mime type, but SVG served with Content-Type: text/plain nosniff to avoid stored XSS)
  • Disk path: UPLOAD_DIR/project_{pid}/{uploader_id}_{yyyymmddhhmmss}_{rand16}.{ext} — path stored relative in DB; never trust client filenames for the path (original name kept separately for download)
  • Serving: GET /files/{path} checks membership of the owning project (lookup by attachments row, not by path prefix), sets Content-Disposition
  • Delete: soft; file stays on disk until cull (12-retention.md deletes row + file together)
  • Image attachments get a thumbnail treatment client-side only (no server thumbnailing in v1)
  • Emit attachment.uploaded/deleted
  • Simple URL + optional title on project/issue/comment; validated absolute http(s) URL
  • Emit link.added/removed

Wiki

  • Per-project markdown pages; slug generated from title (kebab-case, deduplicated with -2 suffix); slug is stable after creation (title edits don't re-slug)
  • Page list + view + edit (single editor, last-write-wins with updated_at conflict warning: PATCH carries expect_updated_at, 409 on mismatch)
  • updated_by tracked; no page history in v1 (17-decisions.md)
  • Wiki bodies participate in mentions/#refs parsing and search indexing
  • Emit wiki.created/updated/deleted

Watchers

  • Any member can watch/unwatch any issue in their projects; watcher list shown on issue detail
  • Auto-watch (insert if no row exists; never flip an existing muted=true row):
    • creator on issue create
    • assignee on assign
    • commenter on comment
    • mentioned user on mention (issue description or comment)
  • Unwatch = mute: DELETE /watch sets muted = true (row kept) so no future auto-watch resurrects the subscription; PUT /watch clears muted
  • Watchers feed notification fan-out (09-notifications-realtime.md); muted watchers receive nothing