# Info-Board Neu - Datenmodell

## Ziel

Das Datenmodell trennt klar zwischen:

- Mandanten und Benutzern
- Bildschirmen und ihrer technischen Laufzeitinformation
- Medien und Playlists
- Steuerbefehlen und Rueckmeldungen

Das Modell ist so ausgelegt, dass:

- jede Firma nur ihren eigenen Monitor bzw. Kanal pflegt
- die Administration alle Monitore zentral sehen und steuern kann
- Offline-Betrieb der Player moeglich bleibt
- Zeitsteuerung mit `valid_from` und `valid_until` sauber abbildbar ist

## Kernentitaeten

### `tenant`

Repraesentiert eine Firma bzw. einen abgeschotteten Inhaltsbereich.

Felder:

- `id`
- `slug`
- `name`
- `active`
- `created_at`
- `updated_at`

Hinweise:

- ein Tenant kann spaeter mehrere Screens haben, auch wenn zunaechst meist 1:1 gearbeitet wird
- alle Medien und Playlists sind einem Tenant zugeordnet

### `user`

Repraesentiert einen Benutzer der Firmen- oder Admin-Oberflaeche.

Felder:

- `id`
- `tenant_id` nullable fuer globale Admins
- `username`
- `email`
- `password_hash`
- `role` (`admin`, `tenant_user`)
- `active`
- `last_login_at`
- `created_at`
- `updated_at`

Regeln:

- `tenant_user` darf nur Daten des eigenen Tenants sehen und bearbeiten
- `admin` darf alle Tenants und alle Screens verwalten

### `screen`

Repraesentiert einen physischen Monitor bzw. einen Player-Client.

Felder:

- `id`
- `tenant_id`
- `slug`
- `name`
- `description`
- `location`
- `hardware_name`
- `enabled`
- `rotation` (`0`, `90`, `180`, `270`)
- `resolution_width`
- `resolution_height`
- `fallback_dir`
- `snapshot_interval_seconds`
- `offline_overlay_enabled`
- `created_at`
- `updated_at`

Hinweise:

- der `slug` dient als technische Kennung fuer Konfiguration, API und MQTT
- `fallback_dir` beschreibt die lokale oder synchronisierte Fallback-Quelle auf dem Client

### `screen_registration`

Optionale Trennung zwischen fachlichem Screen und technischer Registrierung.

Felder:

- `id`
- `screen_id`
- `device_uuid`
- `hostname`
- `api_token_hash`
- `mqtt_client_id`
- `last_seen_at`
- `last_ip`
- `player_version`
- `os_version`
- `created_at`
- `updated_at`

Zweck:

- Wiedererkennung eines konkreten Geraets
- sichere Kommunikation mit API und Broker
- technische Inventarisierung

### `provisioning_job`

Repraesentiert einen Provisionierungslauf fuer einen neuen oder neu aufzubauenden Screen.

Felder:

- `id`
- `screen_id`
- `requested_by_user_id`
- `target_ip`
- `target_port`
- `remote_user`
- `auth_mode` (`password`, `key`)
- `provided_secret_ref`
- `ssh_key_fingerprint` nullable
- `status` (`queued`, `running`, `succeeded`, `failed`, `cancelled`)
- `stage` (`connect`, `bootstrap`, `key_install`, `package_install`, `deploy`, `verify`, `done`)
- `log_excerpt` nullable
- `error_message` nullable
- `started_at` nullable
- `finished_at` nullable
- `created_at`
- `updated_at`

Regeln:

- das eigentliche Passwort wird nicht im Klartext in der Datenbank gespeichert
- `provided_secret_ref` verweist auf einen kurzlebigen oder extern geschuetzten Secret-Eintrag
- pro Job soll klar erkennbar sein, in welchem Schritt ein Fehler aufgetreten ist

### `screen_group`

Optionale logische Gruppierung von Screens.

Felder:

- `id`
- `slug`
- `name`
- `description`
- `created_at`
- `updated_at`

### `screen_group_member`

Zuordnung eines Screens zu einer Gruppe.

Felder:

- `id`
- `screen_group_id`
- `screen_id`
- `created_at`

Zweck:

- praktische Zielauswahl fuer Kampagnen und Provisionierungsaktionen

### `media_asset`

Repraesentiert ein verwaltetes Medium.

Felder:

- `id`
- `tenant_id`
- `screen_id` nullable
- `title`
- `description`
- `type` (`image`, `video`, `pdf`, `web`)
- `source_kind` (`upload`, `remote_url`)
- `storage_path` nullable bei reinen Web-URLs
- `original_url` nullable
- `mime_type`
- `checksum`
- `size_bytes`
- `enabled`
- `created_by_user_id`
- `created_at`
- `updated_at`

Regeln:

- `upload` bedeutet: Datei liegt im Medien-Storage
- `remote_url` bedeutet: Quelle ist extern und wird vom Player oder Server gecacht
- `screen_id` kann optional gesetzt werden, wenn ein Medium nur fuer einen Monitor gedacht ist

### `playlist`

Repraesentiert eine logische Playlist eines Screens.

Felder:

- `id`
- `tenant_id`
- `screen_id`
- `name`
- `is_active`
- `default_duration_seconds`
- `fallback_enabled`
- `fallback_dir`
- `shuffle_enabled`
- `created_at`
- `updated_at`

Regeln:

- pro Screen gibt es in v1 genau eine aktive Playlist
- spaeter koennen mehrere Playlists mit Umschaltung moeglich werden

### `display_template`

Repraesentiert ein globales Admin-Template zur monitoruebergreifenden Orchestrierung.

Beispiele:

- Schriftzug ueber mehrere Monitore
- saisonales Motiv auf allen Monitoren
- Veranstaltungslayout fuer einen befristeten Zeitraum

Felder:

- `id`
- `slug`
- `name`
- `description`
- `template_type` (`message_wall`, `full_screen_media`, `screen_specific_scene`)
- `enabled`
- `created_by_user_id`
- `created_at`
- `updated_at`

### `template_scene`

Repraesentiert die konkrete Auspraegung eines Templates fuer einen oder mehrere Screens.

Felder:

- `id`
- `display_template_id`
- `screen_id` nullable
- `screen_slot` nullable
- `type` (`image`, `video`, `pdf`, `web`, `html`)
- `src`
- `duration_seconds`
- `load_timeout_seconds`
- `cache_policy`
- `on_error`
- `layout_json`
- `created_at`
- `updated_at`

Hinweise:

- `screen_id` erlaubt die direkte Zuweisung zu einem konkreten Monitor
- `screen_slot` erlaubt spaeter abstrakte Wand-Slots wie `row1-col2`
- `layout_json` kann z. B. Textpositionen, Farben oder Teilbereiche des Gesamtmotivs beschreiben

### `campaign`

Repraesentiert eine aktivierbare globale Kampagne auf Basis eines Templates.

Felder:

- `id`
- `display_template_id`
- `name`
- `description`
- `priority`
- `active`
- `valid_from` nullable
- `valid_until` nullable
- `override_mode` (`replace_tenant_content`, `replace_all`, `merge_reserved_slots`)
- `created_by_user_id`
- `created_at`
- `updated_at`

Regeln:

- Kampagnen ueberschreiben in v1 standardmaessig den Firmencontent
- nach Ende oder Deaktivierung greift automatisch wieder der tenantbezogene Normalbetrieb

### `template_assignment`

Repraesentiert die Zuordnung einer Kampagne oder eines Templates zu einem oder mehreren Screens.

Felder:

- `id`
- `campaign_id`
- `screen_id`
- `enabled`
- `created_at`
- `updated_at`

Zweck:

- Auswahl einzelner Monitore, Gruppen oder aller Monitore

### `playlist_item`

Repraesentiert einen einzelnen Eintrag in einer Playlist.

Felder:

- `id`
- `playlist_id`
- `screen_id`
- `order_index`
- `media_asset_id` nullable
- `type` (`image`, `video`, `pdf`, `web`, `dir`)
- `src`
- `title`
- `duration_seconds`
- `load_timeout_seconds`
- `cache_policy` (`required`, `prefer_cache`, `no_cache`)
- `on_error` (`skip`, `retry`, `fallback`)
- `retry_count`
- `valid_from` nullable
- `valid_until` nullable
- `enabled`
- `created_at`
- `updated_at`

Regeln:

- `media_asset_id` wird bei verwalteten Medien gesetzt
- `src` enthaelt den effektiven Pfad oder die URL, damit der Player auch ohne Join-Struktur arbeiten kann
- `type=dir` erlaubt definierte Verzeichnisreferenzen innerhalb der Playlist

### `playlist_item_dir_rule`

Optionale Zusatzregel fuer `type=dir`.

Felder:

- `id`
- `playlist_item_id`
- `directory_path`
- `sort_mode` (`name_asc`, `name_desc`, `mtime_asc`, `mtime_desc`, `shuffle`)
- `per_item_duration_seconds`
- `recursive`
- `file_filter` nullable

Zweck:

- sauberere Modellierung fuer Verzeichnisbasierte Fallbacks oder Einschuebe

### `screen_status`

Repraesentiert den letzten bekannten Laufzeitstatus eines Players.

Felder:

- `screen_id`
- `online`
- `server_connected`
- `mqtt_connected`
- `last_heartbeat_at`
- `last_sync_at`
- `current_playlist_id`
- `current_playlist_item_id` nullable
- `current_content_source` (`campaign`, `tenant_playlist`, `fallback`)
- `current_campaign_id` nullable
- `current_item_type` nullable
- `current_item_label` nullable
- `current_item_started_at` nullable
- `current_item_duration_seconds` nullable
- `cache_state` (`ok`, `stale`, `missing`, `error`)
- `overlay_state` (`online`, `degraded`, `offline`)
- `error_code` nullable
- `error_message` nullable
- `player_version`
- `uptime_seconds`
- `free_disk_bytes` nullable
- `temperature_celsius` nullable
- `updated_at`

Hinweise:

- `screen_status` ist eine verdichtete Sicht fuer Dashboard und Ueberwachung
- historische Verlaeufe koennen spaeter separat gespeichert werden

### `screen_snapshot`

Repraesentiert eine Vorschauaufnahme des aktuellen Bildschirminhalts.

Felder:

- `id`
- `screen_id`
- `captured_at`
- `storage_path`
- `width`
- `height`
- `mime_type`
- `source` (`scheduled`, `item_change`, `manual`)

Regeln:

- in der UI wird typischerweise nur der letzte Snapshot angezeigt
- aeltere Snapshots koennen nach Zeit oder Anzahl aufgeraeumt werden

### `device_command`

Repraesentiert einen an einen Screen gesendeten Befehl.

Felder:

- `id`
- `screen_id`
- `command_type` (`reload`, `restart_player`, `reboot`, `display_on`, `display_off`, `refresh_snapshot`, `clear_cache`)
- `payload_json`
- `requested_by_user_id`
- `requested_at`
- `delivery_state` (`queued`, `sent`, `acknowledged`, `failed`, `expired`)
- `delivered_at` nullable
- `acknowledged_at` nullable
- `result_code` nullable
- `result_message` nullable

Zweck:

- nachvollziehbare Fernsteuerung mit Audit-Trail

### `sync_state`

Repraesentiert den Synchronisationsstand eines Screens.

Felder:

- `screen_id`
- `config_revision`
- `playlist_revision`
- `media_revision`
- `last_successful_sync_at`
- `last_failed_sync_at` nullable
- `last_error_message` nullable

Zweck:

- Erkennung, ob ein Player auf dem aktuellen Stand ist

## Beziehungen

- ein `tenant` hat viele `users`
- ein `tenant` hat viele `screens`
- ein `tenant` hat viele `media_assets`
- ein `screen` hat genau eine aktive `playlist` in v1
- eine `playlist` hat viele `playlist_items`
- ein `display_template` hat viele `template_scenes`
- ein `display_template` hat viele `campaigns`
- eine `campaign` hat viele `template_assignments`
- ein `screen` hat viele `provisioning_jobs`
- eine `screen_group` hat viele `screen_group_members`
- ein `screen` hat genau einen aktuellen `screen_status`
- ein `screen` hat viele `screen_snapshots`
- ein `screen` hat viele `device_commands`
- ein `screen` hat genau einen aktuellen `sync_state`

## Zugriffsregeln

### Tenant-User

Ein `tenant_user` darf:

- nur den eigenen `tenant` sehen
- nur den eigenen `screen` bzw. die dem Tenant zugeordneten Screens sehen
- nur eigene Medien hochladen und pflegen
- nur eigene Playlists bearbeiten
- nur die Vorschau des eigenen Screens sehen

Ein `tenant_user` darf nicht:

- andere Tenants sehen
- globale Steuerkommandos senden
- andere Screens administrieren

### Admin

Ein `admin` darf:

- alle Tenants, Screens, Medien und Playlists sehen
- globale Steuerbefehle senden
- Vorschau und Status aller Screens sehen
- neue Screens und Benutzer anlegen

## Revisions- und Caching-Modell

Damit Offline-Betrieb einfach bleibt, arbeitet der Player nicht gegen beliebige Einzelobjekte, sondern gegen Revisionen.

Sinnvoll sind mindestens:

- `playlist_revision`
- `media_revision`
- `config_revision`
- `campaign_revision`

Der Player kann damit erkennen:

- ob neue Konfiguration vorliegt
- ob Medien nachgeladen werden muessen
- ob die lokale Kopie noch gueltig ist
- ob sich globale Uebersteuerungen geaendert haben

## Prioritaetsmodell der Inhaltsauswahl

Der Player wertet Inhalte in dieser Reihenfolge aus:

1. aktive Kampagne mit gueltigem Zeitfenster und Assignment fuer den Screen
2. aktive tenantbezogene Playlist des Screens
3. Fallback-Verzeichnis

Eine Kampagne ist aktiv, wenn:

- `active = true`
- `valid_from` leer oder erreicht ist
- `valid_until` leer oder noch nicht abgelaufen ist
- eine `template_assignment` fuer den Screen aktiv ist

Dadurch ist die globale Uebersteuerung ein Kernbestandteil und kein nachtraeglicher Sonderfall.

## Beispiel fuer aktive Playlist-Auswertung

Ein `playlist_item` ist aktiv, wenn:

- `enabled = true`
- `valid_from` leer oder in der Vergangenheit liegt
- `valid_until` leer oder in der Zukunft liegt

Wenn kein aktives Item existiert:

- greift die globale Fallback-Regel des Screens oder der Playlist

## Beispiel fuer Admin-Steuerung

Wenn ein Admin `reload` ausloest:

1. Ein `device_command` wird gespeichert
2. der Befehl wird per MQTT an den Screen signalisiert
3. der Player fuehrt den Reload aus
4. der Player sendet ein ACK
5. `device_command.delivery_state` wird aktualisiert

## Offene spaetere Erweiterungen

- mehrere Screens pro Tenant in der Firmenoberflaeche
- Vorlagen und globale Playlists
- Historie der Statusdaten als Zeitreihe
- Geplante Kampagnen mit Prioritaeten
- serverseitige Konvertierung oder Transkodierung von Medien
