# 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`, `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

### `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

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.

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

## 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
