docs: annotations & voting system design
Thread-centric model: one pin = one thread with flat replies. Server-authoritative real-time sync (all clients receive canonical events). Soft Review Mode for pin visibility without locking editing. Denormalized thread summaries for efficient panel queries. Public boards: read-only feedback, auth required to participate. Phased: comments first, then canvas pins, then voting.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# Annotations & Voting System — Design
|
||||
|
||||
## Goal
|
||||
|
||||
Add thread-centric comments (object-level and region-pinned) and per-object upvoting to refboard, turning it into a collaborative feedback tool for game dev art review and mood board curation.
|
||||
|
||||
## Core Decisions
|
||||
|
||||
| Decision | Choice |
|
||||
|----------|--------|
|
||||
| Comment anchoring | Both object-level and region-pinned (anchor_type + nullable pinX/pinY) |
|
||||
| Pin coordinates | Relative 0-1 range (survives transforms naturally) |
|
||||
| Structure | Thread-centric: one pin = one thread, flat replies inside |
|
||||
| Reply depth | Flat (v1) — root comment + flat replies, no nested threading |
|
||||
| Thread status | Enum: open, resolved, archived (with resolved_by, resolved_at) |
|
||||
| Voting | Simple upvote per object, one vote per user, toggle on/off |
|
||||
| Real-time sync | Everything live via socket events, server-authoritative |
|
||||
| Socket broadcast | Include sender — canonical event is the single codepath for all clients |
|
||||
| Canvas UX | Pin markers + side panel + Review Mode (soft, not modal) |
|
||||
| Object delete | Soft orphan — comments hidden from canvas, visible in panel |
|
||||
| Object duplicate | Comments do not copy |
|
||||
| Object ID stability | Scene object UUIDs must remain stable across saves, sync, and reloads |
|
||||
|
||||
## Critical Assumption
|
||||
|
||||
`object_id` in threads/votes references scene object UUIDs inside `canvas_state`. These IDs **must remain stable** across saves, sync, and board reloads. This is already the case in the current SceneManager (objects get UUIDs at creation, preserved through all operations), but it is a hard dependency for this feature.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `comment_threads` table
|
||||
|
||||
Thread is the anchor — one pin = one thread. Carries the spatial/object binding.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS comment_threads (
|
||||
id TEXT PRIMARY KEY,
|
||||
board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
||||
object_id TEXT NOT NULL, -- scene object UUID (stable across saves)
|
||||
anchor_type TEXT NOT NULL DEFAULT 'object', -- 'object' | 'point' (future: 'rect')
|
||||
pin_x REAL, -- null for anchor_type='object', 0-1 for 'point'
|
||||
pin_y REAL,
|
||||
status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'resolved' | 'archived'
|
||||
resolved_by TEXT REFERENCES users(id),
|
||||
resolved_at TEXT,
|
||||
comment_count INTEGER NOT NULL DEFAULT 1, -- denormalized, includes root comment
|
||||
last_commented_at TEXT, -- denormalized, updated on each reply
|
||||
last_commented_by TEXT, -- denormalized, user_id of last commenter
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_threads_board ON comment_threads(board_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_threads_object ON comment_threads(board_id, object_id);
|
||||
```
|
||||
|
||||
### `comments` table
|
||||
|
||||
Flat replies inside threads. No anchor fields — positioning belongs to the thread.
|
||||
No parent_id in v1 — all replies are flat, ordered by created_at.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
author_name TEXT NOT NULL, -- snapshot at creation time
|
||||
author_color TEXT, -- user color snapshot for avatar
|
||||
content TEXT NOT NULL,
|
||||
edited_at TEXT, -- set on edit, null if never edited
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_thread ON comments(thread_id);
|
||||
```
|
||||
|
||||
### `object_votes` table
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS object_votes (
|
||||
board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
||||
object_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (board_id, object_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_votes_object ON object_votes(board_id, object_id);
|
||||
```
|
||||
|
||||
### Object lifecycle
|
||||
|
||||
**Transform**: Pins derive position from object bounds each frame. Relative coords (0-1) survive move/scale/rotate automatically.
|
||||
|
||||
**Delete**: Don't hard-delete threads. Threads whose object_id no longer exists in canvas_state are "orphaned" — hidden from canvas pins but visible in panel under a "Deleted items" section. Review history matters.
|
||||
|
||||
**Duplicate**: Comments do not copy with the object. Clean slate for the duplicate.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Threads & Comments
|
||||
|
||||
```
|
||||
GET /api/boards/:boardId/threads — all threads + comments for board
|
||||
POST /api/boards/:boardId/threads — create thread + first comment
|
||||
{ object_id, anchor_type, pin_x?, pin_y?, content }
|
||||
PATCH /api/boards/:boardId/threads/:threadId — update status { status: 'open'|'resolved'|'archived' }
|
||||
DELETE /api/boards/:boardId/threads/:threadId — delete thread + all comments
|
||||
|
||||
POST /api/boards/:boardId/threads/:threadId/comments — add reply { content }
|
||||
PUT /api/boards/:boardId/threads/:threadId/comments/:id — edit own comment { content }
|
||||
DELETE /api/boards/:boardId/threads/:threadId/comments/:id — delete own comment
|
||||
```
|
||||
|
||||
### Votes
|
||||
|
||||
```
|
||||
GET /api/boards/:boardId/votes — all votes for board
|
||||
POST /api/boards/:boardId/votes — toggle vote { object_id }
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
- **Public (unauthenticated)**: read-only — can view threads and vote counts, cannot comment or vote
|
||||
- **Viewer** (collection member): read threads, add threads/comments, vote
|
||||
- **Editor**: all viewer + resolve/archive threads
|
||||
- **Owner**: all editor + delete any thread/comment
|
||||
|
||||
Public boards expose feedback as read-only. Participating in the review loop requires authentication and collection membership.
|
||||
|
||||
---
|
||||
|
||||
## Socket Events
|
||||
|
||||
Server-authoritative: client sends REST request → server writes DB → server broadcasts canonical event to **all clients in the room including sender**. The REST response returns minimal confirmation (id, timestamp); the socket event is the single codepath that updates local state for everyone.
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `thread:add` | `{ boardId, thread, comment }` |
|
||||
| `thread:status` | `{ boardId, threadId, status, resolvedBy, resolvedAt }` |
|
||||
| `thread:delete` | `{ boardId, threadId }` |
|
||||
| `comment:add` | `{ boardId, threadId, comment }` |
|
||||
| `comment:update` | `{ boardId, threadId, commentId, content, editedAt }` |
|
||||
| `comment:delete` | `{ boardId, threadId, commentId }` |
|
||||
| `vote:toggle` | `{ boardId, objectId, userId, active }` |
|
||||
|
||||
All broadcast to `board:${boardId}` room.
|
||||
|
||||
---
|
||||
|
||||
## Canvas UX — Pin Markers
|
||||
|
||||
### Pin appearance
|
||||
- Small circle (16-20px at 1x zoom, scale-independent)
|
||||
- Unresolved: red fill with white thread count
|
||||
- Resolved: gray fill, dimmed (hidden by default, toggle to show)
|
||||
- Region pins: positioned at `(obj.x + pinX * obj.w, obj.y + pinY * obj.h)` in world space
|
||||
- Object-level pins: top-right corner of object bounding box
|
||||
- Do NOT render comment text on canvas — pins are minimal dots only
|
||||
|
||||
### Vote badge
|
||||
- Small upvote arrow + count pill at bottom-right of object
|
||||
- Only visible when count > 0 or in Review Mode
|
||||
|
||||
### Interaction
|
||||
- Click pin → opens panel scrolled to that thread
|
||||
- Hover pin → tooltip: author + first line of content
|
||||
- Pins and vote badges visible in **Review Mode**
|
||||
|
||||
---
|
||||
|
||||
## Review Mode
|
||||
|
||||
Soft toggle in toolbar — not a separate locked mode. When enabled:
|
||||
|
||||
- Comment pins and vote badges become visible on canvas
|
||||
- Editing still works — selection, move, resize all functional
|
||||
- A "Place comment" subtool activates pin placement (crosshair cursor, click on object to place)
|
||||
- Panel auto-opens to feedback view
|
||||
- Exiting the subtool returns to normal select behavior
|
||||
|
||||
When disabled:
|
||||
|
||||
- Pins and badges hidden — clean canvas for arranging/editing
|
||||
- Panel can still be opened manually but defaults to layer view
|
||||
|
||||
This keeps Review Mode useful without trapping users in a separate workflow.
|
||||
|
||||
---
|
||||
|
||||
## Panel UX — Feedback Panel
|
||||
|
||||
### Layout
|
||||
- Right-side panel, 280px wide, same pattern as LayerPanel
|
||||
- Toggled via toolbar button (comment icon) or auto-opens in Review Mode
|
||||
- Tabs or toggle to switch between Layer view and Feedback view
|
||||
|
||||
### Filters (top of panel)
|
||||
- Unresolved only (default on)
|
||||
- Mine only
|
||||
- Selected object
|
||||
- Most voted
|
||||
|
||||
### Sort options
|
||||
- Newest activity
|
||||
- Unresolved first
|
||||
- By vote count
|
||||
|
||||
### Views
|
||||
|
||||
**1. Overview (no selection)**
|
||||
- List of objects with active threads, grouped by object
|
||||
- Each row: object name, unresolved thread count badge, vote count + vote button
|
||||
- "Top voted" section at top for lightweight poll results
|
||||
- "Deleted items" collapsible section at bottom for orphaned threads
|
||||
|
||||
**2. Object detail (object selected or pin clicked)**
|
||||
- Object name + vote button + count at top
|
||||
- List of threads for that object
|
||||
- Each thread: pin indicator (object/point), author, timestamp, first comment preview, reply count, status badge
|
||||
- Click to expand
|
||||
|
||||
**3. Thread expanded**
|
||||
- Root comment + flat replies in chronological order
|
||||
- Reply input at bottom
|
||||
- Resolve/archive button (editor+)
|
||||
- "Edited" label on modified comments
|
||||
- Delete button on own comments
|
||||
- "Jump to object" button — pans/zooms viewport to center on the target object
|
||||
|
||||
---
|
||||
|
||||
## Frontend State
|
||||
|
||||
### Loaded on board open
|
||||
- `GET /api/boards/:boardId/threads` → `Map<string, Thread>` with nested comments
|
||||
- `GET /api/boards/:boardId/votes` → `Map<string, Set<string>>` (objectId → userIds)
|
||||
|
||||
### Socket updates (single codepath for all clients including sender)
|
||||
- `thread:add` → insert thread + comment, add pin to overlay
|
||||
- `thread:status` → update status + resolved_by/at, re-render pin
|
||||
- `thread:delete` → remove thread, remove pin
|
||||
- `comment:add` → append to thread's comment list
|
||||
- `comment:update` → update comment content + edited_at
|
||||
- `comment:delete` → remove from thread's comment list
|
||||
- `vote:toggle` → add/remove userId from vote set, update badge
|
||||
|
||||
### Pin overlay
|
||||
- Separate PixiJS Container on viewport (above objects, below selection handles)
|
||||
- Re-derives positions from object world bounds on viewport change
|
||||
- Only rendered when Review Mode is active
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Ship comments first, then votes, then review mode polish.
|
||||
|
||||
### Phase 1: Comments (core value)
|
||||
1. Backend: DB schema (comment_threads, comments tables + indexes)
|
||||
2. Backend: REST endpoints for threads and comments
|
||||
3. Backend: Socket event relay in board-room.js
|
||||
4. Frontend: State management (load on board open, socket handlers)
|
||||
5. Frontend: Feedback panel (overview, object detail, thread views)
|
||||
|
||||
### Phase 2: Canvas Pins
|
||||
6. Frontend: Review Mode toggle in toolbar
|
||||
7. Frontend: Pin overlay rendering (PixiJS container)
|
||||
8. Frontend: Pin placement subtool (click-to-place region pins)
|
||||
9. Frontend: Jump-to-object from thread
|
||||
|
||||
### Phase 3: Voting
|
||||
10. Backend: object_votes table + REST endpoints + socket events
|
||||
11. Frontend: Vote button in panel + badge on canvas
|
||||
12. Frontend: "Top voted" section in panel overview
|
||||
|
||||
### Phase 4: Polish
|
||||
13. Frontend: Orphan detection (deleted objects, show in panel)
|
||||
14. Frontend: Filters and sort options in panel
|
||||
15. Frontend: "Edited" labels, timestamps, author snapshots
|
||||
Reference in New Issue
Block a user