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

494 lines
18 KiB
Markdown

# 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`
```sql
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`
```sql
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:
```sql
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