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

53 lines
3.7 KiB
Markdown

# 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](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`
## Links
- [ ] 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](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](09-notifications-realtime.md)); muted watchers receive nothing