# Design: Globaler Override & Wochenend-Sperre **Datum:** 2026-03-27 **Status:** Approved ## Zusammenfassung Schul-Ferien, Elternabende und Wochenenden erfordern, dass Monitorzeitpläne temporär ignoriert werden. Diese Funktion ergänzt das bestehende Zeitplansystem um: 1. **Globaler Override** — alle zugänglichen Monitore bis zu einem Zeitpunkt aus- oder einschalten 2. **Per-Screen Override** — einen einzelnen Monitor trotz globalem Off oder Wochenende einschalten 3. **Wochenend-Sperre** — samstags und sonntags werden Zeitpläne ignoriert, Monitore bleiben aus --- ## Datenmodell ### Migration 006 — Tabelle `global_override` ```sql CREATE TABLE global_override ( id int PRIMARY KEY DEFAULT 1, type text NOT NULL, -- 'on' oder 'off' until timestamptz NOT NULL, set_at timestamptz NOT NULL DEFAULT now(), CHECK (id = 1) ); ``` Immer genau eine Zeile (Upsert auf `id = 1`). Kein aktiver Override = leere Tabelle. ### Migration 007 — Spalte in `screen_schedules` ```sql ALTER TABLE screen_schedules ADD COLUMN override_on_until timestamptz; ``` `NULL` = kein per-Screen-Override aktiv. ### Store-Erweiterungen - `GlobalOverrideStore`: `Get(ctx)`, `Upsert(ctx, type, until)`, `Delete(ctx)` - `ScreenScheduleStore.Upsert`: nimmt `override_on_until` auf; `Get` liefert es zurück --- ## Prioritätslogik Die neue Funktion `resolveDesiredState()` im Reconciler löst `desiredState()` ab: ``` 1. per-Screen override_on_until > now → "on" 2. Globaler Override aktiv (until > now): type "off" → "off" type "on" → "on" 3. Wochenende (Samstag oder Sonntag) → "off" 4. Normaler Zeitplan (bestehende Logik) ``` **Scheduler (Minuten-Tick):** Sendet kein Kommando, wenn Override oder Wochenende aktiv ist — der Reconciler korrigiert den Zustand. **Reconciler (5-Minuten-Tick):** Nutzt `resolveDesiredState()`. Abgelaufene Overrides werden automatisch ignoriert (`until < now`), kein explizites Löschen nötig. **Sofortige Aktivierung:** Beim Setzen eines globalen Overrides schickt der Handler nach dem DB-Upsert sofort MQTT-Kommandos an alle zugänglichen Screens. --- ## API-Endpunkte ### Globaler Override | Methode | Pfad | Beschreibung | |---------|------|--------------| | `GET` | `/api/v1/global-override` | Aktuellen Override abrufen (204 wenn keiner aktiv) | | `POST` | `/api/v1/global-override` | Override setzen + sofort MQTT an alle Screens | | `DELETE` | `/api/v1/global-override` | Override löschen (Reconciler normalisiert) | **POST-Body:** ```json { "type": "off", "until": "2026-04-05T18:00:00Z" } ``` **GET-Response (200):** ```json { "type": "off", "until": "2026-04-05T18:00:00Z", "set_at": "..." } ``` **Auth:** `authUser` (eingeloggt). Handler filtert auf alle zugänglichen Screens des Users. ### Per-Screen Override Erweiterung des bestehenden Endpunkts: ``` POST /api/v1/screens/{screenSlug}/schedule ``` Neues optionales Feld im Body: ```json { "override_on_until": "2026-04-05T18:00:00Z" } ``` `null` löscht den Override. Kein neuer Endpunkt nötig. --- ## UI ### Monitorübersicht (`/manage`) — globaler Override - Neuer Banner-Bereich oberhalb der Monitorkarten - Zwei Buttons: **"Alle ausschalten bis…"** / **"Alle einschalten bis…"** - Klick klappt ein Inline-Formular auf (`` + "Setzen"-Button) - Wenn Override aktiv: farbiges Hinweisband mit Zeitangabe und "Aufheben"-Link - Bestehender 30s-Poll aktualisiert auch diesen Status ### Monitorkarte (Übersicht) — per-Screen Override - Unter den Ein/Aus-Buttons: aufklappbarer Bereich mit `` + "Einschalten bis"-Button - Wenn aktiv: Badge `"ein bis 05.04. 18:00 ✕"` ### Detailseite (`/manage/{slug}`) — per-Screen Override - Im bestehenden "Zeitplan"-Kasten: neues Feld "Einschalten bis (Override)" - `` + "Setzen" / "Aufheben"-Button - Wird über den bestehenden `saveSchedule()`-Aufruf gespeichert (neues Feld im Body) --- ## Timezone `` liefert Browser-Lokalzeit ohne Timezone-Offset. Das Frontend sendet die Zeit als ISO-8601-String mit Offset (z.B. `+01:00`). Der Backend-Handler parst mit `time.Parse(time.RFC3339, ...)`. Die bestehende `TZ`-Env-Variable des Servers steuert die Interpretation im Scheduler/Reconciler — kein neuer Konfigurationsaufwand. --- ## Nicht im Scope - Per-Screen "ausschalten bis" — nur "einschalten bis" pro Monitor - Globaler Override ist nicht per-Screen einschränkbar (kommt aus der Übersicht, gilt für alle zugänglichen Screens) - Sofortige Aktion beim Override-Ablauf — Reconciler (≤5 Min) reicht