From ae183d399e9fe0b8673ec4836d1edf5b3ac52fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Sun, 22 Mar 2026 13:11:51 +0100 Subject: [PATCH] Forme das Datenmodell als Schemaentwurf aus --- README.md | 1 + docs/SCHEMA.md | 541 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 docs/SCHEMA.md diff --git a/README.md b/README.md index 9054e42..3359d24 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Die Trennung von `/srv/docker/infoboard-netboot` ist sinnvoll, damit: - Architekturplan: `PLAN.md` - Umsetzungs-Todo: `TODO.md` - Datenmodell: `DATENMODELL.md` +- Schema-Entwurf: `docs/SCHEMA.md` - API- und MQTT-Vertrag: `API-MQTT-VERTRAG.md` - Technologieentscheidungen: `TECH-STACK.md` - Template-/Kampagnenkonzept: `docs/TEMPLATE-KONZEPT.md` diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md new file mode 100644 index 0000000..cacf46e --- /dev/null +++ b/docs/SCHEMA.md @@ -0,0 +1,541 @@ +# 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 uuid primary key +tenant_id uuid null references tenants(id) on delete set null +username text not null unique +email text not null unique +password_hash text not null +role text not null +active boolean not null default true +last_login_at timestamptz null +created_at timestamptz not null +updated_at timestamptz not null +``` + +Regeln: + +- `role` in v1: `admin`, `tenant_user` + +### `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 + +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 +screen_id uuid not null references screens(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 + +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 +``` + +## 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