api/notifications: register preview endpoint in main.go
The GET /api/notifications/preview endpoint was already implemented in internal/api/notifications.go but was never registered in main.go. This commit wires up the NotificationsHandler to enable the test thumbnail endpoint for UI development and QA. The endpoint accepts query parameters: - type: notification type (fall, anomaly, zone_enter, sleep) - person: person name (optional, defaults to "Alice") It calls the appropriate Generate*Thumbnail function from the render package and returns PNG bytes with Content-Type: image/png. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
45aa553a1f
commit
37571ece97
3 changed files with 181 additions and 0 deletions
|
|
@ -527,6 +527,16 @@ func main() {
|
|||
notificationSettingsHandler.RegisterRoutes(r)
|
||||
log.Printf("[INFO] Notification settings API registered at /api/settings/notifications")
|
||||
|
||||
// Phase 6: Notifications REST API (channels, preview, test)
|
||||
notificationsHandler, err := api.NewNotificationsHandler(filepath.Join(cfg.DataDir, "notifications.db"))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to create notifications handler: %v", err)
|
||||
} else {
|
||||
defer notificationsHandler.Close()
|
||||
notificationsHandler.RegisterRoutes(r)
|
||||
log.Printf("[INFO] Notifications API registered at /api/notifications/*")
|
||||
}
|
||||
|
||||
// Phase 6: Feature discovery notifications
|
||||
// Notifier manages one-time feature discovery notifications with quiet hours support
|
||||
featureNotifier, err := featurehelp.NewNotifier(mainDB)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import (
|
|||
|
||||
"github.com/go-chi/chi/v5"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/spaxel/mothership/internal/render"
|
||||
)
|
||||
|
||||
// NotificationsHandler manages notification delivery channels.
|
||||
|
|
@ -312,6 +314,7 @@ func (n *NotificationsHandler) RegisterRoutes(r chi.Router) {
|
|||
r.Get("/api/notifications/config", n.handleGetConfig)
|
||||
r.Post("/api/notifications/config", n.handleSetConfig)
|
||||
r.Post("/api/notifications/test", n.handleSendTest)
|
||||
r.Get("/api/notifications/preview", n.handlePreview)
|
||||
}
|
||||
|
||||
// notificationConfigResponse is the response for channel configuration requests.
|
||||
|
|
@ -435,6 +438,73 @@ func (n *NotificationsHandler) handleSendTest(w http.ResponseWriter, r *http.Req
|
|||
})
|
||||
}
|
||||
|
||||
// handlePreview handles GET /api/notifications/preview
|
||||
// Returns a rendered test image for UI development and QA.
|
||||
// Query params:
|
||||
// - type: notification type (fall, anomaly, zone_enter, sleep)
|
||||
// - person: person name (optional, defaults to "Alice")
|
||||
func (n *NotificationsHandler) handlePreview(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse query parameters
|
||||
notifType := r.URL.Query().Get("type")
|
||||
personName := r.URL.Query().Get("person")
|
||||
|
||||
// Set defaults
|
||||
if personName == "" {
|
||||
personName = "Alice"
|
||||
}
|
||||
|
||||
// Define test zones
|
||||
zones := []render.Zone{
|
||||
{ID: "kitchen", Name: "Kitchen", X: 0, Y: 0, W: 4, D: 3, Color: "#4fc3f7"},
|
||||
{ID: "living", Name: "Living", X: 4, Y: 0, W: 5, D: 4, Color: "#81c784"},
|
||||
{ID: "hallway", Name: "Hallway", X: 4, Y: 4, W: 2, D: 2, Color: "#ffb74d"},
|
||||
{ID: "bedroom", Name: "Bedroom", X: 6, Y: 4, W: 3, D: 3, Color: "#ba68c8"},
|
||||
}
|
||||
|
||||
// Define test person
|
||||
person := render.Person{
|
||||
Name: personName,
|
||||
X: 2.0,
|
||||
Y: 1.5,
|
||||
Z: 1.0,
|
||||
Color: "#4488ff",
|
||||
Confidence: 0.85,
|
||||
IsFall: false,
|
||||
}
|
||||
|
||||
var pngData []byte
|
||||
var err error
|
||||
|
||||
// Generate thumbnail based on notification type
|
||||
switch notifType {
|
||||
case "fall":
|
||||
pngData, err = render.GenerateFallDetectedThumbnail(10.0, 8.0, zones, person, "Kitchen")
|
||||
case "anomaly":
|
||||
pngData, err = render.GenerateAnomalyAlertThumbnail(10.0, 8.0, zones, "Living")
|
||||
case "zone_enter":
|
||||
pngData, err = render.GenerateZoneEnterThumbnail(10.0, 8.0, zones, person, "Kitchen")
|
||||
case "sleep":
|
||||
person.Z = 0.5 // Sleeping position
|
||||
pngData, err = render.GenerateSleepSummaryThumbnail(10.0, 8.0, zones, person, "7h 23m")
|
||||
default:
|
||||
// Default to fall detection preview
|
||||
pngData, err = render.GenerateFallDetectedThumbnail(10.0, 8.0, zones, person, "Kitchen")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to generate preview thumbnail: %v", err)
|
||||
http.Error(w, "failed to generate preview", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers and write PNG data
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if _, err := w.Write(pngData); err != nil {
|
||||
log.Printf("[ERROR] Failed to write preview response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notification sending (called by automation engine) ────────────────────────────
|
||||
|
||||
// SendNotification sends a notification via all enabled channels.
|
||||
|
|
|
|||
|
|
@ -304,6 +304,107 @@ func TestNotificationsHandler(t *testing.T) {
|
|||
t.Errorf("Expected body 'Custom Body', got '%s'", mockSender.body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - fall detection", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview?type=fall&person=Bob", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "image/png" {
|
||||
t.Errorf("Expected Content-Type 'image/png', got '%s'", ct)
|
||||
}
|
||||
|
||||
// Verify PNG data was returned (PNG magic bytes)
|
||||
body := w.Body.Bytes()
|
||||
if len(body) < 8 {
|
||||
t.Fatalf("Expected PNG data, got %d bytes", len(body))
|
||||
}
|
||||
// PNG magic bytes: 137 80 78 71 13 10 26 10
|
||||
if body[0] != 0x89 || body[1] != 0x50 || body[2] != 0x4e || body[3] != 0x47 {
|
||||
t.Errorf("Expected PNG magic bytes, got %x %x %x %x", body[0], body[1], body[2], body[3])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - zone_enter", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview?type=zone_enter&person=Alice", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "image/png" {
|
||||
t.Errorf("Expected Content-Type 'image/png', got '%s'", ct)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - anomaly", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview?type=anomaly", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "image/png" {
|
||||
t.Errorf("Expected Content-Type 'image/png', got '%s'", ct)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - sleep", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview?type=sleep", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "image/png" {
|
||||
t.Errorf("Expected Content-Type 'image/png', got '%s'", ct)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - default (no type)", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Default should return fall detection preview
|
||||
body := w.Body.Bytes()
|
||||
if len(body) < 8 {
|
||||
t.Fatalf("Expected PNG data, got %d bytes", len(body))
|
||||
}
|
||||
// Verify PNG magic bytes
|
||||
if body[0] != 0x89 || body[1] != 0x50 || body[2] != 0x4e || body[3] != 0x47 {
|
||||
t.Errorf("Expected PNG magic bytes, got %x %x %x %x", body[0], body[1], body[2], body[3])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/notifications/preview - cache control", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/notifications/preview?type=fall", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
cc := w.Header().Get("Cache-Control")
|
||||
if cc != "no-cache, no-store, must-revalidate" {
|
||||
t.Errorf("Expected Cache-Control 'no-cache, no-store, must-revalidate', got '%s'", cc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mockNotifySender is a test implementation of NotifySender.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue