# Tasks: Detail & Real-Time Communication

**Date:** 2026-05-30  
**Status:** Draft  
**Depends on:** `tasks-board-engine`, `system-communications-notifications`, `foundation-design-system`  
**Referenced by:** `time-management`, `crm-support-center`

---

## Overview

Task detail view, rich text editor, correspondence stream (comments + system messages + attachments), and real-time WebSocket updates via Cloudflare Durable Objects.

---

## Task Detail Route

Route pattern: `/tasks/:id` — opens as a full page, but also supports intercepted modal (URL changes while board stays visible behind overlay). React Router `useModal` pattern: if navigated from board, render as modal; if direct URL, full page.

---

## Detail Layout

```
┌──────────────────────────────────────────────────────┐
│ [← Back to board]              [Assignee] [Status ▾] │
│                                                      │
│ Title (editable inline, click to edit)               │
│                                                      │
│ ┌──────────────────────┐ ┌──────────────────────┐   │
│ │   Description        │ │   Metadata           │   │
│ │   (Rich editor)      │ │   Project: ...       │   │
│ │                      │ │   Priority: ...      │   │
│ │                      │ │   Due date: ...      │   │
│ │                      │ │   Reporter: ...      │   │
│ │                      │ │   Labels: ...        │   │
│ │                      │ │   Created: ...       │   │
│ └──────────────────────┘ └──────────────────────┘   │
│                                                      │
│ Activity / Correspondence                            │
│ ┌──────────────────────────────────────────────┐    │
│ │ [message 1]                                  │    │
│ │ [system: status changed to IN_PROGRESS]      │    │
│ │ [message 2 with attachment]                  │    │
│ └──────────────────────────────────────────────┘    │
│ [Reply box]                                          │
└──────────────────────────────────────────────────────┘
```

---

## Rich Text Editor

**Library:** Tiptap v2 (ProseMirror-based)

Extensions enabled:
- StarterKit (bold, italic, headings, lists, blockquote, code, hr)
- `@tiptap/extension-image` — paste-to-upload (see below)
- `@tiptap/extension-link`
- `@tiptap/extension-mention` — @user mentions
- `@tiptap/extension-placeholder`
- `@tiptap/extension-character-count` — 50,000 char limit on task description

Storage: Tiptap JSON stored as JSONB in `tasks.description`. Rendered client-side by Tiptap read-only view. No server-side HTML rendering.

Sanitization: JSON structure is sanitized on the server — only allowed node types are accepted (whitelist). Raw HTML is never stored or served.

### Paste-to-upload (images)

When user pastes an image into the editor:
1. Intercept paste event in Tiptap
2. Upload file to `/api/attachments` (multipart)
3. API stores in R2, returns signed URL
4. Replace pasted image with `<img src="{signedUrl}" />` in editor

### Editor Accessibility

- Editor div: `role="textbox"` `aria-multiline="true"` `aria-label="Task description editor"`
- Toolbar: `role="toolbar"` `aria-label="Text formatting"`
- Toolbar buttons: `aria-pressed` for toggle states (Bold, Italic, Underline, etc.)
- Keyboard: full formatting via `⌘B`/`⌘I`/`⌘U`; shortcuts must not be overridden
- Focus: Tab enters editor; Escape exits to last focused element outside editor

### RTL Configuration

```ts
import { Direction } from '@tiptap/extension-text-direction'

const extensions = [
  // ...other extensions
  Direction.configure({
    defaultDirection: locale === 'he-IL' ? 'rtl' : 'ltr',
    // Per-paragraph direction override via toolbar toggle (↔ icon)
  }),
]
// Direction persists as dir attribute on paragraph nodes in stored JSONB
```

---

## Correspondence Stream

### Data model

```sql
task_messages (
  id UUID PRIMARY KEY,
  task_id UUID NOT NULL,
  tenant_id UUID NOT NULL,
  author_id UUID,                  -- NULL for system messages
  message_type TEXT NOT NULL,      -- 'comment' | 'system'
  content TEXT NOT NULL,           -- HTML (sanitized) for comments; plain text for system
  created_at TIMESTAMPTZ DEFAULT now(),
  deleted_at TIMESTAMPTZ           -- soft delete (author can delete own)
)

task_message_attachments (
  id UUID PRIMARY KEY,
  message_id UUID NOT NULL,
  tenant_id UUID NOT NULL,
  filename TEXT NOT NULL,
  url TEXT NOT NULL,               -- R2 signed URL
  r2_key TEXT NOT NULL,            -- R2 object key for deletion
  size_bytes INTEGER NOT NULL,
  mime_type TEXT NOT NULL,
  created_at TIMESTAMPTZ
)
```

### Message types

**User comment** (`type = 'comment'`): rich text, optionally with attachments. Author can delete own message (soft delete — "deleted" placeholder shown).

**System message** (`type = 'system'`): generated by the API on:
- Status change: `"Status changed from TODO to IN_PROGRESS by {actor}"`
- Assignment change: `"Assigned to {user} by {actor}"`
- Priority change
- Due date change

System messages are not deletable.

### HTML sanitization

Comment content is HTML (from Tiptap serialized to HTML for display).

**Server-side (before storage):** allowlist sanitization — allowed tags: `p`, `br`, `strong`, `em`, `u`, `s`, `h1`–`h3`, `ul`, `ol`, `li`, `blockquote`, `code`, `pre`, `a`, `img`. Stripped: all other tags and attributes. `a` tags: only `href` (http/https/mailto). `img` tags: only `src` (must be from `*.r2.dev` domain or configured R2 domain).

**Client-side (before render):** stored comment HTML must pass through DOMPurify before `dangerouslySetInnerHTML` — same pattern as `kb-article-editor` (spec 101). Defense-in-depth: strips anything that bypassed server sanitization.

Task _descriptions_ are Tiptap JSONB rendered through Tiptap's read-only React component (no `dangerouslySetInnerHTML`). Server-side ALLOWED_NODE_TYPES validation (see line 66) is sufficient for descriptions.

---

## Attachments

File upload on task/message:
- Max file size: 25MB
- Allowed MIME types: images, PDFs, Office docs, archives (configurable per tenant)
- Stored in R2 under key `{tenantId}/tasks/{taskId}/{uuid}-{filename}`
- API returns signed URL (1h TTL) for serving
- PDF viewer modal for `.pdf` attachments (uses `react-pdf`)

### Attachment metadata

Displayed as chips below message: filename, size, download link. PDF thumbnail generated if MIME is `application/pdf`.

---

## Real-Time Updates (delegated to real-time-infrastructure)

This module does **not** own the WebSocket transport, the per-tenant Durable Object, or the
client connection. Transport ownership is the **real-time-infrastructure** spec
(`docs/specs/2026-05-31-real-time-infrastructure.md`): one `TenantRealtimeDO` per tenant
(WebSocket Hibernation API), a single client connection at
`wss://app.zync.is/api/realtime/connect?token=<jwt>`, and a Cloudflare Queue →
consumer Worker → DO fan-out. This module **publishes** its events into that pipeline and
**consumes** them through the canonical `@zync/realtime` client — it builds no transport of
its own (no `/api/ws/:tenantId` route, no parallel DO, no direct `stub.fetch('/broadcast')`).

### Events this module publishes

Task routes fire-and-forget via `publishRealtimeEvent(env.REALTIME_QUEUE, event)` from
`@zync/realtime`. The event vocabulary lives in `@zync/realtime` (`RealtimeEventType`); this
module contributes these members (enveloped — `{ id, type, tenantId, targetUserId?, payload,
timestamp }`):

```ts
// added to @zync/realtime RealtimeEventType + payload interfaces:
'task.updated'         // payload: { taskId, changes }  (changes = Partial<TaskObject>)
'task.message_added'   // payload: { taskId, message }  (message = TaskMessage)
'task.status_changed'  // payload: { taskId, boardId, fromStatus, toStatus, movedByUserId }
```

`notification.new` is published by system-communications-notifications, not here.

### Consuming

The client subscribes through the canonical `@zync/realtime` client (`client.on(type, handler)`):
task-level events when a task detail is open; board-level — all task updates for loaded tasks.
Updates land in the Zustand store / TanStack Query cache.

### Optimistic updates

Status drag/drop: update Zustand store immediately → background PATCH → on error, revert + toast notification.

Message send: append message optimistically with `status: 'pending'` → on success, replace with server message.

---

## Audit Log

Every mutation to a task (status, assignee, priority, due date, description save) generates an audit record:

```sql
task_audit_log (
  id UUID PRIMARY KEY,
  task_id UUID NOT NULL,
  tenant_id UUID NOT NULL,
  actor_id UUID NOT NULL,
  action TEXT NOT NULL,          -- 'status_changed', 'assigned', 'edited', etc.
  previous_value JSONB,
  new_value JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
)
```

Displayed in correspondence stream as system messages (not a separate tab — interleaved with comments, sorted by `created_at`).

---

## Permissions

| Action | Required permission |
|--------|-------------------|
| View task detail | `tasks:read` |
| Add comment | `tasks:write` |
| Delete own comment | `tasks:write` |
| Upload attachment | `tasks:write` |
| Edit task description | `tasks:write` |

---

## API Endpoints

```
GET    /api/tasks/:id/messages         → correspondence stream
POST   /api/tasks/:id/messages         → add comment
DELETE /api/tasks/:id/messages/:mid    → soft-delete own message
POST   /api/attachments                → upload file, returns { url, key, ... }
DELETE /api/attachments/:id            → delete (own attachment)
GET    /api/tasks/:id/audit            → audit log
// WebSocket transport owned by real-time-infrastructure: GET /api/realtime/connect?token=<jwt>
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Rich editor | Tiptap v2 | ProseMirror foundation, extensions, paste-to-upload easy, active maintenance |
| Storage format | Tiptap JSON (JSONB) | Flexible; client renders; server doesn't parse HTML |
| Sanitization scope | Server-side on message HTML only | Task description stays as JSON; messages are HTML |
| Real-time | Cloudflare Durable Objects | CF-native, no external WebSocket service; one DO per tenant scales to hundreds of connections |
| Optimistic updates | Zustand + revert on error | Immediate UX; correction only on actual failure |
| PDF viewer | react-pdf | Client-side rendering, no server-side PDF service |
