From 2bf82eed5379c49a79420160002afd2c6715459e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Fri, 27 Mar 2026 20:30:52 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Upsert=20l=C3=B6scht=20override=5Fon=5Fu?= =?UTF-8?q?ntil=20nicht=20mehr;=20README=20+=20Auth-Kommentar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScreenScheduleStore.Upsert: override_on_until aus INSERT und ON CONFLICT entfernt — verhindert stillen Datenverlust beim Speichern eines Zeitplans. SetOverrideOnUntil bleibt alleinig zuständig für diese Spalte. - README.md: GlobalOverrideStore, vier neue API-Routen, Wochenend-Sperre und Migration 006_override.sql dokumentiert. - override.go: Auth-Scope-Kommentar über HandleSetGlobalOverride ergänzt. Co-Authored-By: Claude Sonnet 4.6 --- server/backend/README.md | 23 +++++++++++++++++-- .../internal/httpapi/manage/override.go | 4 ++++ server/backend/internal/store/store.go | 15 ++++++------ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/server/backend/README.md b/server/backend/README.md index 760b1f9..328b41b 100644 --- a/server/backend/README.md +++ b/server/backend/README.md @@ -17,7 +17,7 @@ Dieses Verzeichnis enthaelt das zentrale Go-Backend fuer das Info-Board-System. - `internal/app/` — App-Initialisierung und Lifecycle - `internal/config/` — Konfiguration via Umgebungsvariablen - `internal/db/` — PostgreSQL-Anbindung und Migrations-Runner -- `internal/store/` — Datenbankzugriff (TenantStore, ScreenStore, MediaStore, PlaylistStore, AuthStore) +- `internal/store/` — Datenbankzugriff (TenantStore, ScreenStore, MediaStore, PlaylistStore, AuthStore, ScreenScheduleStore, GlobalOverrideStore) - `internal/fileutil/` — Upload-Hilfsfunktionen (SaveUploadedFile mit Tenant-Isolation) - `internal/httpapi/` — HTTP-Routing, Middleware und Handler - `internal/httpapi/csrf.go` — Double-Submit-Cookie CSRF-Schutz @@ -42,6 +42,10 @@ Uhrzeit übereinstimmt — per MQTT den Befehl `display_on` bzw. `display_off` s Der Scheduler wird in `internal/app/app.go` als Goroutine gestartet und laeuft bis zum Kontext-Abbruch beim Server-Shutdown. +**Wochenend-Sperre:** An Samstagen und Sonntagen werden Zeitplaene ignoriert — der Reconciler +sendet dann keine automatischen Ein-/Ausschalt-Kommandos. Manuelle Overrides (global oder +per-Screen) wirken jedoch auch am Wochenende. + ## Datenbank-Stores ### AuthStore (`internal/store/auth.go`) @@ -60,6 +64,16 @@ Kontext-Abbruch beim Server-Shutdown. - `EnsureAdminUser(ctx, tenantSlug, password)` — Admin-User beim Start anlegen - `VerifyPassword(ctx, userID, password)` — Passwort gegen bcrypt-Hash pruefen +### GlobalOverrideStore (`internal/store/store.go`) + +Verwaltet einen systemweiten Display-Override (max. 1 Zeile in `global_override`): + +- `Get(ctx)` — aktuellen globalen Override laden (nil wenn keiner gesetzt) +- `Upsert(ctx, type, until)` — Override setzen oder ueberschreiben (`type`: `"on"` | `"off"`) +- `Delete(ctx)` — Override entfernen + +Der Reconciler im Scheduler wertet den globalen Override aus und wendet ihn auf alle Screens an. + ### ScreenStore (`internal/store/screen.go`) **Screen-User Zugriffskontrolle:** @@ -117,6 +131,10 @@ Kontext-Abbruch beim Server-Shutdown. | GET | `/api/v1/screens/{screenId}/screenshot` | Screenshot eines Screens abrufen | | POST | `/api/v1/screens/{screenSlug}/display` | Display ein-/ausschalten (MQTT) | | POST | `/api/v1/screens/{screenSlug}/schedule` | Display-Zeitplan speichern | +| GET | `/api/v1/global-override` | Globalen Override abrufen (204 = kein aktiver Override) | +| POST | `/api/v1/global-override` | Globalen Override setzen (type + until); sendet sofort MQTT | +| DELETE | `/api/v1/global-override` | Globalen Override loeschen | +| POST | `/api/v1/screens/{screenSlug}/override` | Per-Screen-Override setzen oder loeschen (on_until: null = loeschen) | ### Nur Admins (`RequireAuth` + `RequireAdmin`) @@ -182,8 +200,9 @@ Middleware zur rollenbasierten Zugriffskontrolle auf Screen-Ressourcen. ## Migrationen -- `001_core.sql` — initiales Schema (Tenants, Screens, Playlists, Media, etc.) +- `001_initial.sql` — initiales Schema (Tenants, Screens, Playlists, Media, etc.) - `002_auth.sql` — Auth-Tabellen (`users`, `sessions`) - `003_user_screen_permissions.sql` — Screen-User Management (`user_screen_permissions`) - `004_screen_status.sql` — Display-Zustand pro Screen (`screen_status`: screen_id, display_state, reported_at) - `005_screen_schedules.sql` — Zeitplan pro Screen (`screen_schedules`: screen_id, schedule_enabled, power_on_time, power_off_time) +- `006_override.sql` — Spalte `override_on_until` in `screen_schedules` (per-Screen-Override) und Tabelle `global_override` (systemweiter Display-Override) diff --git a/server/backend/internal/httpapi/manage/override.go b/server/backend/internal/httpapi/manage/override.go index 466e673..067e5b0 100644 --- a/server/backend/internal/httpapi/manage/override.go +++ b/server/backend/internal/httpapi/manage/override.go @@ -38,6 +38,10 @@ func HandleGetGlobalOverride(overrides globalOverrideStore) http.HandlerFunc { } // HandleSetGlobalOverride setzt den globalen Override und schickt sofort MQTT an alle Screens. +// Hinweis: Der Override wird global gespeichert und vom Reconciler auf alle Screens angewendet. +// Über authOnly haben alle eingeloggten Nutzer Zugriff; die sofortigen MQTT-Kommandos gehen +// jedoch nur an ihre zugänglichen Screens. Soll der Zugriff auf Admins beschränkt werden, +// authOnly durch authAdmin ersetzen. func HandleSetGlobalOverride(overrides globalOverrideStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { diff --git a/server/backend/internal/store/store.go b/server/backend/internal/store/store.go index 9f98f30..2595920 100644 --- a/server/backend/internal/store/store.go +++ b/server/backend/internal/store/store.go @@ -667,16 +667,17 @@ func (s *ScreenScheduleStore) Get(ctx context.Context, screenID string) (*Screen } // Upsert speichert oder aktualisiert den Zeitplan eines Screens. +// Hinweis: override_on_until wird hier bewusst nicht angefasst – das ist +// ausschließlich Aufgabe von SetOverrideOnUntil (saubere Trennung, kein Datenverlust). func (s *ScreenScheduleStore) Upsert(ctx context.Context, sc *ScreenSchedule) error { _, err := s.pool.Exec(ctx, - `insert into screen_schedules (screen_id, schedule_enabled, power_on_time, power_off_time, override_on_until) - values ($1, $2, $3, $4, $5) + `insert into screen_schedules (screen_id, schedule_enabled, power_on_time, power_off_time) + values ($1, $2, $3, $4) on conflict (screen_id) do update - set schedule_enabled = excluded.schedule_enabled, - power_on_time = excluded.power_on_time, - power_off_time = excluded.power_off_time, - override_on_until = excluded.override_on_until`, - sc.ScreenID, sc.ScheduleEnabled, sc.PowerOnTime, sc.PowerOffTime, sc.OverrideOnUntil) + set schedule_enabled = excluded.schedule_enabled, + power_on_time = excluded.power_on_time, + power_off_time = excluded.power_off_time`, + sc.ScreenID, sc.ScheduleEnabled, sc.PowerOnTime, sc.PowerOffTime) return err }