feat(api): POST /api/v1/screens/{slug}/schedule + Scheduler verdrahtet

ScheduleStore in RouterDeps, HandleUpdateSchedule-Handler, Scheduler-Goroutine
in app.Run(), ScreenStore.GetByID hinzugefügt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 07:17:45 +01:00
parent 9b766f9086
commit 83af005fad
3 changed files with 81 additions and 20 deletions

View file

@ -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/db"
"git.az-it.net/az/morz-infoboard/server/backend/internal/httpapi" "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/mqttnotifier"
"git.az-it.net/az/morz-infoboard/server/backend/internal/scheduler"
"git.az-it.net/az/morz-infoboard/server/backend/internal/store" "git.az-it.net/az/morz-infoboard/server/backend/internal/store"
) )
type App struct { type App struct {
Config config.Config Config config.Config
server *http.Server server *http.Server
notifier *mqttnotifier.Notifier notifier *mqttnotifier.Notifier
authStore *store.AuthStore authStore *store.AuthStore
dbPool *db.Pool // V7: für db.Close() im Shutdown scheduleStore *store.ScreenScheduleStore
logger *log.Logger screenStore *store.ScreenStore
dbPool *db.Pool // V7: für db.Close() im Shutdown
logger *log.Logger
} }
func New() (*App, error) { func New() (*App, error) {
@ -58,6 +61,7 @@ func New() (*App, error) {
media := store.NewMediaStore(pool.Pool) media := store.NewMediaStore(pool.Pool)
playlists := store.NewPlaylistStore(pool.Pool) playlists := store.NewPlaylistStore(pool.Pool)
authStore := store.NewAuthStore(pool.Pool) authStore := store.NewAuthStore(pool.Pool)
schedules := store.NewScreenScheduleStore(pool.Pool)
// Ensure admin user exists — generate a random password if none is configured. // Ensure admin user exists — generate a random password if none is configured.
adminPassword := cfg.AdminPassword adminPassword := cfg.AdminPassword
@ -96,18 +100,21 @@ func New() (*App, error) {
AuthStore: authStore, AuthStore: authStore,
Notifier: notifier, Notifier: notifier,
ScreenshotStore: ss, ScreenshotStore: ss,
ScheduleStore: schedules,
Config: cfg, Config: cfg,
UploadDir: cfg.UploadDir, UploadDir: cfg.UploadDir,
Logger: logger, Logger: logger,
}) })
return &App{ return &App{
Config: cfg, Config: cfg,
server: &http.Server{Addr: cfg.HTTPAddress, Handler: handler}, server: &http.Server{Addr: cfg.HTTPAddress, Handler: handler},
notifier: notifier, notifier: notifier,
authStore: authStore, authStore: authStore,
dbPool: pool, // V7: Referenz für Shutdown scheduleStore: schedules,
logger: logger, screenStore: screens,
dbPool: pool, // V7: Referenz für Shutdown
logger: logger,
}, nil }, 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. // W2: Signal-Handler für Graceful Shutdown.
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)

View file

@ -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)
}
}

View file

@ -13,17 +13,18 @@ import (
// RouterDeps holds all dependencies needed to build the HTTP router. // RouterDeps holds all dependencies needed to build the HTTP router.
type RouterDeps struct { type RouterDeps struct {
StatusStore playerStatusStore StatusStore playerStatusStore
TenantStore *store.TenantStore TenantStore *store.TenantStore
ScreenStore *store.ScreenStore ScreenStore *store.ScreenStore
MediaStore *store.MediaStore MediaStore *store.MediaStore
PlaylistStore *store.PlaylistStore PlaylistStore *store.PlaylistStore
AuthStore *store.AuthStore AuthStore *store.AuthStore
Notifier *mqttnotifier.Notifier Notifier *mqttnotifier.Notifier
ScreenshotStore *ScreenshotStore ScreenshotStore *ScreenshotStore
ScheduleStore *store.ScreenScheduleStore
Config config.Config Config config.Config
UploadDir string UploadDir string
Logger *log.Logger Logger *log.Logger
} }
func NewRouter(deps RouterDeps) http.Handler { 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", mux.Handle("POST /api/v1/screens/{screenSlug}/display",
authScreen(http.HandlerFunc(manage.HandleDisplayCommand(notifier)))) 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 ──────────────────────────────────────────────── // ── JSON API — screens ────────────────────────────────────────────────
// Self-registration: no auth (player calls this on startup). // Self-registration: no auth (player calls this on startup).
mux.HandleFunc("POST /api/v1/screens/register", mux.HandleFunc("POST /api/v1/screens/register",