- 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>
80 lines
4.5 KiB
JSON
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."
|
|
}
|
|
]
|
|
}
|