fix: Upsert löscht override_on_until nicht mehr; README + Auth-Kommentar

- 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 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-27 20:30:52 +01:00
parent db68c84d45
commit 2bf82eed53
3 changed files with 33 additions and 9 deletions

View file

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

View file

@ -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 {

View file

@ -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)
power_off_time = excluded.power_off_time`,
sc.ScreenID, sc.ScheduleEnabled, sc.PowerOnTime, sc.PowerOffTime)
return err
}