summaryrefslogtreecommitdiff
path: root/internal/api/push.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-16 20:43:28 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-16 20:43:28 +0000
commit7f920ca63af5329c19a0e5a879c649c594beea35 (patch)
tree803150e7c895d3232bad35c729aad647aaa54348 /internal/api/push.go
parent072652f617653dce74368cedb42b88189e5014fb (diff)
feat: add web push notifications and file drop
Web Push: - WebPushNotifier with VAPID auth; urgency mapped to event type (BLOCKED=urgent, FAILED=high, COMPLETED=low) - Auto-generates VAPID keys on first serve, persists to config file - push_subscriptions table in SQLite (upsert by endpoint) - GET /api/push/vapid-key, POST/DELETE /api/push/subscribe endpoints - Service worker (sw.js) handles push events and notification clicks - Notification bell button in web UI; subscribes on click File Drop: - GET /api/drops, GET /api/drops/{filename}, POST /api/drops - Persistent ~/.claudomator/drops/ directory - CLAUDOMATOR_DROP_DIR env var passed to agent subprocesses - Drops tab (📁) in web UI with file listing and download links Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/push.go')
-rw-r--r--internal/api/push.go105
1 files changed, 105 insertions, 0 deletions
diff --git a/internal/api/push.go b/internal/api/push.go
new file mode 100644
index 0000000..6fd805a
--- /dev/null
+++ b/internal/api/push.go
@@ -0,0 +1,105 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/google/uuid"
+ "github.com/thepeterstone/claudomator/internal/storage"
+)
+
+// pushSubscriptionStore is the minimal interface needed by push handlers.
+type pushSubscriptionStore interface {
+ SavePushSubscription(sub storage.PushSubscription) error
+ DeletePushSubscription(endpoint string) error
+ ListPushSubscriptions() ([]storage.PushSubscription, error)
+}
+
+// SetVAPIDConfig configures VAPID keys and email for web push notifications.
+func (s *Server) SetVAPIDConfig(pub, priv, email string) {
+ s.vapidPublicKey = pub
+ s.vapidPrivateKey = priv
+ s.vapidEmail = email
+}
+
+// SetPushStore configures the push subscription store.
+func (s *Server) SetPushStore(store pushSubscriptionStore) {
+ s.pushStore = store
+}
+
+// SetDropsDir configures the file drop directory.
+func (s *Server) SetDropsDir(dir string) {
+ s.dropsDir = dir
+}
+
+// handleGetVAPIDKey returns the VAPID public key for client-side push subscription.
+func (s *Server) handleGetVAPIDKey(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, http.StatusOK, map[string]string{"public_key": s.vapidPublicKey})
+}
+
+// handlePushSubscribe saves a new push subscription.
+func (s *Server) handlePushSubscribe(w http.ResponseWriter, r *http.Request) {
+ var input struct {
+ Endpoint string `json:"endpoint"`
+ Keys struct {
+ P256DH string `json:"p256dh"`
+ Auth string `json:"auth"`
+ } `json:"keys"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
+ return
+ }
+ if input.Endpoint == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "endpoint is required"})
+ return
+ }
+ if input.Keys.P256DH == "" || input.Keys.Auth == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "keys.p256dh and keys.auth are required"})
+ return
+ }
+
+ sub := storage.PushSubscription{
+ ID: uuid.New().String(),
+ Endpoint: input.Endpoint,
+ P256DHKey: input.Keys.P256DH,
+ AuthKey: input.Keys.Auth,
+ }
+
+ store := s.pushStore
+ if store == nil {
+ store = s.store
+ }
+
+ if err := store.SavePushSubscription(sub); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusCreated, map[string]string{"id": sub.ID})
+}
+
+// handlePushUnsubscribe deletes a push subscription.
+func (s *Server) handlePushUnsubscribe(w http.ResponseWriter, r *http.Request) {
+ var input struct {
+ Endpoint string `json:"endpoint"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
+ return
+ }
+ if input.Endpoint == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "endpoint is required"})
+ return
+ }
+
+ store := s.pushStore
+ if store == nil {
+ store = s.store
+ }
+
+ if err := store.DeletePushSubscription(input.Endpoint); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+}