From bb3f11fa66b4e795bedd09e3f4f979293d25e537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Fri, 27 Mar 2026 19:55:58 +0100 Subject: [PATCH] =?UTF-8?q?docs:=20Design-Spec=20f=C3=BCr=20globalen=20Ove?= =?UTF-8?q?rride=20und=20Wochenend-Sperre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../2026-03-27-override-wochenende-design.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-27-override-wochenende-design.md diff --git a/docs/superpowers/specs/2026-03-27-override-wochenende-design.md b/docs/superpowers/specs/2026-03-27-override-wochenende-design.md new file mode 100644 index 0000000..e0abf1e --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-override-wochenende-design.md @@ -0,0 +1,140 @@ +# 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