docs: Design-Spec für globalen Override und Wochenend-Sperre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ccec32c832
commit
bb3f11fa66
1 changed files with 140 additions and 0 deletions
140
docs/superpowers/specs/2026-03-27-override-wochenende-design.md
Normal file
140
docs/superpowers/specs/2026-03-27-override-wochenende-design.md
Normal file
|
|
@ -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 (`<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
|
||||||
Loading…
Add table
Reference in a new issue