- SCHEMA.md: screen_id in user_screen_permissions von uuid auf text korrigiert - SCHEMA.md: Phasen-Hinweis zur screens-Tabelle hinzugefügt (6 Spalten aktuell implementiert, erweiterte Phase 2-3) - SERVER-KONZEPT.md: RequireScreenAccess-Middleware dokumentiert inkl. Route-Gruppen und Verhaltens-Details - server/backend/README.md: Env-Variable DATABASE_URL → MORZ_INFOBOARD_DATABASE_URL korrigiert - DEVELOPMENT.md: Compose-Stack von "später" auf "existiert bereits" aktualisiert - API-ENDPOINTS.md: HandlePlayerPlaylist Response um fehlende Felder ergänzt (playlist_id, media_asset_id, order_index, created_at) - DEVELOPMENT.md: Architekturentscheidungen präzisiert (message_wall=implementiert, Kampagnen=geplant) Co-Authored-By: Klaus <noreply@example.com>
630 lines
17 KiB
Markdown
630 lines
17 KiB
Markdown
# 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: `<bezug>_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
|