morz-infoboard/docs/TEMPLATE-EDITOR.md
Jesko Anschütz dd3ec070f7 Security-Review + Phase 6: CSRF, Rate-Limiting, Tenant-Isolation, Screenshot, Ansible
### Security-Fixes (K1–K6, W1–W4, W7, N1, N5–N6, V1, V5–V7)
- K1: CSRF-Schutz via Double-Submit-Cookie (httpapi/csrf.go + csrf_helpers.go)
- K2: requireScreenAccess() in allen manage-Handlern (Tenant-Isolation)
- K3: Tenant-Check bei DELETE /api/v1/media/{id}
- K4: requirePlaylistAccess() + GetByItemID() für JSON-API Playlist-Routen
- K5: Admin-Passwort nur noch als [gesetzt] geloggt
- K6: POST /api/v1/screens/register mit Pre-Shared-Secret (MORZ_INFOBOARD_REGISTER_SECRET)
- W1: Race Condition bei order_index behoben (atomare Subquery in AddItem)
- W2: Graceful Shutdown mit 15s Timeout auf SIGTERM/SIGINT
- W3: http.MaxBytesReader (512 MB) in allen Upload-Handlern
- W4: err.Error() nicht mehr an den Client
- W7: Template-Execution via bytes.Buffer (kein partial write bei Fehler)
- N1: Rate-Limiting auf /login (5 Versuche/Minute pro IP, httpapi/ratelimit.go)
- N5: Directory-Listing auf /uploads/ deaktiviert (neuteredFileSystem)
- N6: Uploads nach Tenant getrennt (uploads/{tenantSlug}/)
- V1: Upload-Logik konsolidiert in internal/fileutil/fileutil.go
- V5: Cookie-Name als Konstante reqcontext.SessionCookieName
- V6: Strukturiertes Logging mit log/slog + JSON-Handler
- V7: DB-Pool wird im Graceful-Shutdown geschlossen

### Phase 6: Screenshot-Erzeugung
- player/agent/internal/screenshot/screenshot.go erstellt
- Integration in app.go mit MORZ_INFOBOARD_SCREENSHOT_EVERY Config

### UX: PDF.js Integration
- pdf.min.js + pdf.worker.min.js als lokale Assets eingebettet
- Automatisches Seitendurchblättern im Player

### Ansible: Neue Rollen
- signage_base, signage_server, signage_provision erstellt
- inventory.yml und site.yml erweitert

### Konzept-Docs
- GRUPPEN-KONZEPT.md, KAMPAGNEN-AKTIVIERUNG.md, MONITORING-KONZEPT.md
- PROVISION-KONZEPT.md, TEMPLATE-EDITOR.md, WATCHDOG-KONZEPT.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:06:35 +01:00

18 KiB

Info-Board Neu - Template-Editor fuer globale Kampagnen

Ziel

Der Template-Editor ist ein Bereich des Admin-UI fuer die fachliche Erstellung und Verwaltung globaler Templates und deren operativen Aktivierungen als Kampagnen.

Dieses Dokument definiert:

  • Welche Schritte ein Admin unternimmt, um ein Template zu erstellen
  • Welche Felder und Optionen der Editor anbietet
  • Wie Templates zu Kampagnen aktiviert werden
  • Wie die Abbildung im Datenmodell aussieht

Grundlagen zu Template-Typen, Slot-Modell und Message-Wall finden sich in docs/TEMPLATE-KONZEPT.md.

1. Template-Verwaltung

Template-Liste

Seite: Admin → Templates

Anzeige:

Tabelle mit allen Templates:

Name Typ Zielgruppe Szenen Erstellt Status
Weihnachtsmotiv 2025 full_screen_media alle 1 2025-01-15 draft
Schriftzug Infowand message_wall wall-all 9 2025-02-01 active
Event-Tag 25.03 screen_specific_scene [info01, info02, ...] 2 2025-03-01 draft

Aktionen pro Zeile:

  • "Bearbeiten" — öffnet Template-Editor
  • "Kopieren" — dupliziert als neue Draft
  • "Löschen" — nur wenn keine aktiven Kampagnen
  • "Vorschau" — zeigt Layout (fuer message_wall) oder Asset-Galerien
  • "Aktivieren" — schneller Weg zu Kampagne starten

Template-Editor (Erstellung/Bearbeitung)

Phase 1 — Grunddaten

┌─────────────────────────────────────────┐
│ Neues Template erstellen                │
├─────────────────────────────────────────┤
│                                         │
│ Name *                                  │
│ [ Weihnachtsmotiv 2025_______________ ]│
│ technischer slug wird automatisch       │
│                                         │
│ Template-Typ *                          │
│ ⦿ full_screen_media                    │
│ ○ message_wall                         │
│ ○ screen_specific_scene                │
│                                         │
│ Beschreibung                            │
│ [ Weihnachtliche Grafik fuer alle___  ] │
│   [ Screens __________________________ ]│
│                                         │
│ Zielgruppe / Screens *                  │
│ ⦿ Alle Screens                         │
│ ○ Nach Gruppe auswaehlen               │
│   [Dropdown: wall-all, single-all, ...] │
│ ○ Einzelne Screens auswaehlen          │
│   [Checkbox-Liste mit Filterung]       │
│                                         │
│ [Weiter >]  [Abbrechen]                │
└─────────────────────────────────────────┘

Validierung:

  • Name ist erforderlich
  • Name ist eindeutig
  • Template-Typ ist erforderlich
  • Zielgruppe ist erforderlich (keine leere Zuweisung)

Phase 2 — Szenen/Inhalte

Fuer full_screen_media:

┌─────────────────────────────────────────┐
│ Szenen und Inhalte                      │
├─────────────────────────────────────────┤
│                                         │
│ Szene 1: Vollbild-Grafik                │
│                                         │
│ Medientyp *                             │
│ ○ Bild                                  │
│ ○ Video                                 │
│ ○ PDF                                   │
│ ⦿ Webseite (HTML)                      │
│                                         │
│ Portrait-Asset (Hochformat)             │
│ [Upload oder URL]                       │
│ [ Datei auswaehlen ]  [Neue URL]        │
│ oder vorher gemanagte Assets: [Liste]   │
│                                         │
│ Landscape-Asset (Querformat) [optional] │
│ [ Datei auswaehlen ]  [Neue URL]        │
│                                         │
│ Anzeigedauer (Sekunden)                 │
│ [60_____] Standard: 10                  │
│                                         │
│ Load-Timeout (Sekunden)                 │
│ [10_____] Standard: 10                  │
│                                         │
│ gueltig ab                              │
│ [ 2025-03-25 ] [ 00:00 ]               │
│ (leer = sofort gueltig)                 │
│                                         │
│ gueltig bis                             │
│ [ 2025-04-01 ] [ 00:00 ]               │
│ (leer = unendlich)                      │
│                                         │
│ [+ Weitere Szene hinzufuegen]           │
│                                         │
│ [Zurueck <]  [Speichern & Aktivieren]  │
│             [Speichern]                 │
│             [Abbrechen]                 │
└─────────────────────────────────────────┘

Fuer message_wall:

┌─────────────────────────────────────────┐
│ Message-Wall Layout                     │
├─────────────────────────────────────────┤
│                                         │
│ Layout-Template                         │
│ [Dropdown: 3x3-Grid, 2x2-Grid, ...]    │
│                                         │
│ Anzeigedauer (Sekunden)                 │
│ [10_____]                              │
│                                         │
│ Gesamt-Grafik oder Text eingeben        │
│ [Rich-Text-Editor oder Bild-Upload]    │
│                                         │
│ Vorschau: [Zeigt Einteilung in Slots] │
│                                         │
│ Slot-Zuordnung: [Interaktive Zuordnung] │
│ Slot wall-r1-c1 → Screen info01        │
│ Slot wall-r1-c2 → Screen info02        │
│ ... (9 Slots insgesamt)                │
│                                         │
│ [+ Layout-Typ aendernx]  [Speichern]   │
│                                         │
│ [Zurueck <]  [Speichern & Aktivieren]  │
│             [Speichern]                 │
│             [Abbrechen]                 │
└─────────────────────────────────────────┘

Fuer screen_specific_scene:

┌─────────────────────────────────────────┐
│ Monitorindividuelle Szenen              │
├─────────────────────────────────────────┤
│                                         │
│ Szene 1: Infowand                       │
│                                         │
│ Zielgruppe                              │
│ ⦿ Gruppe: [Dropdown: wall-all]         │
│ ○ Einzelne Screens: [Checkboxen]       │
│                                         │
│ Asset                                   │
│ [Upload oder URL]                      │
│                                         │
│ Dauer, Timeout, gueltig_von/bis        │
│ [... wie oben ...]                      │
│                                         │
│ [+ Weitere Szene hinzufuegen]           │
│                                         │
│ [Zurueck <]  [Speichern & Aktivieren]  │
└─────────────────────────────────────────┘

2. Kampagnen-Verwaltung

Kampagnen sind die operativen Instanzen von Templates.

Kampagnen-Liste

Seite: Admin → Kampagnen

Anzeige:

Name Template Aktiv Zielgruppe gueltig von gueltig bis Betroffene Screens
Weihnachten Dekoration Weihnachtsmotiv 2025 alle 2025-12-01 2025-12-26 13 Screens
Schriftzug Januar Schriftzug Infowand wall-all 2025-01-06 2025-01-31 9 Screens

Aktionen:

  • "Bearbeiten" — Kampagnen-Eigenschaften aendern
  • "Aktivieren/Deaktivieren" — Toggle sofort
  • "Vorschau" — zeigt betroffene Screens mit Rendering
  • "Duplizieugen" — als neue Kampagne mit anderem Template
  • "Loeschen" — wenn inaktiv und abgelaufen

Neue Kampagne starten

Workflow Option 1 — Von Template aus:

Template-Liste → [Template] → "Aktivieren"

┌─────────────────────────────────────────┐
│ Kampagne starten: Weihnachtsmotiv 2025 │
├─────────────────────────────────────────┤
│                                         │
│ Kampagnen-Name                          │
│ [ Weihnachten 2025 einfuehrung____ ]   │
│                                         │
│ Aktiv ab sofort?                        │
│ ⦿ Ja                                   │
│ ○ Geplant fuer: [Datum/Zeit auswaehlen]│
│   [ 2025-12-01 ] [ 09:00 ]             │
│                                         │
│ Gueltig von                             │
│ [ 2025-12-01 ]  [ 00:00 ]              │
│                                         │
│ Gueltig bis                             │
│ [ 2025-12-26 ]  [ 23:59 ]              │
│                                         │
│ Prioritaet (gegenueber Playlist)        │
│ [1 (hoehere Werte sind wichtiger)] ___ │
│                                         │
│ Auto-Deaktivierung bei Ablauf?          │
│ ⦿ Ja                                   │
│ ○ Nein (Kampagne bleibt inaktiv)       │
│                                         │
│ [Kampagne starten]  [Abbrechen]        │
└─────────────────────────────────────────┘

Workflow Option 2 — Neue Kampagne ohne Template:

Admin → Kampagnen → "+ Neue Kampagne"

[Template auswaehlen] → [Grunddaten] → [Aktivierung]

Kampagnen-Detailseite

Anzeige einer laufenden Kampagne:

Kampagne: Weihnachten 2025 einfuehrung
Status: AKTIV seit 2025-12-01 09:00

Template: Weihnachtsmotiv 2025 (full_screen_media)
Zielgruppe: Alle (13 Screens)

Gueltig: 2025-12-01 00:00 bis 2025-12-26 23:59
Prioritaet: 1

Betroffene Screens:
┌──────────────────────────────┐
│ info01  online   aktiv       │ [Screenshot]
│ info02  online   aktiv       │ [Screenshot]
│ info03  offline  ausstehend  │
│ info04  online   aktiv       │ [Screenshot]
│ ... (10 weitere) ...         │
└──────────────────────────────┘

Aktionen:
[Deaktivieren]  [Bearbeiten]  [Vorschau aendernx]

Aktivierungsverlauf:
2025-12-01 09:00 — Kampagne gestartet von admin@...
2025-12-01 09:05 — 9 Screens haben gerendert
2025-12-01 10:30 — info03 ging offline, Kampagnen-Inhalt wartet auf Rueckkehr

3. Verknuepfung zur Prioritaetsregel

Die Regel campaign > tenant_playlist > fallback ist:

  • hardcoded im Player
  • administrierbar ueber die Kampagnen-Aktivierung
  • vorhersagbar durch klare Doku

Abbildung im System

Fuer jeden Screen:
  IF Kampagne fuer diesen Screen aktiv UND gueltig_von <= jetzt <= gueltig_bis
    THEN Zeige Kampagnen-Inhalt
  ELSE IF Tenant-Playlist hat gueltige Items
    THEN Zeige Tenant-Playlist
  ELSE
    Zeige Fallback

Diese Logik wird:

  1. Serverseitig berechnet bei jedem Sync-Request (HTTP /api/v1/screens/{screenSlug}/playlist)
  2. Spielerseitig nochmals geprueft beim Rendering (fuer Offline-Robustheit)

Admin-Sichtbarkeit

Die Admin-UI zeigt auf der Seite "Screens" fuer jeden Monitor:

info01
├── Kampagne (AKTIV bis 2025-12-26)
│   └── Weihnachten 2025 einfuehrung
├── Fallback (wird nach Kampagnen-Ablauf gezeigt)
└── Tenant Playlist
    ├── Playlist A (Tenant XYZ)
    │   ├── Bild-1 (gueltig bis 2025-04-01)
    │   ├── Video-2 (laedt...)
    │   └── Webseite-3
    └── Fallback-Verzeichnis

Diese View zeigt, was der Screen aktuell gerade zeigt und warum.

4. Datenmodell

Tabelle templates

CREATE TABLE templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  slug TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  description TEXT,
  template_type TEXT NOT NULL CHECK (template_type IN ('message_wall', 'full_screen_media', 'screen_specific_scene')),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_by_user_id TEXT NOT NULL,

  -- Serializierte Konfiguration (JSON)
  config JSONB NOT NULL DEFAULT '{}'
  -- Beispiele:
  -- {
  --   "target_mode": "all_screens" | "group" | "specific_screens",
  --   "target_group": "wall-all" (wenn target_mode = "group"),
  --   "target_screen_ids": ["..."] (wenn target_mode = "specific_screens"),
  --   "scenes": [
  --     {
  --       "media_type": "image|video|pdf|webpage|html",
  --       "asset_id": "...",
  --       "portrait_asset_id": "..." (optional),
  --       "landscape_asset_id": "..." (optional),
  --       "duration_sec": 10,
  --       "load_timeout_sec": 10,
  --       "valid_from": "2025-03-25T00:00:00Z",
  --       "valid_until": "2025-04-01T23:59:59Z"
  --     }
  --   ]
  -- }
);

Tabelle campaigns

CREATE TABLE campaigns (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  template_id UUID NOT NULL REFERENCES templates(id),
  active BOOLEAN NOT NULL DEFAULT false,
  priority INT NOT NULL DEFAULT 1,
  valid_from TIMESTAMPTZ NOT NULL,
  valid_until TIMESTAMPTZ,
  auto_deactivate BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_by_user_id TEXT NOT NULL,

  -- ueberschreiben/erweitern Template-Zielgruppe (optional)
  target_mode TEXT CHECK (target_mode IN ('template', 'all_screens', 'group', 'specific_screens')),
  target_group TEXT,
  target_screen_ids UUID[] DEFAULT '{}'::uuid[]
);

Tabelle campaign_screen_assignments (generiert)

Diese Tabelle wird serverseitig generiert/gepflegt, wenn eine Kampagne aktiv wird.

Sie expandiert Gruppen in konkrete Screen-IDs:

CREATE TABLE campaign_screen_assignments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  campaign_id UUID NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
  screen_id UUID NOT NULL REFERENCES screens(id) ON DELETE CASCADE,
  assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(campaign_id, screen_id)
);

Logik:

IF campaign.target_mode = 'template'
  THEN Fuelle campaign_screen_assignments aus template.config.target_screen_ids
ELSE IF campaign.target_mode = 'group'
  THEN Fuelle campaign_screen_assignments aus allen Screens in campaign.target_group
ELSE IF campaign.target_mode = 'specific_screens'
  THEN Fuelle campaign_screen_assignments aus campaign.target_screen_ids
ELSE
  (alle Screens)

5. Praxis-Beispiele

Beispiel 1 — Weihnachtsplakatierung (full_screen_media)

Szenario:

Admin will ab 01.12.2025 fuer 4 Wochen ein rotes Weihnachtsmotiv auf allen Screens zeigen.

Schritte:

  1. Admin → Templates → "+ Neues Template"

    • Name: Weihnachtsmotiv 2025
    • Typ: full_screen_media
    • Zielgruppe: Alle Screens
  2. Szene hinzufuegen:

    • Bild hochladen (passend fuer Portrait und Landscape)
    • Dauer: 10 Sekunden
  3. Speichern → Editor zeigt Draft mit Vorschau

  4. Admin → Templates → [Weihnachtsmotiv 2025] → "Aktivieren"

    • Kampagnen-Name: Weihnachten 2025 globale Dekoration
    • Gueltig von: 2025-12-01
    • Gueltig bis: 2025-12-26
    • Aktiv ab: sofort
  5. Kampagne speichern → Sofort sichtbar auf allen Screens

Beispiel 2 — Schriftzug ueber die Infowand (message_wall)

Szenario:

Admin hat eine neue message_wall-Gruppe "wall-all" mit 9 Screens. Er will ein riesiges rotes Schriftzug-Motiv aufteilen und auf allen 9 Screens verteilen.

Schritte:

  1. Admin → Templates → "+ Neues Template"

    • Name: Rotes Schriftzug auf Infowand
    • Typ: message_wall
    • Zielgruppe: Gruppe: wall-all
  2. Layout waehlen: 3x3-Grid (passt zu 9 Screens)

  3. Gesamte Grafik hochladen (oder als Text eingeben)

  4. Slot-Zuordnung:

    • System zeigt interaktive 3x3-Vorschau
    • Admin tuen: "Slot 1 → info01", "Slot 2 → info02", ...
    • System generiert automatisch die Crop-Regionen
  5. Speichern + Aktivieren

    • Jeder Screen zeigt seinen Ausschnitt

Beispiel 3 — Deaktivierung und Fallback

Szenario:

Kampagne laueft seit 2 Wochen. Admin will sie sofort stoppen, damit Screens auf ihre normalen Playlists zurueckfallen.

Aktion:

Admin → Kampagnen → [Kampagne] → "Deaktivieren"

Folge:

  • Server setzt campaigns.active = false
  • Bei naechstem Sync ladet jeder Player wieder die Tenant-Playlist
  • Fallback-Verzeichnis wird nur noch angezeigt, wenn tenantbezogene Playlist leer ist

6. Zusammenfassung

Der Template-Editor:

  • ist zwei-stufig — Template-Verwaltung + Kampagnen-Aktivierung
  • ist intuitiv — Multi-Step-Formulare mit Vorschauen
  • unterstützt alle Template-Typen — full_screen, message_wall, screen_specific
  • haelt die Prioritaetsregel transparent — Admin sieht, welche Kampagne welche Screens uebersteuert
  • ist zukunftssicher — Datenmodell skaliert mit neuen Template-Typen