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

4.7 KiB

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

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

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:

{ "type": "off", "until": "2026-04-05T18:00:00Z" }

GET-Response (200):

{ "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:

{ "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