refactor(server): extract inbox creation to bus listeners, add agent visibility filtering
- Move all CreateInboxItem calls from handlers to centralized inbox_listeners.go - Enrich issue:updated payload with change context (assignee_changed, status_changed, prev values) - Enrich comment:created payload with issue context (assignee info) - Bus listeners handle: issue assign, unassign, reassign, status change, comment notification - ListAgents filters private agents: only visible to owner_id or workspace admin - Zero CreateInboxItem calls remain in handler package Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19504a217c
commit
759dd741bd
7 changed files with 279 additions and 125 deletions
|
|
@ -123,7 +123,8 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
|
|||
|
||||
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := resolveWorkspaceID(r)
|
||||
if _, ok := h.requireWorkspaceMember(w, r, workspaceID, "workspace not found"); !ok {
|
||||
member, ok := h.requireWorkspaceMember(w, r, workspaceID, "workspace not found")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -133,12 +134,22 @@ func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
resp := make([]AgentResponse, len(agents))
|
||||
for i, a := range agents {
|
||||
resp[i] = agentToResponse(a)
|
||||
userID := requestUserID(r)
|
||||
isAdmin := roleAllowed(member.Role, "owner", "admin")
|
||||
|
||||
// Filter private agents: only visible to owner_id or workspace admin
|
||||
var visible []AgentResponse
|
||||
for _, a := range agents {
|
||||
if a.Visibility == "private" && !isAdmin && uuidToString(a.OwnerID) != userID {
|
||||
continue
|
||||
}
|
||||
visible = append(visible, agentToResponse(a))
|
||||
}
|
||||
if visible == nil {
|
||||
visible = []AgentResponse{}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
writeJSON(w, http.StatusOK, visible)
|
||||
}
|
||||
|
||||
func (h *Handler) GetAgent(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -98,27 +98,12 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
resp := commentToResponse(comment)
|
||||
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{"comment": resp})
|
||||
|
||||
// Notify issue assignee about new comment (if assignee is a member and != commenter)
|
||||
if issue.AssigneeType.Valid && issue.AssigneeID.Valid &&
|
||||
issue.AssigneeType.String == "member" && uuidToString(issue.AssigneeID) != userID {
|
||||
body := req.Content
|
||||
inboxItem, inboxErr := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: issue.WorkspaceID,
|
||||
RecipientType: "member",
|
||||
RecipientID: issue.AssigneeID,
|
||||
Type: "mentioned",
|
||||
Severity: "info",
|
||||
IssueID: issue.ID,
|
||||
Title: "New comment on: " + issue.Title,
|
||||
Body: ptrToText(&body),
|
||||
})
|
||||
if inboxErr == nil {
|
||||
h.publish(protocol.EventInboxNew, uuidToString(issue.WorkspaceID), "member", userID,
|
||||
map[string]any{"item": inboxToResponse(inboxItem)})
|
||||
}
|
||||
}
|
||||
h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{
|
||||
"comment": resp,
|
||||
"issue_title": issue.Title,
|
||||
"issue_assignee_type": textToPtr(issue.AssigneeType),
|
||||
"issue_assignee_id": uuidToPtr(issue.AssigneeID),
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,23 +236,8 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
resp := issueToResponse(issue)
|
||||
h.publish(protocol.EventIssueCreated, workspaceID, "member", creatorID, map[string]any{"issue": resp})
|
||||
|
||||
// Create inbox notification for assignee
|
||||
// Only ready issues in todo are enqueued for agents.
|
||||
if issue.AssigneeType.Valid && issue.AssigneeID.Valid {
|
||||
inboxItem, err := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: issue.WorkspaceID,
|
||||
RecipientType: issue.AssigneeType.String,
|
||||
RecipientID: issue.AssigneeID,
|
||||
Type: "issue_assigned",
|
||||
Severity: "action_required",
|
||||
IssueID: issue.ID,
|
||||
Title: "New issue assigned: " + issue.Title,
|
||||
Body: ptrToText(req.Description),
|
||||
})
|
||||
if err == nil {
|
||||
h.publish(protocol.EventInboxNew, workspaceID, "member", creatorID, map[string]any{"item": inboxToResponse(inboxItem)})
|
||||
}
|
||||
|
||||
// Only ready issues in todo are enqueued for agents.
|
||||
if h.shouldEnqueueAgentTask(r.Context(), issue) {
|
||||
h.TaskService.EnqueueTaskForIssue(r.Context(), issue)
|
||||
}
|
||||
|
|
@ -368,12 +353,22 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
resp := issueToResponse(issue)
|
||||
h.publish(protocol.EventIssueUpdated, workspaceID, "member", userID, map[string]any{"issue": resp})
|
||||
|
||||
assigneeChanged := (req.AssigneeType != nil || req.AssigneeID != nil) &&
|
||||
(prevIssue.AssigneeType.String != issue.AssigneeType.String || uuidToString(prevIssue.AssigneeID) != uuidToString(issue.AssigneeID))
|
||||
statusChanged := req.Status != nil && prevIssue.Status != issue.Status
|
||||
|
||||
h.publish(protocol.EventIssueUpdated, workspaceID, "member", userID, map[string]any{
|
||||
"issue": resp,
|
||||
"assignee_changed": assigneeChanged,
|
||||
"status_changed": statusChanged,
|
||||
"prev_assignee_type": textToPtr(prevIssue.AssigneeType),
|
||||
"prev_assignee_id": uuidToPtr(prevIssue.AssigneeID),
|
||||
"prev_status": prevIssue.Status,
|
||||
"creator_type": prevIssue.CreatorType,
|
||||
"creator_id": uuidToString(prevIssue.CreatorID),
|
||||
})
|
||||
|
||||
// If assignee or readiness status changed, reconcile the task queue.
|
||||
if assigneeChanged || statusChanged {
|
||||
h.TaskService.CancelTasksForIssue(r.Context(), issue.ID)
|
||||
|
|
@ -383,84 +378,6 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// If assignee changed, create a notification for the new assignee.
|
||||
if assigneeChanged {
|
||||
// Notify old assignee about unassignment
|
||||
if prevIssue.AssigneeID.Valid && prevIssue.AssigneeType.Valid &&
|
||||
prevIssue.AssigneeType.String == "member" && uuidToString(prevIssue.AssigneeID) != userID {
|
||||
oldInbox, oErr := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: prevIssue.WorkspaceID,
|
||||
RecipientType: "member",
|
||||
RecipientID: prevIssue.AssigneeID,
|
||||
Type: "status_change",
|
||||
Severity: "info",
|
||||
IssueID: prevIssue.ID,
|
||||
Title: "Unassigned from: " + issue.Title,
|
||||
Body: ptrToText(nil),
|
||||
})
|
||||
if oErr == nil {
|
||||
h.publish(protocol.EventInboxNew, workspaceID, "member", userID,
|
||||
map[string]any{"item": inboxToResponse(oldInbox)})
|
||||
}
|
||||
}
|
||||
|
||||
// Create inbox notification for new assignee
|
||||
if issue.AssigneeType.Valid && issue.AssigneeID.Valid {
|
||||
inboxItem, err := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: issue.WorkspaceID,
|
||||
RecipientType: issue.AssigneeType.String,
|
||||
RecipientID: issue.AssigneeID,
|
||||
Type: "issue_assigned",
|
||||
Severity: "action_required",
|
||||
IssueID: issue.ID,
|
||||
Title: "Assigned to you: " + issue.Title,
|
||||
})
|
||||
if err == nil {
|
||||
h.publish(protocol.EventInboxNew, workspaceID, "member", userID, map[string]any{"item": inboxToResponse(inboxItem)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If status changed, create a notification
|
||||
if req.Status != nil {
|
||||
if issue.AssigneeType.Valid && issue.AssigneeID.Valid {
|
||||
inboxItem, err := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: issue.WorkspaceID,
|
||||
RecipientType: issue.AssigneeType.String,
|
||||
RecipientID: issue.AssigneeID,
|
||||
Type: "status_change",
|
||||
Severity: "info",
|
||||
IssueID: issue.ID,
|
||||
Title: issue.Title + " moved to " + *req.Status,
|
||||
})
|
||||
if err == nil {
|
||||
h.publish(protocol.EventInboxNew, workspaceID, "member", userID, map[string]any{"item": inboxToResponse(inboxItem)})
|
||||
}
|
||||
}
|
||||
|
||||
// Notify creator about status change (if creator is member and != the person making change)
|
||||
if prevIssue.CreatorType == "member" && uuidToString(prevIssue.CreatorID) != userID {
|
||||
// Don't double-notify if creator is also the assignee
|
||||
isAlsoAssignee := prevIssue.AssigneeID.Valid && uuidToString(prevIssue.AssigneeID) == uuidToString(prevIssue.CreatorID)
|
||||
if !isAlsoAssignee {
|
||||
creatorInbox, cErr := h.Queries.CreateInboxItem(r.Context(), db.CreateInboxItemParams{
|
||||
WorkspaceID: prevIssue.WorkspaceID,
|
||||
RecipientType: "member",
|
||||
RecipientID: prevIssue.CreatorID,
|
||||
Type: "status_change",
|
||||
Severity: "info",
|
||||
IssueID: prevIssue.ID,
|
||||
Title: "Status changed: " + issue.Title,
|
||||
Body: ptrToText(nil),
|
||||
})
|
||||
if cErr == nil {
|
||||
h.publish(protocol.EventInboxNew, workspaceID, "member", userID,
|
||||
map[string]any{"item": inboxToResponse(creatorInbox)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue