spaxel/mothership/internal/analytics/alert_handler.go
jedarden f99dc15a2d feat: complete crowd flow visualization implementation
- Fix Viz3D exports to include flow visualization functions
- Export setFlowLayerVisible, setDwellLayerVisible, setCorridorLayerVisible
- Export setFlowTimeFilter, setFlowData, setDwellData, setCorridorData
- Remove duplicate setDwellLayerVisible function definition

This completes the crowd flow visualization feature that was
already implemented in the backend (flow.go) and frontend
(crowdflow.js, viz3d.js) but had missing exports in the Viz3D module.
2026-04-11 07:27:21 -04:00

204 lines
5.7 KiB
Go

// Package analytics provides alert handling for anomaly detection using the notification service.
package analytics
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/spaxel/mothership/internal/events"
)
// NotificationAlertHandler implements AlertHandler using a notification service.
type NotificationAlertHandler struct {
notifyService NotificationService
httpClient *http.Client
webhookURL string
escalationURL string
}
// NotificationService is the interface needed from the notify package.
type NotificationService interface {
Send(notif Notification) error
GenerateFloorPlanThumbnail(width, height int, blobs []struct {
X, Y, Z float64
Identity string
IsFall bool
}) ([]byte, error)
}
// Notification represents a notification to send.
type Notification struct {
Title string
Body string
Priority int
Tags []string
Image []byte
ImageType string
Data map[string]interface{}
Timestamp time.Time
}
// NewNotificationAlertHandler creates a new alert handler backed by the notification service.
func NewNotificationAlertHandler(notifyService NotificationService) *NotificationAlertHandler {
return &NotificationAlertHandler{
notifyService: notifyService,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
// SetWebhookURL sets the webhook URL for anomaly alerts.
func (h *NotificationAlertHandler) SetWebhookURL(url string) {
h.webhookURL = url
}
// SetEscalationURL sets the escalation webhook URL.
func (h *NotificationAlertHandler) SetEscalationURL(url string) {
h.escalationURL = url
}
// SendAlert sends an alert notification.
func (h *NotificationAlertHandler) SendAlert(event events.AnomalyEvent, immediate bool) error {
// Generate floor plan thumbnail
thumbnail, err := h.notifyService.GenerateFloorPlanThumbnail(400, 300, []struct {
X, Y, Z float64
Identity string
IsFall bool
}{
{
X: event.Position.X,
Y: event.Position.Y,
Z: event.Position.Z,
Identity: event.PersonName,
IsFall: false,
},
})
if err != nil {
log.Printf("[WARN] Failed to generate thumbnail for alert: %v", err)
}
notif := Notification{
Title: "Anomaly Detected",
Body: event.Description,
Priority: 2, // High priority for anomalies
Tags: []string{"spaxel", "anomaly"},
Image: thumbnail,
ImageType: "image/png",
Data: map[string]interface{}{
"anomaly_id": event.ID,
"anomaly_type": string(event.Type),
"zone_id": event.ZoneID,
"zone_name": event.ZoneName,
"person_id": event.PersonID,
"person_name": event.PersonName,
"timestamp": event.Timestamp.Format(time.RFC3339),
"immediate": immediate,
},
Timestamp: time.Now(),
}
// Set higher priority for security mode
if immediate {
notif.Priority = 4 // Emergency priority
notif.Tags = append(notif.Tags, "security")
}
return h.notifyService.Send(notif)
}
// SendWebhook sends a webhook notification.
func (h *NotificationAlertHandler) SendWebhook(event events.AnomalyEvent, immediate bool) error {
if h.webhookURL == "" {
return nil // No webhook configured
}
payload := map[string]interface{}{
"anomaly_id": event.ID,
"type": string(event.Type),
"score": event.Score,
"description": event.Description,
"timestamp": event.Timestamp.Format(time.RFC3339),
"zone_id": event.ZoneID,
"zone_name": event.ZoneName,
"person_id": event.PersonID,
"person_name": event.PersonName,
"device_mac": event.DeviceMAC,
"position": event.Position,
"hour_of_week": event.HourOfWeek,
"expected_occupancy": event.ExpectedOccupancy,
"immediate": immediate,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal webhook payload: %w", err)
}
req, err := http.NewRequest("POST", h.webhookURL, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("create webhook request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Spaxel/1.0")
resp, err := h.httpClient.Do(req)
if err != nil {
return fmt.Errorf("send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
log.Printf("[INFO] Anomaly webhook sent: %s (status: %d)", event.ID, resp.StatusCode)
return nil
}
// SendEscalation sends an escalation webhook notification.
func (h *NotificationAlertHandler) SendEscalation(event events.AnomalyEvent) error {
if h.escalationURL == "" {
return nil // No escalation webhook configured
}
payload := map[string]interface{}{
"anomaly_id": event.ID,
"type": string(event.Type),
"score": event.Score,
"description": event.Description,
"timestamp": event.Timestamp.Format(time.RFC3339),
"zone_id": event.ZoneID,
"zone_name": event.ZoneName,
"person_id": event.PersonID,
"person_name": event.PersonName,
"escalation": true,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal escalation payload: %w", err)
}
req, err := http.NewRequest("POST", h.escalationURL, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("create escalation request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Spaxel/1.0")
resp, err := h.httpClient.Do(req)
if err != nil {
return fmt.Errorf("send escalation: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("escalation returned status %d", resp.StatusCode)
}
log.Printf("[INFO] Anomaly escalation sent: %s (status: %d)", event.ID, resp.StatusCode)
return nil
}