package notify import ( "encoding/json" "fmt" "log/slog" webpush "github.com/SherClockHolmes/webpush-go" "github.com/thepeterstone/claudomator/internal/storage" ) // PushSubscriptionStore is the minimal storage interface needed by WebPushNotifier. type PushSubscriptionStore interface { ListPushSubscriptions() ([]storage.PushSubscription, error) } // WebPushNotifier sends web push notifications to all registered subscribers. type WebPushNotifier struct { Store PushSubscriptionStore VAPIDPublicKey string VAPIDPrivateKey string VAPIDEmail string Logger *slog.Logger } // notificationContent derives urgency, title, body, and tag from a notify Event. // Exported only for tests; use lowercase in production code via this same file. func notificationContent(ev Event) (urgency, title, body, tag string) { tag = "task-" + ev.TaskID switch ev.Status { case "BLOCKED": urgency = "urgent" title = "Needs input" body = fmt.Sprintf("%s is waiting for your response", ev.TaskName) case "FAILED", "BUDGET_EXCEEDED", "TIMED_OUT": urgency = "high" title = "Task failed" if ev.Error != "" { body = fmt.Sprintf("%s failed: %s", ev.TaskName, ev.Error) } else { body = fmt.Sprintf("%s failed", ev.TaskName) } case "COMPLETED": urgency = "low" title = "Task done" body = fmt.Sprintf("%s completed ($%.2f)", ev.TaskName, ev.CostUSD) default: urgency = "normal" title = "Task update" body = fmt.Sprintf("%s: %s", ev.TaskName, ev.Status) } return } // Notify sends a web push notification to all registered subscribers. func (n *WebPushNotifier) Notify(ev Event) error { subs, err := n.Store.ListPushSubscriptions() if err != nil { return fmt.Errorf("listing push subscriptions: %w", err) } if len(subs) == 0 { return nil } urgency, title, body, tag := notificationContent(ev) payload := map[string]string{ "title": title, "body": body, "tag": tag, } data, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshaling push payload: %w", err) } opts := &webpush.Options{ Subscriber: n.VAPIDEmail, VAPIDPublicKey: n.VAPIDPublicKey, VAPIDPrivateKey: n.VAPIDPrivateKey, Urgency: webpush.Urgency(urgency), TTL: 86400, } var lastErr error for _, sub := range subs { wSub := &webpush.Subscription{ Endpoint: sub.Endpoint, Keys: webpush.Keys{ P256dh: sub.P256DHKey, Auth: sub.AuthKey, }, } resp, sendErr := webpush.SendNotification(data, wSub, opts) if sendErr != nil { n.Logger.Error("webpush send failed", "endpoint", sub.Endpoint, "error", sendErr) lastErr = sendErr continue } resp.Body.Close() if resp.StatusCode >= 400 { n.Logger.Warn("webpush returned error status", "endpoint", sub.Endpoint, "status", resp.StatusCode) } } return lastErr }