Milestone 7: Ticket list and detail
Purpose: Agents see ticket list with filters, open ticket detail with thread, reply and add internal notes, manage tags and assignee; real-time via Server-Sent Events (SSE).
Exit state: Ticket list (filters, sort, pagination); ticket detail (thread, reply composer with Tiptap, internal note); tags and assignee; reply sends via M06; SSE for real-time UI updates.
Spec reference: §2.4 Ticket Management, §5.5 Ticket, §5.6 Message, §5.13 Tag, §6.4–6.6 Layout and composer, §6.10 tickets pages, §4.11 Real-time (SSE), §10.2–10.3 Entity lifecycle (tickets, messages).
Prerequisites: M06 (Email pipeline — tickets exist from inbound or manual create).
7.1 Ticket list
Tasks
-
tRPC list
- Procedure: tenantId from context; filters: status, assignee_id, inbox_id, tag_ids (array), date_from, date_to; sort: created_at, updated_at, priority; order asc/desc; pagination: cursor or offset (page, limit). Return tickets with ticket_number, subject, inbox_id, status, priority, assignee_id, updated_at, first_response_at, resolved_at; include inbox name and assignee name/avatar for display.
- Enforce role: Viewer can list; Agent and Admin can list and filter.
-
List UI
- Table or card list: columns/cards show subject, inbox badge, status badge, priority, assignee avatar, relative time (e.g. "2h ago"). Click row → navigate to
/app/[slug]/tickets/[id]. - Filter bar: status (multi or single), assignee (combobox), inbox (select), tags (multi-select), date range. Persist in URL query so shareable.
- Sort: dropdown (newest, oldest, last activity, priority). Pagination: "Load more" or pages.
- Table or card list: columns/cards show subject, inbox badge, status badge, priority, assignee avatar, relative time (e.g. "2h ago"). Click row → navigate to
-
Manual ticket creation (optional)
- "New ticket" button: form (subject, inbox_id, optional priority, optional assignee). Create ticket with first message as internal note or empty; or create with no messages and allow first reply. Section 2.4 implies tickets can be created manually; API supports it (Section 2.8). Implement tRPC create and minimal modal/page.
Acceptance criteria
| Criterion | Status |
|---|---|
| Ticket list loads with default sort (e.g. updated_at desc); filters and sort apply; pagination works. | ✅ Done |
| Agent can create ticket manually (subject, inbox); ticket appears in list. | ✅ Done |
| URL reflects filters so link can be shared. | ✅ Done |
| Ticket list stays in sync when tickets change (SSE: ticket.updated, ticket.message_added, etc. invalidate list refetch). | Pending |
7.2 Ticket detail and thread
Tasks
-
tRPC get ticket + messages
- Procedure: ticket by id (and tenantId); include inbox, assignee, tags (ticket_tags + tag names). Load messages by ticket_id order by created_at asc; exclude internal notes from "thread" if needed for display (or include with visual distinction). Return ticket + messages.
-
Detail page
- Route:
/app/[slug]/tickets/[id]. Header: subject (editable inline), inbox name, status select, assignee select, priority select. Section 6.5. - Thread: scrollable list of message bubbles. Inbound vs outbound visually distinct; internal notes with "Internal" badge and different style. Each bubble: sender (name/email), date, body (HTML sanitized for inbound — DOMPurify), attachments (links to download via pre-signed URL). Section 6.6.
- Inbound HTML: sanitize with DOMPurify before render; external images optional "Show images" toggle per spec.
- Route:
-
Reply composer
- Tiptap editor: rich text (bold, italic, lists, blockquote, links, inline code); output HTML. Section 6.6. Tab or toggle: "Reply" (outbound email) vs "Internal note". Send button: on submit call tRPC (create message outbound or internal_note); for outbound, M06 mutation creates message and enqueues send job. Optimistic update or refetch after success.
- Internal note: Tiptap Mention extension (or equivalent); typing @ triggers user picker (tenant members); selection stored in content; on submit M08 parses mention nodes and creates notifications for mentioned users (Section 2.10).
- Empty body validation; show send error (e.g. from M06 send_status failed) and "Retry" on message row.
-
Status and assignee inline
- Status: select (open, in_progress, waiting, resolved, closed); save on change (tRPC update ticket). Assignee: combobox (tenant members); save on change; trigger assignment notification in M08.
- First response: when first outbound is sent, set tickets.first_response_at. Resolved: when status set to resolved, set tickets.resolved_at (optional automation later).
Acceptance criteria
| Criterion | Status |
|---|---|
| Opening ticket shows full thread; messages in order; internal notes distinguished; inbound HTML sanitized. | ✅ Done |
| Reply (email) and internal note both work; outbound triggers send job and shows sent/failed state. | ✅ Done |
| Status and assignee editable inline; changes persist; first_response_at and resolved_at set per spec. | ✅ Done |
| Internal note composer supports @mention (user picker); M08 creates notifications for mentioned users. | Pending |
| Thread and header stay in sync when messages or ticket metadata change (SSE: ticket.updated, ticket.message_added trigger refetch). | Pending |
7.3 Tags
Tasks
-
Tags CRUD
- tRPC: list tags (tenant); create (name, color); update; delete. Unique (tenant_id, name). Section 5.13.
-
Ticket tags
- Add tag to ticket: tRPC add ticket_tag (ticket_id, tag_id); remove: delete ticket_tag. List tags on ticket in sidebar or header; tag input (multi-select or combobox) to add; remove by X on tag.
-
Filter by tag
- In ticket list filter bar, tag multi-select; filter tickets that have any of selected tags (ticket_tags join).
Acceptance criteria
| Criterion | Status |
|---|---|
| Admin/Agent can create and edit tags; add/remove tags on ticket; filter list by tag. | ✅ Done |
| Tags are tenant-scoped; unique by name per tenant. | ✅ Done |
| Ticket tag changes reflected in real time for other viewers (SSE: ticket.updated). | Pending |
7.4 Assignee
Tasks
-
Assignee select
- Load tenant members (tenant_memberships + users); show in combobox. Update ticket assignee_id via tRPC. Enforce tenant membership.
-
Notifications
- On assign: create in-app notification and send email (ticket assigned) to new assignee. Implement in M08; from M07 only the tRPC update and event/call to M08 notification trigger are needed.
Acceptance criteria
| Criterion | Status |
|---|---|
| Assignee can be set and changed; list shows assignee; filter by assignee works. | ✅ Done |
| Assignment triggers notification (M08); M07 only needs to call or emit event. | ✅ Done |
| Assignee changes reflected in list and detail in real time (SSE: ticket.assigned / ticket.updated). | Pending |
7.5 Real-time (Server-Sent Events)
Tasks
-
SSE endpoint and subscription
- Expose an authenticated SSE endpoint (e.g.
/api/app/[slug]/eventsor equivalent) that streams events to the client for the current tenant. Section 4.11. - Client subscribes when the user is on a ticket-related view (list or detail); subscription is scoped to tenant (tenant_id from session). Connection authenticated via session cookie or bearer token.
- Expose an authenticated SSE endpoint (e.g.
-
Event flow and client revalidation
- Emit events when ticket or message data changes: e.g.
ticket.updated,ticket.message_added,ticket.assigned,notification.new. Payloads are lightweight (e.g.{ entity: "ticket", id, tenant_id }or{ entity: "message", ticket_id, id }). - Client on receipt of an event: invalidate relevant React Query keys (e.g.
tickets.list,tickets.getByIdfor the affected ticket) so data refetches. Optional: refetchOnWindowFocus as fallback.
- Emit events when ticket or message data changes: e.g.
-
Scalability and distribution
- Use Redis Pub/Sub from day one for event distribution (Section 4.11): all publishers (web app after tRPC mutation, worker after DB write) publish to Redis; every app instance subscribes and forwards to its local SSE connections whose tenant matches. Same architecture for single- and multi-instance; no later migration when scaling.
- SSE connection limits and rate limiting per tenant or per user to protect against abuse.
-
Reconnect and fallback
- Client implements reconnect with backoff on connection drop. Optional: fallback to short-interval polling (e.g. refetchInterval) when SSE is unavailable or after repeated reconnect failures.
-
Redis and environment
- Staging and production: Use Upstash Redis for pub/sub (SSE event distribution).
- Local development: Add a Redis service to docker-compose so the web app and worker can connect to Redis without Upstash.
- CI: Add Redis as a service (or dependency) in CI so that future E2E tests that exercise SSE or worker→Redis publishing can run. For now this is documented only; no CI change is required in M07.
Acceptance criteria
| Criterion | Status |
|---|---|
| List and thread refresh when relevant SSE events are received; new messages appear in near real-time. | Pending |
| SSE is the primary real-time mechanism; Redis Pub/Sub is used from day one for event distribution (horizontal scaling). | Pending |
7.6 Inbox disconnected banner
Tasks
- Banner in ticket view
- When ticket’s inbox has connected_at null or last_error set, show banner above composer: "Replies are paused — inbox disconnected. [Reconnect ▸]" with link to inbox settings. Section 6.8.
- Optionally disable send button when inbox disconnected.
Acceptance criteria
| Criterion | Status |
|---|---|
| Banner shown when inbox is disconnected; Reconnect links to settings; send disabled or warned. | Pending |
7.7 Ticket delete and move to inbox
Tasks
-
Ticket delete (Admin only)
- tRPC: delete ticket (Admin role check). Hard delete: ticket + messages + ticket_tags; enqueue Inngest job or trigger async cleanup for S3 attachments (message attachments). Section 10.2. UI: "Delete ticket" in ticket header or dropdown; confirmation dialog ("This will permanently delete the ticket and all N messages. This cannot be undone."); on confirm call tRPC. Audit: ticket.deleted (M09).
-
Move ticket to another inbox
- tRPC: update ticket inbox_id (tenant-scoped; target inbox must belong to tenant). Section 10.2. UI: inbox dropdown or "Move to inbox" in header/command palette (M16); select target inbox; save. Thread (messages) stays with ticket; no message row changes. Audit: ticket.moved with metadata (from_inbox_id, to_inbox_id). M16 command palette "Move to inbox" invokes this.
Acceptance criteria
| Criterion | Status |
|---|---|
| Admin can delete a ticket with confirmation; ticket and messages removed; S3 cleanup enqueued; audit ticket.deleted. | Pending |
| Agent/Admin can move ticket to another tenant inbox; audit ticket.moved; command palette uses same tRPC. | Pending |
7.8 Bulk actions on ticket list
Tasks
- Multi-select and bulk actions
- List UI: checkboxes or row selection; "Select all" (current page). Toolbar when selection non-empty: bulk change status, bulk assign (assignee), bulk add/remove tags, bulk move to inbox, bulk delete (Admin only). Section 10.2. Each action: confirmation where appropriate (e.g. bulk delete: "Permanently delete N tickets?"). tRPC: bulk update procedures (e.g. tickets.bulkUpdateStatus, tickets.bulkDelete) with tenant scope and role checks (bulk delete = Admin only).
Acceptance criteria
| Criterion | Status |
|---|---|
| User can select multiple tickets; bulk change status, assignee, tags, move to inbox; bulk delete (Admin only) with confirmation. | Pending |
7.9 Message delete (Admin / GDPR)
Tasks
- Admin delete single message
- tRPC: delete message (Admin only); hard delete message row and attachment refs; enqueue S3 cleanup for attachments. Section 10.3. UI: on message bubble (e.g. overflow menu) "Delete message" for Admins; confirmation ("This message will be permanently deleted."). Audit: message.deleted (M09). Ticket remains; if first message deleted, ticket subject preserved.
Acceptance criteria
| Criterion | Status |
|---|---|
| Admin can delete a single message with confirmation; message and attachment refs removed; S3 cleanup enqueued; audit message.deleted. | Pending |
Milestone 7 sign-off
| Criterion | Status |
|---|---|
| All tasks in 7.1–7.9 complete. | Pending |
| All acceptance criteria met. | Pending |
| Agents can list, filter, open tickets; reply and add internal notes; manage tags and assignee; ticket delete/move and bulk actions (Admin where required); see updates via SSE. | Pending |
| E2E: Tickets critical path (list, open ticket, reply) — see INDEX — Testing strategy. | Pending |
| Ready for M08 (Notifications and snippets). | Pending |