- SCHEMA.md: Index idx_media_assets_created_by_user_id hinzugefügt - API-ENDPOINTS.md: GET/POST Media-Endpoints erweitert um owner_is_restricted und owner_username - API-ENDPOINTS.md: DELETE Media-Endpoint mit Berechtigungslogik dokumentiert * admin_user: immer erlaubt * screen_user: erlaubt wenn asset.tenant_id == user.tenant_id * restricted: erlaubt nur wenn asset.created_by_user_id == user.id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
19 KiB
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:
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:
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:
rolein v1:admin,screen_user,tenantusernameist nur innerhalb eines Tenants eindeutig (Unique-Constraint auf(tenant_id, username))tenant_idistNOT NULL— jeder User gehoert genau einem Tenant- IDs sind
text, nichtuuid, enthalten aber UUID-Werte (viagen_random_uuid()::text) - Felder wie
email,active,last_login_atundupdated_atexistieren in v1 nicht
user_screen_permissions
Zweck:
- Zuordnung von Screen-Usern zu Screens (rollenbasierter Zugriff)
Spalten:
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_idmuss ein User mitrole = 'screen_user'seinscreen_idmuss existieren; Loeschen des Screens loescht auch die Permission- Loeschen des Users loescht auch alle seine Permissions
sessions
Zweck:
- Sitzungstokens fuer den Browser-Login
Spalten:
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:
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.CreateSessionuebergibt 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(ausserMORZ_INFOBOARD_DEV_MODE=true)
screen_groups
Zweck:
- logische Gruppen wie
wall-allodervertretungsplan-all
Spalten:
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:
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:
orientationin v1:portrait,landscaperotationin v1:0,90,180,270screen_classin v1 z. B.info_wall_display,single_info_display,vertretungsplan_display
screen_group_members
Zweck:
- Zuordnung von Screens zu Gruppen
Spalten:
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:
unique (screen_group_id, screen_id)
screen_registrations
Zweck:
- technische Registrierung und Wiedererkennung eines Geraets
Spalten:
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:
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
statusin v1:queued,running,succeeded,failed,cancelled
media_assets
Zweck:
- verwaltete Medien und Web-Referenzen
Spalten:
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:
typein v1:image,video,pdf,websource_kindin v1:upload,remote_url
playlists
Zweck:
- tenantbezogene Hauptplaylist eines Screens
Spalten:
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:
unique (screen_id, is_active) where is_active = true
playlist_items
Zweck:
- Elemente einer tenantbezogenen Playlist
Spalten:
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:
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:
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:
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:
orientationerlaubt z. B. getrennte Portrait-/Landscape-Szenen innerhalb einer Kampagne
campaigns
Zweck:
- aktivierbare Instanzen globaler Templates
Spalten:
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:
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:
unique (campaign_id, screen_id)
Laufzeit- und Betriebsdaten
screen_status
Zweck:
- aktueller verdichteter Laufzeitstatus pro Screen
Spalten:
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
Hinweis: Die Spalten oben beschreiben das geplante Langzeitschema. Migration 004_screen_status.sql
implementiert eine vereinfachte Version dieser Tabelle mit nur drei Spalten:
screen_id TEXT PRIMARY KEY REFERENCES screens(id) ON DELETE CASCADE
display_state TEXT NOT NULL DEFAULT 'unknown' -- "on", "off", "unknown"
reported_at TIMESTAMPTZ NOT NULL DEFAULT now()
Diese kompakte Tabelle speichert ausschliesslich den zuletzt vom Agent gemeldeten Display-Zustand. Weitere Laufzeitfelder (online, heartbeat, etc.) werden in einem spaeteren Migrations-Schritt ergaenzt.
screen_schedules
Zweck:
- taeglich konfigurierbarer Ein-/Ausschalt-Zeitplan pro Screen
Migration: 005_screen_schedules.sql
Spalten:
screen_id TEXT PRIMARY KEY REFERENCES screens(id) ON DELETE CASCADE
schedule_enabled BOOLEAN NOT NULL DEFAULT false
power_on_time TEXT NOT NULL DEFAULT '' -- HH:MM (24h), leer = nicht gesetzt
power_off_time TEXT NOT NULL DEFAULT '' -- HH:MM (24h), leer = nicht gesetzt
Regeln:
- Wird durch den
scheduler-Package ausgewertet (prueft jede Minute alle aktiven Zeitplaene) schedule_enabled = falsebedeutet: Zeitplan vorhanden, aber deaktiviert- Leere Zeitfelder bedeuten: kein Einschalt- bzw. kein Ausschaltbefehl
Neue Spalte in screen_schedules (Migration 006):
override_on_until timestamptz— Einschalten-Override: Monitor bleibt bis zu diesem Zeitpunkt eingeschaltet (null = kein Override)
global_override (Migration 006)
Zweck:
- Speichert den globalen Display-Override (maximal eine Zeile)
Spalten:
id INT PRIMARY KEY DEFAULT 1
type TEXT NOT NULL -- "on" oder "off"
until TIMESTAMPTZ NOT NULL -- Override aktiv bis zu diesem Zeitpunkt
set_at TIMESTAMPTZ NOT NULL DEFAULT now() -- Wann der Override gesetzt wurde
Constraint:
CHECK (id = 1)
Regeln:
- Die Tabelle enthaelt maximal eine Zeile (id = 1)
typebestimmt den globalen Zielzustand (alle Screens)untilgibt an, wann der Override automatisch aufgehoben wird- Der Scheduler prueft jede Minute, ob der Override noch aktiv ist (aktuell <= until)
screen_snapshots
Zweck:
- Screenshots fuer Vorschau und Diagnose
Spalten:
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:
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:
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.
Die Display-Steuerung wird durch zwei weitere Migrationen angelegt:
004_screen_status.sql— vereinfachtescreen_status-Tabelle (display_state + reported_at)005_screen_schedules.sql—screen_schedules-Tabelle fuer Zeitplaene
Der AuthStore (internal/store/auth.go) stellt folgende Methoden bereit:
GetUserByUsername(ctx, username)— Nutzer per Username laden (inkl.TenantSlugvia LEFT JOIN)CreateSession(ctx, userID, ttl)— neue Session anlegenGetSessionUser(ctx, sessionID)— User zu gueltigem Session-Token ladenDeleteSession(ctx, sessionID)— Session loeschen (Logout)CleanExpiredSessions(ctx)— abgelaufene Sessions bereinigenEnsureAdminUser(ctx, tenantSlug, password)— Admin-User beim Start anlegen wenn nicht vorhandenVerifyPassword(ctx, userID, password)— Passwort gegen bcrypt-Hash pruefenCreateScreenUser(ctx, tenantID, username, password)— neuen Screen-User anlegenListScreenUsers(ctx, tenantID)— alle Screen-User eines Tenants auflistenDeleteUser(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 hatHasUserScreenAccess(ctx, userID, screenID)— prueft ob User auf Screen zugreifen darfAddUserToScreen(ctx, userID, screenID)— User zu Screen hinzufuegenRemoveUserFromScreen(ctx, userID, screenID)— User von Screen entfernenGetScreenUsers(ctx, screenID)— alle User, die auf Screen Zugriff haben
Wichtige Indizes
Empfohlen mindestens:
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_media_assets_created_by_user_id on media_assets(created_by_user_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:
- aktive Kampagne mit passender Assignment-Zuordnung
- aktive tenantbezogene Playlist-Eintraege
- Fallback-Verzeichnis
Eine Kampagne ist aktiv, wenn:
campaigns.active = truevalid_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 = truevalid_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_itemsenthalten keinen direktenscreen_id-Fremdschluessel- Kampagnengruppen werden serverseitig in
template_assignmentsauf konkrete Screens expandiert message_wallwird nicht im Player segmentiert, sondern serverseitig aufbereitet