# 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
