Analytics: time-series message graph with 5 time spans (1h/4h/24h/7d/30d), top-5 agents and channels leaderboards, summary cards. 4 new REST endpoints. PWA: web app manifest, service worker with cache-first static/network-only API strategy, push notifications via Web Push API with VAPID keys, push subscription management endpoints, SQLite migration for subscriptions. UX fixes: auto-resize compose textarea (3-12 lines), inline editable agent display name, editable human display name in settings, smart mention/channel highlighting (existing→link, deleted→inactive badge, unknown→plain text), font size -/+ preference (12-24px persisted in localStorage), version footer with GitHub link. MCP: 4 prompts — daily-digest, agent-health-check, channel-overview, debug-agent. Registered with prompt capabilities enabled. Code review fixes: scoped push unsubscribe to user, capped analytics limit at 100, hardened HTML strip regex, bounded SW cache, backend push unsub on disable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/synapbus/synapbus/internal/push"
|
|
)
|
|
|
|
// PushHandler manages Web Push notification subscription endpoints.
|
|
type PushHandler struct {
|
|
pushService *push.Service
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewPushHandler creates a new push notification handler.
|
|
func NewPushHandler(pushService *push.Service) *PushHandler {
|
|
return &PushHandler{
|
|
pushService: pushService,
|
|
logger: slog.Default().With("component", "api.push"),
|
|
}
|
|
}
|
|
|
|
// subscribeRequest is the JSON body for POST /api/push/subscribe.
|
|
type subscribeRequest struct {
|
|
Endpoint string `json:"endpoint"`
|
|
KeyP256dh string `json:"key_p256dh"`
|
|
KeyAuth string `json:"key_auth"`
|
|
}
|
|
|
|
// unsubscribeRequest is the JSON body for DELETE /api/push/subscribe.
|
|
type unsubscribeRequest struct {
|
|
Endpoint string `json:"endpoint"`
|
|
}
|
|
|
|
// Subscribe handles POST /api/push/subscribe.
|
|
// Registers a Web Push subscription for the authenticated user.
|
|
func (h *PushHandler) Subscribe(w http.ResponseWriter, r *http.Request) {
|
|
ownerID, ok := OwnerIDFromContext(r.Context())
|
|
if !ok {
|
|
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
|
|
return
|
|
}
|
|
|
|
var req subscribeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body"))
|
|
return
|
|
}
|
|
|
|
if req.Endpoint == "" || req.KeyP256dh == "" || req.KeyAuth == "" {
|
|
writeJSON(w, http.StatusBadRequest, errorBody("validation_error", "endpoint, key_p256dh, and key_auth are required"))
|
|
return
|
|
}
|
|
|
|
userAgent := r.Header.Get("User-Agent")
|
|
|
|
if err := h.pushService.Subscribe(r.Context(), ownerID, req.Endpoint, req.KeyP256dh, req.KeyAuth, userAgent); err != nil {
|
|
h.logger.Error("push subscribe failed",
|
|
"user_id", ownerID,
|
|
"error", err,
|
|
)
|
|
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to register subscription"))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("push subscription registered",
|
|
"user_id", ownerID,
|
|
)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "subscribed"})
|
|
}
|
|
|
|
// Unsubscribe handles DELETE /api/push/subscribe.
|
|
// Removes a Web Push subscription for the authenticated user.
|
|
func (h *PushHandler) Unsubscribe(w http.ResponseWriter, r *http.Request) {
|
|
ownerID, ok := OwnerIDFromContext(r.Context())
|
|
if !ok {
|
|
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
|
|
return
|
|
}
|
|
|
|
var req unsubscribeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body"))
|
|
return
|
|
}
|
|
|
|
if req.Endpoint == "" {
|
|
writeJSON(w, http.StatusBadRequest, errorBody("validation_error", "endpoint is required"))
|
|
return
|
|
}
|
|
|
|
if err := h.pushService.Unsubscribe(r.Context(), ownerID, req.Endpoint); err != nil {
|
|
h.logger.Error("push unsubscribe failed",
|
|
"error", err,
|
|
)
|
|
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to remove subscription"))
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "unsubscribed"})
|
|
}
|
|
|
|
// VAPIDKey handles GET /api/push/vapid-key.
|
|
// Returns the VAPID public key needed by clients to subscribe to push notifications.
|
|
func (h *PushHandler) VAPIDKey(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"vapid_public_key": h.pushService.GetVAPIDPublicKey(),
|
|
})
|
|
}
|