// server/backend/internal/httpapi/manage/override.go package manage import ( "context" "encoding/json" "log/slog" "net/http" "time" "git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier" "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) // globalOverrideStore ist das Interface für Handler-Tests. type globalOverrideStore interface { Get(ctx context.Context) (*store.GlobalOverride, error) Upsert(ctx context.Context, overrideType string, until time.Time) error Delete(ctx context.Context) error } // HandleGetGlobalOverride gibt den aktuellen globalen Override zurück (204 wenn keiner aktiv). func HandleGetGlobalOverride(overrides globalOverrideStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { o, err := overrides.Get(r.Context()) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if o == nil || time.Now().After(o.Until) { w.WriteHeader(http.StatusNoContent) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(o) //nolint:errcheck } } // HandleSetGlobalOverride setzt den globalen Override und schickt sofort MQTT an alle Screens. func HandleSetGlobalOverride(overrides globalOverrideStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { Type string `json:"type"` Until time.Time `json:"until"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if body.Type != "on" && body.Type != "off" { http.Error(w, `type must be "on" or "off"`, http.StatusBadRequest) return } if body.Until.IsZero() || !time.Now().Before(body.Until) { http.Error(w, "until must be in the future", http.StatusBadRequest) return } if err := overrides.Upsert(r.Context(), body.Type, body.Until); err != nil { slog.Error("set global override: upsert failed", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } // Sofort MQTT an alle zugänglichen Screens schicken (falls screens/notifier vorhanden) if screens != nil && notifier != nil { u := reqcontext.UserFromContext(r.Context()) var allScreens []*store.Screen if u != nil { switch u.Role { case "admin": allScreens, _ = screens.ListAll(r.Context()) default: allScreens, _ = screens.GetAccessibleScreens(r.Context(), u.ID) } } action := "display_" + body.Type for _, sc := range allScreens { if err := notifier.SendDisplayCommand(sc.Slug, action); err != nil { slog.Warn("set global override: send command failed", "slug", sc.Slug, "err", err) } } } w.WriteHeader(http.StatusNoContent) } } // HandleDeleteGlobalOverride entfernt den globalen Override. func HandleDeleteGlobalOverride(overrides globalOverrideStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := overrides.Delete(r.Context()); err != nil { slog.Error("delete global override: failed", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } } // HandleSetScreenOverride setzt oder löscht den per-Screen-Override (on_until: null → löschen). func HandleSetScreenOverride(screens *store.ScreenStore, schedules *store.ScreenScheduleStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { screenSlug := r.PathValue("screenSlug") screen, err := screens.GetBySlug(r.Context(), screenSlug) if err != nil { http.Error(w, "screen not found", http.StatusNotFound) return } if !requireScreenAccess(w, r, screen) { return } var body struct { OnUntil *time.Time `json:"on_until"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if body.OnUntil != nil && !time.Now().Before(*body.OnUntil) { http.Error(w, "on_until must be in the future", http.StatusBadRequest) return } if err := schedules.SetOverrideOnUntil(r.Context(), screen.ID, body.OnUntil); err != nil { slog.Error("set screen override: db error", "screen_id", screen.ID, "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } }