# Info-Board Neu - Schema-Entwurf ## Ziel Dieses Dokument bringt das fachliche Datenmodell in eine konkrete, relationale Form. Es ist noch kein finales Migrationsskript, aber bereits so strukturiert, dass daraus spaeter direkt PostgreSQL-Tabellen und Migrationen abgeleitet werden koennen. ## Grundannahmen - Ziel-Datenbank: PostgreSQL - IDs als UUID oder technisch gleichwertige stabile Primärschluessel - Zeitstempel in UTC - Status- und Typwerte zunaechst als textuelle Enum-Werte oder PostgreSQL-Enums ## Konventionen - Primärschluessel: `id` - Fremdschluessel: `_id` - Zeitstempel: `created_at`, `updated_at` - nullable Felder nur dort, wo fachlich wirklich noetig ## Zentrale Tabellen ### `tenants` Zweck: - abgeschottete Firmen- oder Inhaltsbereiche Spalten: ```sql id uuid primary key slug text not null unique name text not null active boolean not null default true created_at timestamptz not null updated_at timestamptz not null ``` ### `users` Zweck: - Admin- und Tenant-Benutzer Spalten: ```sql id text primary key default gen_random_uuid()::text tenant_id text not null references tenants(id) on delete cascade username text not null password_hash text not null role text not null default 'tenant' created_at timestamptz not null default now() unique(tenant_id, username) ``` Regeln: - `role` in v1: `admin`, `screen_user`, `tenant` - `username` ist nur innerhalb eines Tenants eindeutig (Unique-Constraint auf `(tenant_id, username)`) - `tenant_id` ist `NOT NULL` — jeder User gehoert genau einem Tenant - IDs sind `text`, nicht `uuid`, enthalten aber UUID-Werte (via `gen_random_uuid()::text`) - Felder wie `email`, `active`, `last_login_at` und `updated_at` existieren in v1 nicht ### `user_screen_permissions` Zweck: - Zuordnung von Screen-Usern zu Screens (rollenbasierter Zugriff) Spalten: ```sql id uuid primary key user_id text not null references users(id) on delete cascade screen_id text not null references screens(id) on delete cascade created_at timestamptz not null default now() unique(user_id, screen_id) ``` Regeln: - `user_id` muss ein User mit `role = 'screen_user'` sein - `screen_id` muss existieren; Loeschen des Screens loescht auch die Permission - Loeschen des Users loescht auch alle seine Permissions ### `sessions` Zweck: - Sitzungstokens fuer den Browser-Login Spalten: ```sql id text primary key default gen_random_uuid()::text user_id text not null references users(id) on delete cascade created_at timestamptz not null default now() expires_at timestamptz not null default (now() + interval '8 hours') ``` Indizes: ```sql create index idx_sessions_user_id on sessions(user_id); create index idx_sessions_expires_at on sessions(expires_at); ``` Regeln: - Session-TTL beim Anlegen betraegt standardmaessig 8 Stunden (Migration-Default); `AuthStore.CreateSession` uebergibt die tatsaechliche TTL als Parameter (aktuell 24 Stunden) - Abgelaufene Sessions werden stuendlich per Hintergrund-Ticker bereinigt (`CleanExpiredSessions`) - Cookie-Name: `morz_session`; `HttpOnly=true`, `Secure=true` (ausser `MORZ_INFOBOARD_DEV_MODE=true`) ### `screen_groups` Zweck: - logische Gruppen wie `wall-all` oder `vertretungsplan-all` Spalten: ```sql id uuid primary key slug text not null unique name text not null description text null created_at timestamptz not null updated_at timestamptz not null ``` ### `screens` Zweck: - physische Displays bzw. Player-Geraete **Hinweis:** Die Spalten unten beschreiben das geplante Schema (Phase 2-3). Der aktuelle Code (Migration `001_core.sql`) implementiert nur eine Teilmenge: `id`, `tenant_id`, `slug`, `name`, `orientation`, `created_at` (6 Spalten). Weitere Felder werden in späteren Phasen hinzugefügt. Spalten: ```sql id uuid primary key tenant_id uuid null references tenants(id) on delete set null slug text not null unique name text not null description text null location text null hardware_name text null screen_class text not null enabled boolean not null default true orientation text not null rotation integer not null default 0 resolution_width integer null resolution_height integer null fallback_dir text not null snapshot_interval_seconds integer not null default 60 offline_overlay_enabled boolean not null default true created_at timestamptz not null updated_at timestamptz not null ``` Regeln: - `orientation` in v1: `portrait`, `landscape` - `rotation` in v1: `0`, `90`, `180`, `270` - `screen_class` in v1 z. B. `info_wall_display`, `single_info_display`, `vertretungsplan_display` ### `screen_group_members` Zweck: - Zuordnung von Screens zu Gruppen Spalten: ```sql id uuid primary key screen_group_id uuid not null references screen_groups(id) on delete cascade screen_id uuid not null references screens(id) on delete cascade created_at timestamptz not null ``` Unique: ```sql unique (screen_group_id, screen_id) ``` ### `screen_registrations` Zweck: - technische Registrierung und Wiedererkennung eines Geraets Spalten: ```sql id uuid primary key screen_id uuid not null unique references screens(id) on delete cascade device_uuid text not null unique hostname text null api_token_hash text not null mqtt_client_id text not null unique last_seen_at timestamptz null last_ip inet null player_version text null os_version text null created_at timestamptz not null updated_at timestamptz not null ``` ### `provisioning_jobs` Zweck: - technische Erstinstallation und Re-Provisionierung von Screens Spalten: ```sql id uuid primary key screen_id uuid not null references screens(id) on delete cascade requested_by_user_id uuid not null references users(id) on delete restrict target_ip inet not null target_port integer not null default 22 remote_user text not null default 'root' auth_mode text not null provided_secret_ref text null ssh_key_fingerprint text null status text not null stage text not null log_excerpt text null error_message text null started_at timestamptz null finished_at timestamptz null created_at timestamptz not null updated_at timestamptz not null ``` Regeln: - Passwort nicht direkt in dieser Tabelle speichern - `status` in v1: `queued`, `running`, `succeeded`, `failed`, `cancelled` ### `media_assets` Zweck: - verwaltete Medien und Web-Referenzen Spalten: ```sql id uuid primary key tenant_id uuid not null references tenants(id) on delete cascade screen_id uuid null references screens(id) on delete set null title text not null description text null type text not null source_kind text not null storage_path text null original_url text null mime_type text null checksum text null size_bytes bigint null enabled boolean not null default true created_by_user_id uuid not null references users(id) on delete restrict created_at timestamptz not null updated_at timestamptz not null ``` Regeln: - `type` in v1: `image`, `video`, `pdf`, `web` - `source_kind` in v1: `upload`, `remote_url` ### `playlists` Zweck: - tenantbezogene Hauptplaylist eines Screens Spalten: ```sql id uuid primary key tenant_id uuid not null references tenants(id) on delete cascade screen_id uuid not null references screens(id) on delete cascade name text not null is_active boolean not null default true default_duration_seconds integer not null default 20 fallback_enabled boolean not null default true fallback_dir text not null shuffle_enabled boolean not null default false created_at timestamptz not null updated_at timestamptz not null ``` Unique: ```sql unique (screen_id, is_active) where is_active = true ``` ### `playlist_items` Zweck: - Elemente einer tenantbezogenen Playlist Spalten: ```sql id uuid primary key playlist_id uuid not null references playlists(id) on delete cascade media_asset_id uuid null references media_assets(id) on delete set null order_index integer not null type text not null src text not null title text null duration_seconds integer not null load_timeout_seconds integer not null default 15 cache_policy text not null default 'prefer_cache' on_error text not null default 'skip' retry_count integer not null default 0 valid_from timestamptz null valid_until timestamptz null enabled boolean not null default true created_at timestamptz not null updated_at timestamptz not null ``` ### `playlist_item_dir_rules` Zweck: - Zusatzregeln fuer `dir`-Items Spalten: ```sql id uuid primary key playlist_item_id uuid not null unique references playlist_items(id) on delete cascade directory_path text not null sort_mode text not null default 'name_asc' per_item_duration_seconds integer not null default 20 recursive boolean not null default false file_filter text null ``` ## Kampagnen- und Template-Tabellen ### `display_templates` Zweck: - globale Templates fuer adminseitige Uebersteuerungen Spalten: ```sql id uuid primary key slug text not null unique name text not null description text null template_type text not null enabled boolean not null default true created_by_user_id uuid not null references users(id) on delete restrict created_at timestamptz not null updated_at timestamptz not null ``` ### `template_scenes` Zweck: - konkrete Szenen eines Templates fuer Screens, Gruppen oder Slots Spalten: ```sql id uuid primary key display_template_id uuid not null references display_templates(id) on delete cascade screen_id uuid null references screens(id) on delete cascade screen_group_id uuid null references screen_groups(id) on delete cascade screen_slot text null orientation text null type text not null src text not null duration_seconds integer not null default 20 load_timeout_seconds integer not null default 15 cache_policy text not null default 'prefer_cache' on_error text not null default 'skip' layout_json jsonb not null default '{}'::jsonb created_at timestamptz not null updated_at timestamptz not null ``` Hinweis: - `orientation` erlaubt z. B. getrennte Portrait-/Landscape-Szenen innerhalb einer Kampagne ### `campaigns` Zweck: - aktivierbare Instanzen globaler Templates Spalten: ```sql id uuid primary key display_template_id uuid not null references display_templates(id) on delete cascade name text not null description text null priority integer not null default 100 active boolean not null default false valid_from timestamptz null valid_until timestamptz null override_mode text not null default 'replace_tenant_content' created_by_user_id uuid not null references users(id) on delete restrict created_at timestamptz not null updated_at timestamptz not null ``` ### `template_assignments` Zweck: - explizite Zielzuordnung einer Kampagne - Gruppen werden in v1 serverseitig in konkrete Screen-Zuordnungen expandiert Spalten: ```sql id uuid primary key campaign_id uuid not null references campaigns(id) on delete cascade screen_id uuid not null references screens(id) on delete cascade enabled boolean not null default true created_at timestamptz not null updated_at timestamptz not null ``` Unique: ```sql unique (campaign_id, screen_id) ``` ## Laufzeit- und Betriebsdaten ### `screen_status` Zweck: - aktueller verdichteter Laufzeitstatus pro Screen Spalten: ```sql screen_id uuid primary key references screens(id) on delete cascade online boolean not null default false server_connected boolean not null default false mqtt_connected boolean not null default false last_heartbeat_at timestamptz null last_sync_at timestamptz null current_playlist_id uuid null references playlists(id) on delete set null current_playlist_item_id uuid null references playlist_items(id) on delete set null current_content_source text null current_campaign_id uuid null references campaigns(id) on delete set null current_item_type text null current_item_label text null current_item_started_at timestamptz null current_item_duration_seconds integer null cache_state text not null default 'ok' overlay_state text not null default 'online' error_code text null error_message text null player_version text null uptime_seconds bigint null free_disk_bytes bigint null temperature_celsius numeric(5,2) null updated_at timestamptz not null ``` ### `screen_snapshots` Zweck: - Screenshots fuer Vorschau und Diagnose Spalten: ```sql id uuid primary key screen_id uuid not null references screens(id) on delete cascade captured_at timestamptz not null storage_path text not null width integer null height integer null mime_type text not null source text not null ``` ### `device_commands` Zweck: - nachvollziehbare Fernsteuerbefehle an Screens Spalten: ```sql id uuid primary key screen_id uuid not null references screens(id) on delete cascade command_type text not null payload_json jsonb not null default '{}'::jsonb requested_by_user_id uuid not null references users(id) on delete restrict requested_at timestamptz not null delivery_state text not null delivered_at timestamptz null acknowledged_at timestamptz null result_code text null result_message text null ``` ### `sync_state` Zweck: - letzter bekannter Synchronisationsstand pro Screen Spalten: ```sql screen_id uuid primary key references screens(id) on delete cascade config_revision bigint not null default 0 playlist_revision bigint not null default 0 media_revision bigint not null default 0 campaign_revision bigint not null default 0 last_successful_sync_at timestamptz null last_failed_sync_at timestamptz null last_error_message text null ``` ## Auth-Datenbankschema Die Auth-Tabellen werden durch `server/backend/internal/db/migrations/002_auth.sql` angelegt und sind vollstaendig unter den Abschnitten `users` und `sessions` oben beschrieben. Die Screen-Usserverwaltung wird durch `server/backend/internal/db/migrations/003_user_screen_permissions.sql` angelegt und ist unter dem Abschnitt `user_screen_permissions` oben beschrieben. Der `AuthStore` (`internal/store/auth.go`) stellt folgende Methoden bereit: - `GetUserByUsername(ctx, username)` — Nutzer per Username laden (inkl. `TenantSlug` via LEFT JOIN) - `CreateSession(ctx, userID, ttl)` — neue Session anlegen - `GetSessionUser(ctx, sessionID)` — User zu gueltigem Session-Token laden - `DeleteSession(ctx, sessionID)` — Session loeschen (Logout) - `CleanExpiredSessions(ctx)` — abgelaufene Sessions bereinigen - `EnsureAdminUser(ctx, tenantSlug, password)` — Admin-User beim Start anlegen wenn nicht vorhanden - `VerifyPassword(ctx, userID, password)` — Passwort gegen bcrypt-Hash pruefen - `CreateScreenUser(ctx, tenantID, username, password)` — neuen Screen-User anlegen - `ListScreenUsers(ctx, tenantID)` — alle Screen-User eines Tenants auflisten - `DeleteUser(ctx, userID)` — User und alle zugeordneten Permissions loeschen Der `ScreenStore` (`internal/store/screen.go`) stellt folgende Methoden bereit: - `GetAccessibleScreens(ctx, userID)` — alle Screens, auf die der User Zugriff hat - `HasUserScreenAccess(ctx, userID, screenID)` — prueft ob User auf Screen zugreifen darf - `AddUserToScreen(ctx, userID, screenID)` — User zu Screen hinzufuegen - `RemoveUserFromScreen(ctx, userID, screenID)` — User von Screen entfernen - `GetScreenUsers(ctx, screenID)` — alle User, die auf Screen Zugriff haben ## Wichtige Indizes Empfohlen mindestens: ```sql create index idx_screens_tenant_id on screens(tenant_id); create index idx_media_assets_tenant_id on media_assets(tenant_id); create index idx_playlists_screen_id on playlists(screen_id); create index idx_playlist_items_playlist_id_order on playlist_items(playlist_id, order_index); create index idx_campaigns_active_validity on campaigns(active, valid_from, valid_until); create index idx_template_assignments_screen_id on template_assignments(screen_id); create index idx_provisioning_jobs_screen_id on provisioning_jobs(screen_id); create index idx_provisioning_jobs_status on provisioning_jobs(status); create index idx_screen_snapshots_screen_id_captured_at on screen_snapshots(screen_id, captured_at desc); create index idx_device_commands_screen_id_requested_at on device_commands(screen_id, requested_at desc); ``` ## Prioritaetslogik in relationaler Form Ein Screen zeigt in dieser Reihenfolge: 1. aktive Kampagne mit passender Assignment-Zuordnung 2. aktive tenantbezogene Playlist-Eintraege 3. Fallback-Verzeichnis Eine Kampagne ist aktiv, wenn: - `campaigns.active = true` - `valid_from is null or valid_from <= now()` - `valid_until is null or valid_until > now()` - `template_assignments.enabled = true` Ein Playlist-Item ist aktiv, wenn: - `enabled = true` - `valid_from is null or valid_from <= now()` - `valid_until is null or valid_until > now()` ## Offene spaetere Erweiterungen - Historientabellen fuer Heartbeats und Status als Zeitreihe - Versionierung von Playlists und Templates - Medienkonvertierung oder serverseitige Derivate - mehrere aktive Kampagnen mit Kollisionsregeln - Slot-Topologien fuer unterschiedlich grosse Wandsysteme ## Verbindliche Architekturentscheidungen fuer v1 - `playlist_items` enthalten keinen direkten `screen_id`-Fremdschluessel - Kampagnengruppen werden serverseitig in `template_assignments` auf konkrete Screens expandiert - `message_wall` wird nicht im Player segmentiert, sondern serverseitig aufbereitet