package notify import ( "bytes" "encoding/json" "fmt" "log/slog" "net/http" "time" ) // Notifier sends notifications when tasks complete. type Notifier interface { Notify(event Event) error } // Event represents a task completion event. type Event struct { TaskID string `json:"task_id"` TaskName string `json:"task_name"` Status string `json:"status"` CostUSD float64 `json:"cost_usd"` Duration string `json:"duration"` Error string `json:"error,omitempty"` } // WebhookNotifier sends POST requests to a configured URL. type WebhookNotifier struct { URL string client *http.Client logger *slog.Logger } func NewWebhookNotifier(url string, logger *slog.Logger) *WebhookNotifier { return &WebhookNotifier{ URL: url, client: &http.Client{Timeout: 10 * time.Second}, logger: logger, } } func (w *WebhookNotifier) Notify(event Event) error { body, err := json.Marshal(event) if err != nil { return fmt.Errorf("marshaling event: %w", err) } resp, err := w.client.Post(w.URL, "application/json", bytes.NewReader(body)) if err != nil { return fmt.Errorf("sending webhook: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("webhook returned %d", resp.StatusCode) } return nil } // MultiNotifier fans out to multiple notifiers. type MultiNotifier struct { notifiers []Notifier logger *slog.Logger } func NewMultiNotifier(logger *slog.Logger, notifiers ...Notifier) *MultiNotifier { return &MultiNotifier{notifiers: notifiers, logger: logger} } func (m *MultiNotifier) Notify(event Event) error { var lastErr error for _, n := range m.notifiers { if err := n.Notify(event); err != nil { m.logger.Error("notification failed", "error", err) lastErr = err } } return lastErr } // LogNotifier logs events (useful as a default/fallback). type LogNotifier struct { Logger *slog.Logger } func (l *LogNotifier) Notify(event Event) error { l.Logger.Info("task completed", "task_id", event.TaskID, "task_name", event.TaskName, "status", event.Status, "cost_usd", event.CostUSD, "duration", event.Duration, ) return nil }