morz-infoboard/docs/GRUPPEN-KONZEPT.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

16 KiB

Info-Board Neu - Gruppierungs- und Slot-Modell fuer monitoruebergreifende Layouts

Ziel

Dieses Dokument definiert, wie Screens in Gruppen und Slots organisiert werden.

Gruppen und Slots sind notwendig fuer:

  • Massenaktionen — mehrere Screens mit einer Kampagne ansprechen
  • Monitorwaende — Schriftzuege und Layouts auf mehrere Screens verteilen
  • zukuenftige Skalierbarkeit — neue Displays ohne Neustrukturierung hinzufuegen

Siehe auch docs/TEMPLATE-KONZEPT.md fuer Template-Typen, die Gruppen/Slots verwenden.

1. Screen-Gruppen

Konzept

Eine Gruppe ist eine semantische Zusammenfassung mehrerer Screens.

Beispiele:

  • all — alle Screens im System
  • wall-all — alle 9 Infowand-Screens
  • wall-row-1 — die 3 Screens der ersten Reihe
  • wall-row-2 — die 3 Screens der zweiten Reihe
  • single-all — alle Einzelanzeigen (z.B. Vertretungsplan-Displays)
  • outdoor — alle Aussenanzeigetafeln

Typen von Gruppen

Physische Gruppen

Spiegeln die reale Anordnung wider:

  • wall-all — alle Displays einer Infowand
  • wall-row-1, wall-row-2, wall-row-3 — Reihen einer Wand
  • wall-column-1, wall-column-2, wall-column-3 — Spalten einer Wand

Funktionale Gruppen

Spiegeln den Verwendungszweck wider:

  • main-hall-all — alle Displays im Hauptkorridor
  • cafeteria-all — alle Displays in der Kaffeteria
  • info-all — alle Informationsanzeigen

Typen-Gruppen

Spiegeln das Geraetemodell wider:

  • portrait-all — alle Displays im Hochformat
  • landscape-all — alle Displays im Querformat
  • 4k-displays — nur 4K-Monitore

Tenant-Gruppen (Phase 2)

Spiegeln die Mandanten-Zugehoerigkeit wider:

  • tenant-xyz-all — alle Displays fuer Mandant XYZ
  • tenant-xyz-public — nur oeffentliche Displays des Mandants

Hierarchische Struktur

Gruppen koennen verschachtelt sein:

all
  ├── wall-all
  │   ├── wall-row-1
  │   │   ├── info01
  │   │   ├── info02
  │   │   └── info03
  │   ├── wall-row-2
  │   │   ├── info04
  │   │   ├── info05
  │   │   └── info06
  │   └── wall-row-3
  │       ├── info07
  │       ├── info08
  │       └── info09
  ├── single-all
  │   ├── info10 (Vertretungsplan 1)
  │   └── info11 (Vertretungsplan 2)
  └── fallback-displays
      └── [none currently]

Automatische Inferenz:

Ein Screen kann in mehreren Gruppen sein:

info01:
  - all
  - wall-all
  - wall-row-1
  - portrait-all
  - online-displays (automatisch basierend auf Status)

2. Slot-Modell

Konzept

Slots beschreiben feste Positionen innerhalb eines Layouts.

Sie werden hauptsaechlich fuer message_wall-Templates verwendet, um Ausschnitte von Grossmotiven auf einzelne Screens zu verteilen.

Beispiel: 3x3 Infowand

┌─────────────────────────────────┐
│  [0,0]    [0,1]    [0,2]        │  Slot wall-r1-c1, wall-r1-c2, wall-r1-c3
├─────────────────────────────────┤
│  [1,0]    [1,1]    [1,2]        │  Slot wall-r2-c1, wall-r2-c2, wall-r2-c3
├─────────────────────────────────┤
│  [2,0]    [2,1]    [2,2]        │  Slot wall-r3-c1, wall-r3-c2, wall-r3-c3
└─────────────────────────────────┘

Slot-Nomenclatur:

  • wall-r{reihe}-c{spalte} (Zeile/Spalte im 0er-System oder 1er-System)
  • wall-slot-{nummer} (durchnummeriert, z.B. wall-slot-0 bis wall-slot-8)

Geometrische Definition

Fuer jeden Slot wird definiert:

{
  "slot_id": "wall-r1-c1",
  "row": 0,
  "col": 0,
  "layout_name": "3x3_grid",
  "crop_x": 0,
  "crop_y": 0,
  "crop_width": 640,
  "crop_height": 1080,
  "assigned_screen_id": "info01"
}

Diese Werte sind:

  • serverseitig generiert — Admin muss nicht manuell Pixel-Koordinaten eingeben
  • automatisch skalierbar — bei verschiedenen Aufloesungen

3. Datenmodell

Tabelle screen_groups

CREATE TABLE screen_groups (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  slug TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  description TEXT,
  group_type TEXT NOT NULL CHECK (group_type IN (
    'physical', 'functional', 'device_type', 'tenant', 'custom'
  )),
  parent_group_id UUID REFERENCES screen_groups(id),
  active BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Beispiele:

INSERT INTO screen_groups (slug, name, group_type)
VALUES
  ('all', 'Alle Screens', 'custom'),
  ('wall-all', 'Infowand - Alle', 'physical'),
  ('wall-row-1', 'Infowand - Reihe 1', 'physical'),
  ('single-all', 'Einzelanzeigen', 'functional'),
  ('portrait-all', 'Hochformat', 'device_type');

Tabelle screen_group_members

CREATE TABLE screen_group_members (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  group_id UUID NOT NULL REFERENCES screen_groups(id) ON DELETE CASCADE,
  screen_id UUID NOT NULL REFERENCES screens(id) ON DELETE CASCADE,
  added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(group_id, screen_id)
);

Beispiel:

INSERT INTO screen_group_members (group_id, screen_id)
SELECT
  (SELECT id FROM screen_groups WHERE slug = 'wall-row-1'),
  id
FROM screens
WHERE slug IN ('info01', 'info02', 'info03');

Tabelle layout_definitions

CREATE TABLE layout_definitions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  slug TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  layout_type TEXT NOT NULL CHECK (layout_type IN (
    '3x3_grid', '2x2_grid', '1x9_row', '9x1_column', 'custom'
  )),
  rows INT NOT NULL,
  cols INT NOT NULL,
  description TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Beispiel:

INSERT INTO layout_definitions (slug, name, layout_type, rows, cols)
VALUES ('3x3_infowand', 'Infowand 3x3', '3x3_grid', 3, 3);

Tabelle layout_slots

CREATE TABLE layout_slots (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  layout_id UUID NOT NULL REFERENCES layout_definitions(id) ON DELETE CASCADE,
  slot_slug TEXT NOT NULL,
  row INT NOT NULL,
  col INT NOT NULL,
  UNIQUE(layout_id, slot_slug)
);

Beispiel:

INSERT INTO layout_slots (layout_id, slot_slug, row, col)
SELECT
  (SELECT id FROM layout_definitions WHERE slug = '3x3_infowand'),
  'wall-r' || (r) || '-c' || (c),
  r - 1, c - 1
FROM
  CROSS JOIN LATERAL (SELECT GENERATE_SERIES(1, 3) AS r)
  CROSS JOIN LATERAL (SELECT GENERATE_SERIES(1, 3) AS c);

Tabelle slot_screen_assignments

CREATE TABLE slot_screen_assignments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  layout_id UUID NOT NULL REFERENCES layout_definitions(id),
  slot_id UUID NOT NULL REFERENCES layout_slots(id) ON DELETE CASCADE,
  screen_id UUID NOT NULL REFERENCES screens(id),
  assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(layout_id, slot_id, screen_id)
);

Beispiel:

-- Zuordnung: Slot wall-r1-c1 → Screen info01 (in 3x3-Layout)
INSERT INTO slot_screen_assignments (layout_id, slot_id, screen_id)
SELECT
  l.id,
  ls.id,
  s.id
FROM
  layout_definitions l,
  layout_slots ls,
  screens s
WHERE
  l.slug = '3x3_infowand'
  AND ls.layout_id = l.id
  AND ls.slot_slug = 'wall-r1-c1'
  AND s.slug = 'info01';

4. Admin-Verwaltung

Gruppen verwalten

Seite: Admin → Gruppen

┌──────────────────────────────────────────┐
│ Screen-Gruppen                           │
├──────────────────────────────────────────┤
│                                          │
│ Gruppe                    Typ      Screens│
│────────────────────────────────────────│
│ all                    custom          13 │
│ wall-all               physical         9 │
│ wall-row-1             physical         3 │
│ wall-row-2             physical         3 │
│ wall-row-3             physical         3 │
│ single-all             functional       2 │
│ portrait-all           device_type     12 │
│                                          │
│ [+ Neue Gruppe]  [Gruppe bearbeiten]   │
└──────────────────────────────────────────┘

Gruppe erstellen/bearbeiten

┌──────────────────────────────────────────┐
│ Neue Gruppe                              │
├──────────────────────────────────────────┤
│                                          │
│ Name *                                   │
│ [ Infowand Reihe 2 __________________ ] │
│ slug: wall-row-2 (automatisch)          │
│                                          │
│ Gruppentyp *                             │
│ ⦿ physical      (Wand-Anordnung)        │
│ ○ functional    (Verwendungszweck)      │
│ ○ device_type   (Geraetetyp)            │
│ ○ tenant        (Mandant)               │
│ ○ custom        (benutzerdefiniert)     │
│                                          │
│ Beschreibung                             │
│ [ Die obere Reihe der Infowand ______ ] │
│                                          │
│ Screens hinzufuegen                      │
│ [ Suchfeld: "info" ]                    │
│ □ info01 ← obere Reihe                  │
│ □ info02 ← obere Reihe                  │
│ ☑ info03 ← obere Reihe                  │
│ □ info04                                │
│ ... (nur unzugeordnete zeigen)          │
│                                          │
│ Ausgewaehlte Screens                     │
│ info03 (portrait, online)                │
│ [ + weitere hinzufuegen ]               │
│                                          │
│ Uebergruppe                              │
│ [Dropdown: all > wall-all]              │
│ (optional, zur Hierarchie)              │
│                                          │
│ [Speichern]  [Abbrechen]                │
└──────────────────────────────────────────┘

Layout-Definition erstellen (fuer Message-Wall)

Seite: Admin → Layouts

┌──────────────────────────────────────────┐
│ Layout-Definitionen                      │
├──────────────────────────────────────────┤
│                                          │
│ Layout-Name       Typ        Grid  Slots│
│─────────────────────────────────────────│
│ 3x3 Infowand     3x3_grid    3x3    9  │
│ Vertretungsplan  2x2_grid    2x2    4  │
│ News-Lauf       1x9_row     1x9    9  │
│                                          │
│ [+ Neues Layout]  [Bearbeiten]         │
└──────────────────────────────────────────┘

Detailseite eines Layouts:

Layout: 3x3 Infowand

Visualisierung:
┌─────────┬─────────┬─────────┐
│ Slot 1  │ Slot 2  │ Slot 3  │
├─────────┼─────────┼─────────┤
│ Slot 4  │ Slot 5  │ Slot 6  │
├─────────┼─────────┼─────────┤
│ Slot 7  │ Slot 8  │ Slot 9  │
└─────────┴─────────┴─────────┘

Slot-Zuordnungen:
Slot 1 (wall-r1-c1) → Screen info01 (portrait, 1920x1080)
Slot 2 (wall-r1-c2) → Screen info02 (portrait, 1920x1080)
...

[Screen-Zuordnungen aendernx]  [Layout loeschen]

5. Anwendung in Kampagnen

Kampagne auf Gruppe anwenden

Beispiel: Admin aktiviert Weihnachtsmotiv auf wall-all:

Template: Weihnachtsmotiv 2025 (full_screen_media)

Zielgruppe auswaehlen:
⦿ Alle Screens
○ Nach Gruppe:
  [Dropdown: wall-all ]
  oder wall-row-1, single-all, ...
○ Einzelne Screens

→ Kampagne wird auf alle 9 Screens in wall-all aktiviert
→ Jeder Screen zeigt dasselbe Motiv
→ (Portrait/Landscape-Varianten werden serverseitig beruecksichtigt)

Message-Wall-Kampagne mit Slot-Modell

Beispiel: Admin teilt Schriftzug auf Infowand auf:

Template: Schriftzug (message_wall)

Layout: 3x3 Infowand
Zielgruppe: wall-all (auto-expandiert zu Slots)

Gesamte Grafik hochladen oder zeichnen
↓
System generiert automatisch:
  - Slot wall-r1-c1 → Ausschnitt x0-640 y0-1080 → Screen info01
  - Slot wall-r1-c2 → Ausschnitt 640-1280 y0-1080 → Screen info02
  - Slot wall-r1-c3 → Ausschnitt 1280-1920 y0-1080 → Screen info03
  - ... (9 Zuweisungen insgesamt)
↓
Kampagne aktivieren
↓
Jeder Screen ladet seinen zustaendigen Ausschnitt
↓
Schriftzug erscheint verteilt ueber alle 9 Screens

6. Automatische Gruppe-Inferenz

Der Server kann bestimmte Gruppen automatisch generieren:

# Automatisch generierte Gruppen

all:
  - alle Screens im System (manuelle Verwaltung nicht noetig)

online-all:
  - alle Screens, die gerade online sind
  - wird alle 5 Min aktualisiert

offline-all:
  - alle Screens, die gerade offline sind

portrait-all:
  - alle Screens mit Orientierung = "portrait"

landscape-all:
  - alle Screens mit Orientierung = "landscape"

device_type_*:
  - fuer jeden konfigurieren Screen-Typ (z.B. device_type_raspberry_pi)

region_*:
  - optional: auf Basis von Geo-Daten oder Tags

Diese automatischen Gruppen sind read-only im Admin-UI, aber voll verwendbar fuer Kampagnen.

7. Beispiel: Neuinstallation einer Infowand

Szenario: Admin installiert neue 3x3-Infowand mit Screens info01-info09.

Schritte:

  1. Screens anlegen (via Provisionierungs-UI oder direkt)

    info01, info02, ..., info09
    Alle: Orientierung portrait, Geraetetyp "raspberry_pi"
    
  2. Gruppen anlegen

    screen_groups:
      - slug: wall-all, name: "Infowand Alle", type: physical
      - slug: wall-row-1, name: "Infowand Reihe 1", type: physical
      - slug: wall-row-2, name: "Infowand Reihe 2", type: physical
      - slug: wall-row-3, name: "Infowand Reihe 3", type: physical
    
  3. Screens den Gruppen zuordnen

    wall-all: info01-info09
    wall-row-1: info01, info02, info03
    wall-row-2: info04, info05, info06
    wall-row-3: info07, info08, info09
    
  4. Layout definieren

    layout_definitions:
      - slug: 3x3_infowand, rows: 3, cols: 3
    
    layout_slots:
      - wall-r1-c1, wall-r1-c2, wall-r1-c3 (row 0)
      - wall-r2-c1, wall-r2-c2, wall-r2-c3 (row 1)
      - wall-r3-c1, wall-r3-c2, wall-r3-c3 (row 2)
    
    slot_screen_assignments:
      - wall-r1-c1 → info01
      - wall-r1-c2 → info02
      - ... (9 gesamt)
    
  5. Kampagnen verwenden

    Template: Schriftzug
    Zielgruppe: wall-all
    Layout: 3x3_infowand
    → Kampagne kann sofort aktiviert werden
    

8. Zusammenfassung

Das Gruppierungs- und Slot-Modell:

  • ist flexibel — physische, funktionale und typen-basierte Gruppen
  • ist hierarchisch — Gruppen koennen Untergruppen enthalten
  • ist automatisch — Gruppen wie "all" und "online-all" werden inferiert
  • ist geometrisch — Slots definieren Layouts fuer verteilte Motive
  • ist skalierbar — neue Screens werden einfach Gruppen zugeordnet
  • ist intuitiv — Admin-UI zeigt Zuordnungen und Vorschauen