morz-infoboard/docs/SCHEMA.md
Jesko Anschütz db68c84d45 docs: API-ENDPOINTS + SCHEMA für Override und Wochenend-Sperre
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:25:50 +01:00

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:

  • 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:

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:

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.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:

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:

  • 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:

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
  • status in 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:

  • type in v1: image, video, pdf, web
  • source_kind in 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:

  • orientation erlaubt 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 = false bedeutet: 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)
  • type bestimmt den globalen Zielzustand (alle Screens)
  • until gibt 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 — vereinfachte screen_status-Tabelle (display_state + reported_at)
  • 005_screen_schedules.sqlscreen_schedules-Tabelle fuer Zeitplaene

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:

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