morz-infoboard/docs/superpowers/specs/2026-03-27-override-wochenende-design.md
Jesko Anschütz bb3f11fa66 docs: Design-Spec für globalen Override und Wochenend-Sperre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:55:58 +01:00

140 lines
4.7 KiB
Markdown

# 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 (`<input type="datetime-local">` + "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 `<input type="datetime-local">` + "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)"
- `<input type="datetime-local">` + "Setzen" / "Aufheben"-Button
- Wird über den bestehenden `saveSchedule()`-Aufruf gespeichert (neues Feld im Body)
---
## Timezone
`<input type="datetime-local">` 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