package notifications import ( "context" "encoding/json" "fmt" "log" "switchboard-core/events" "switchboard-core/models" "switchboard-core/store" ) // ── Role Fallback ─────────────────────────── // Subscribes to the existing "role.fallback" EventBus event (emitted by // capabilities/resolver.go since v0.17.0) and creates notifications for // all admin users. // roleFallbackPayload matches the payload emitted by the role resolver. type roleFallbackPayload struct { Role string `json:"role"` Primary string `json:"primary_model"` Fallback string `json:"fallback_model"` Error string `json:"error"` } // RoleFallbackHandler returns an events.Handler that creates admin // notifications when a model role falls back. func RoleFallbackHandler(svc *Service, stores store.Stores) events.Handler { return func(e events.Event) { var p roleFallbackPayload if err := json.Unmarshal(e.Payload, &p); err != nil { log.Printf("[notifications] failed to parse role.fallback payload: %v", err) return } title := fmt.Sprintf("Model fallback: %s → %s", p.Primary, p.Fallback) if p.Primary == "" { title = fmt.Sprintf("Role '%s' fell back to %s", p.Role, p.Fallback) } body := "" if p.Error != "" { body = fmt.Sprintf("Primary model error: %s", p.Error) } // Notify all admin users ctx := context.Background() admins, _, err := stores.Users.List(ctx, store.ListOptions{Limit: 500}) if err != nil { log.Printf("[notifications] failed to list users for role.fallback: %v", err) return } for _, u := range admins { if u.Role != "admin" { continue } n := models.Notification{ UserID: u.ID, Type: models.NotifTypeRoleFallback, Title: title, Body: body, } if err := svc.Notify(ctx, &n); err != nil { log.Printf("[notifications] failed to create role.fallback notification for %s: %v", u.ID, err) } } } } // ── Knowledge Base ────────────────────────── // Called directly from the KB handler after indexing completes or fails. // Not a bus subscriber — the KB handler calls these functions. // NotifyKBReady creates a notification when a knowledge base finishes indexing. func NotifyKBReady(svc *Service, userID, kbID, kbName string, chunkCount int) { if svc == nil { return } n := models.Notification{ UserID: userID, Type: models.NotifTypeKBReady, Title: fmt.Sprintf("Knowledge base \"%s\" ready (%d chunks)", kbName, chunkCount), ResourceType: models.ResourceTypeKnowledgeBase, ResourceID: kbID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] kb.ready failed for user %s: %v", userID, err) } } // NotifyKBError creates a notification when KB indexing fails. func NotifyKBError(svc *Service, userID, kbID, kbName, errMsg string) { if svc == nil { return } n := models.Notification{ UserID: userID, Type: models.NotifTypeKBError, Title: fmt.Sprintf("Knowledge base \"%s\" indexing failed", kbName), Body: errMsg, ResourceType: models.ResourceTypeKnowledgeBase, ResourceID: kbID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] kb.error failed for user %s: %v", userID, err) } } // ── Group Membership ──────────────────────── // Called directly from the group handler on add/remove. // NotifyGroupMemberAdded creates a notification when a user is added to a group. func NotifyGroupMemberAdded(svc *Service, userID, groupID, groupName string) { if svc == nil { return } n := models.Notification{ UserID: userID, Type: models.NotifTypeGrantChanged, Title: fmt.Sprintf("You were added to group \"%s\"", groupName), ResourceType: models.ResourceTypeGroup, ResourceID: groupID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] grant.changed (add) failed for user %s: %v", userID, err) } } // NotifyGroupMemberRemoved creates a notification when a user is removed from a group. func NotifyGroupMemberRemoved(svc *Service, userID, groupID, groupName string) { if svc == nil { return } n := models.Notification{ UserID: userID, Type: models.NotifTypeGrantChanged, Title: fmt.Sprintf("You were removed from group \"%s\"", groupName), ResourceType: models.ResourceTypeGroup, ResourceID: groupID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] grant.changed (remove) failed for user %s: %v", userID, err) } } // ── Memory Extraction ────────────────────── // Called from memory/extractor.go after successful extraction. // NotifyMemoryExtracted creates a notification when new memories are extracted. func NotifyMemoryExtracted(svc *Service, userID, channelID string, count int) { if svc == nil { return } title := fmt.Sprintf("%d new memories extracted", count) if count == 1 { title = "1 new memory extracted" } n := models.Notification{ UserID: userID, Type: models.NotifTypeMemoryExtracted, Title: title, ResourceType: models.ResourceTypeChannel, ResourceID: channelID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] memory.extracted failed for user %s: %v", userID, err) } } // ── User Mention ─────────────────────────── // Called from handlers/completion.go when an @mention targets a human user. // NotifyUserMentioned creates a persisted notification for an @mention. func NotifyUserMentioned(svc *Service, mentionedUserID, channelID, fromUserID, preview string) { if svc == nil { return } n := models.Notification{ UserID: mentionedUserID, Type: models.NotifTypeUserMentioned, Title: "You were mentioned in a conversation", Body: preview, ResourceType: models.ResourceTypeChannel, ResourceID: channelID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] user.mentioned failed for user %s: %v", mentionedUserID, err) } } // ── Workflow Claimed ─────────────────────── // Called from handlers/workflow_assignments.go when a user claims an assignment. // NotifyWorkflowClaimed creates a persisted notification when an assignment is claimed. func NotifyWorkflowClaimed(svc *Service, claimerID, assignmentID, channelID string) { if svc == nil { return } n := models.Notification{ UserID: claimerID, Type: models.NotifTypeWorkflowClaimed, Title: "You claimed a workflow assignment", ResourceType: models.ResourceTypeChannel, ResourceID: channelID, } if err := svc.Notify(context.Background(), &n); err != nil { log.Printf("[notifications] workflow.claimed failed for user %s: %v", claimerID, err) } }