multica/_features/inbox-notifications.json
Naiyuan Qing 9127e543d5 feat: add event bus, WS workspace isolation, and global store migration
- Add internal event bus (server/internal/events/) with synchronous
  pub/sub and panic isolation per listener
- Upgrade WebSocket Hub to workspace-scoped rooms with JWT auth
  and membership verification on connect
- Add 10 new WS event types (comment CRUD, inbox read/archive,
  agent create/delete, workspace/member events)
- Refactor all handlers and TaskService to publish events via Bus
  instead of direct Hub.Broadcast calls
- Add WS broadcast listener that routes events to correct workspace
- Frontend: WSClient sends token + workspace_id on connect with
  auto-reconnect refetch
- Frontend: centralized useRealtimeSync hook dispatches all WS
  events to global Zustand stores
- Migrate issues and inbox pages from local useState to global
  useIssueStore/useInboxStore
- Make store addIssue/addItem idempotent to prevent duplicates
- Remove dead packages/hooks/src/use-realtime.ts
- Add feature tracking files for 4 planned features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:08:27 +08:00

80 lines
4.5 KiB
JSON

{
"id": "inbox-notifications",
"name": "Inbox & Notifications",
"status": "designing",
"createdAt": "2026-03-25",
"completedAt": null,
"description": "Complete inbox notification system: sidebar unread badge, notification triggers for all key actions, archive UI, issue navigation from notifications, and real-time sync across tabs.",
"currentState": "Inbox page exists with two-column layout. 5 notification triggers implemented (issue assign, reassign, status change, task complete, task fail). No sidebar badge. No archive button. No link to issue. Mark read/archive don't broadcast WS events. SDK return types wrong for mark read/archive.",
"decisions": [
"Inbox uses useInboxStore from global store (not page-local useState)",
"Sidebar badge reads unread count from store, updated by WS events",
"Backend adds GET /api/inbox/unread-count endpoint using existing CountUnreadInbox SQL query",
"Mark read and archive broadcast WS events (inbox:read, inbox:archived) for cross-tab sync",
"New notification triggers: comment on assigned issue (notify assignee), status change notifies creator too, unassign notifies old assignee",
"SDK markInboxRead and archiveInbox return Promise<InboxItem> not Promise<void>",
"Inbox detail shows 'View Issue' link when issue_id is present"
],
"tasks": [
{
"task": "Backend: Add GET /api/inbox/unread-count endpoint",
"done": false,
"scope": "New handler using existing CountUnreadInbox query. Returns { count: number }. Requires auth + workspace membership. Route added to router.go."
},
{
"task": "Backend: Broadcast WS events for mark read and archive",
"done": false,
"scope": "MarkInboxRead broadcasts inbox:read event with { item_id, recipient_id }. ArchiveInboxItem broadcasts inbox:archived with { item_id, recipient_id }. Events added to protocol/events.go."
},
{
"task": "Backend: Add notification trigger for comment on assigned issue",
"done": false,
"scope": "When comment created, if issue has assignee and commenter is not the assignee, create inbox item type 'mentioned' severity 'info' for assignee. Implemented via event bus listener."
},
{
"task": "Backend: Status change notifies creator in addition to assignee",
"done": false,
"scope": "When issue status changes, create inbox item for creator (if creator != the person making the change). Existing assignee notification stays."
},
{
"task": "Backend: Unassign notifies old assignee",
"done": false,
"scope": "When issue assignee changes from A to B (or to null), create inbox item for old assignee A with type 'status_change' and title 'Unassigned from: {issue title}'."
},
{
"task": "SDK: Fix markInboxRead and archiveInbox return types",
"done": false,
"scope": "Change markInboxRead from Promise<void> to Promise<InboxItem>. Change archiveInbox from Promise<void> to Promise<InboxItem>. Update type imports."
},
{
"task": "Frontend: Add WS event types for inbox:read and inbox:archived",
"done": false,
"scope": "Add inbox:read and inbox:archived to WSEventType in packages/types/src/events.ts. Add payload types."
},
{
"task": "Frontend: Sidebar unread badge",
"done": false,
"scope": "Sidebar Inbox nav item shows unread count badge (shadcn Badge variant). Initial count from /api/inbox/unread-count on mount. Incremented on inbox:new, decremented on inbox:read/inbox:archived WS events."
},
{
"task": "Frontend: Inbox detail 'View Issue' navigation",
"done": false,
"scope": "When selected item has issue_id, show 'View Issue' button/link in InboxDetail that navigates to /issues/{issue_id}. Use shadcn Button variant='outline'."
},
{
"task": "Frontend: Archive button in inbox detail",
"done": false,
"scope": "Archive button next to Mark Read in InboxDetail. Calls api.archiveInbox(id). On success, removes item from store. Shows toast confirmation."
},
{
"task": "Frontend: Inbox real-time sync for read/archive",
"done": false,
"scope": "useRealtimeSync handles inbox:read (mark item read in store) and inbox:archived (remove item from store). Badge count updates automatically from store."
},
{
"task": "Frontend: Inbox loading/empty states with shadcn",
"done": false,
"scope": "Initial load shows Skeleton. Empty inbox shows centered illustration/text 'All caught up'. Error shows toast. Consistent with other pages."
}
]
}