From 83af005fad746b2a62185c213f24df5339e72ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Fri, 27 Mar 2026 07:17:45 +0100 Subject: [PATCH] feat(api): POST /api/v1/screens/{slug}/schedule + Scheduler verdrahtet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScheduleStore in RouterDeps, HandleUpdateSchedule-Handler, Scheduler-Goroutine in app.Run(), ScreenStore.GetByID hinzugefügt. Co-Authored-By: Claude Sonnet 4.6 --- server/backend/internal/app/app.go | 34 +++++++++----- .../internal/httpapi/manage/schedule.go | 46 +++++++++++++++++++ server/backend/internal/httpapi/router.go | 21 +++++---- 3 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 server/backend/internal/httpapi/manage/schedule.go diff --git a/server/backend/internal/app/app.go b/server/backend/internal/app/app.go index 9985a6c..3804c63 100644 --- a/server/backend/internal/app/app.go +++ b/server/backend/internal/app/app.go @@ -17,16 +17,19 @@ import ( "git.az-it.net/az/morz-infoboard/server/backend/internal/db" "git.az-it.net/az/morz-infoboard/server/backend/internal/httpapi" "git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier" + "git.az-it.net/az/morz-infoboard/server/backend/internal/scheduler" "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) type App struct { - Config config.Config - server *http.Server - notifier *mqttnotifier.Notifier - authStore *store.AuthStore - dbPool *db.Pool // V7: für db.Close() im Shutdown - logger *log.Logger + Config config.Config + server *http.Server + notifier *mqttnotifier.Notifier + authStore *store.AuthStore + scheduleStore *store.ScreenScheduleStore + screenStore *store.ScreenStore + dbPool *db.Pool // V7: für db.Close() im Shutdown + logger *log.Logger } func New() (*App, error) { @@ -58,6 +61,7 @@ func New() (*App, error) { media := store.NewMediaStore(pool.Pool) playlists := store.NewPlaylistStore(pool.Pool) authStore := store.NewAuthStore(pool.Pool) + schedules := store.NewScreenScheduleStore(pool.Pool) // Ensure admin user exists — generate a random password if none is configured. adminPassword := cfg.AdminPassword @@ -96,18 +100,21 @@ func New() (*App, error) { AuthStore: authStore, Notifier: notifier, ScreenshotStore: ss, + ScheduleStore: schedules, Config: cfg, UploadDir: cfg.UploadDir, Logger: logger, }) return &App{ - Config: cfg, - server: &http.Server{Addr: cfg.HTTPAddress, Handler: handler}, - notifier: notifier, - authStore: authStore, - dbPool: pool, // V7: Referenz für Shutdown - logger: logger, + Config: cfg, + server: &http.Server{Addr: cfg.HTTPAddress, Handler: handler}, + notifier: notifier, + authStore: authStore, + scheduleStore: schedules, + screenStore: screens, + dbPool: pool, // V7: Referenz für Shutdown + logger: logger, }, nil } @@ -137,6 +144,9 @@ func (a *App) Run() error { } }() + // Display-Zeitplan-Scheduler + go scheduler.Run(ctx, a.scheduleStore, a.screenStore, a.notifier) + // W2: Signal-Handler für Graceful Shutdown. sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) diff --git a/server/backend/internal/httpapi/manage/schedule.go b/server/backend/internal/httpapi/manage/schedule.go new file mode 100644 index 0000000..acc8442 --- /dev/null +++ b/server/backend/internal/httpapi/manage/schedule.go @@ -0,0 +1,46 @@ +package manage + +import ( + "encoding/json" + "net/http" + + "git.az-it.net/az/morz-infoboard/server/backend/internal/store" +) + +// HandleUpdateSchedule speichert den Zeitplan für ein Display. +// Body: {"schedule_enabled":true,"power_on_time":"06:00","power_off_time":"22:00"} +func HandleUpdateSchedule(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 { + ScheduleEnabled bool `json:"schedule_enabled"` + PowerOnTime string `json:"power_on_time"` + PowerOffTime string `json:"power_off_time"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "invalid JSON", http.StatusBadRequest) + return + } + + if err := schedules.Upsert(r.Context(), &store.ScreenSchedule{ + ScreenID: screen.ID, + ScheduleEnabled: body.ScheduleEnabled, + PowerOnTime: body.PowerOnTime, + PowerOffTime: body.PowerOffTime, + }); err != nil { + http.Error(w, "db error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/server/backend/internal/httpapi/router.go b/server/backend/internal/httpapi/router.go index 9b3fd4c..556f9b5 100644 --- a/server/backend/internal/httpapi/router.go +++ b/server/backend/internal/httpapi/router.go @@ -13,17 +13,18 @@ import ( // RouterDeps holds all dependencies needed to build the HTTP router. type RouterDeps struct { - StatusStore playerStatusStore - TenantStore *store.TenantStore - ScreenStore *store.ScreenStore - MediaStore *store.MediaStore - PlaylistStore *store.PlaylistStore - AuthStore *store.AuthStore + StatusStore playerStatusStore + TenantStore *store.TenantStore + ScreenStore *store.ScreenStore + MediaStore *store.MediaStore + PlaylistStore *store.PlaylistStore + AuthStore *store.AuthStore Notifier *mqttnotifier.Notifier ScreenshotStore *ScreenshotStore + ScheduleStore *store.ScreenScheduleStore Config config.Config - UploadDir string - Logger *log.Logger + UploadDir string + Logger *log.Logger } func NewRouter(deps RouterDeps) http.Handler { @@ -187,6 +188,10 @@ func registerManageRoutes(mux *http.ServeMux, d RouterDeps) { mux.Handle("POST /api/v1/screens/{screenSlug}/display", authScreen(http.HandlerFunc(manage.HandleDisplayCommand(notifier)))) + // ── Schedule control ────────────────────────────────────────────────── + mux.Handle("POST /api/v1/screens/{screenSlug}/schedule", + authScreen(http.HandlerFunc(manage.HandleUpdateSchedule(d.ScreenStore, d.ScheduleStore)))) + // ── JSON API — screens ──────────────────────────────────────────────── // Self-registration: no auth (player calls this on startup). mux.HandleFunc("POST /api/v1/screens/register",