morz-infoboard/server/backend/internal/db/migrations/001_initial.sql
Jesko Anschütz 803f355220 Baue Ebene 2: PostgreSQL-Backend, Medien-Upload und Playlist-UI
- DB-Package mit pgxpool, Migrations-Runner und eingebetteten SQL-Dateien
- Schema: tenants, screens, media_assets, playlists, playlist_items
- Store-Layer: alle Repositories (TenantStore, ScreenStore, MediaStore, PlaylistStore)
- JSON-API: Screens, Medien, Playlist-CRUD, Player-Sync-Endpunkt
- Admin-UI (/admin): Screens anlegen, löschen, zur Playlist navigieren
- Playlist-UI (/manage/{slug}): Drag&Drop-Sortierung, Item-Bearbeitung,
  Medienbibliothek, Datei-Upload (Bild/Video/PDF) und Web-URL
- Router auf RouterDeps umgestellt; manage-Routen nur wenn Stores vorhanden
- parseOptionalTime akzeptiert nun RFC3339 und datetime-local HTML-Format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 22:53:00 +01:00

76 lines
3.2 KiB
SQL

-- 001_initial.sql
-- Basis-Schema: Tenants, Screens, Medien, Playlists
create extension if not exists pgcrypto;
create table if not exists tenants (
id text primary key default gen_random_uuid()::text,
slug text not null unique,
name text not null,
created_at timestamptz not null default now()
);
create table if not exists screens (
id text primary key default gen_random_uuid()::text,
tenant_id text not null references tenants(id) on delete cascade,
slug text not null unique,
name text not null,
orientation text not null default 'landscape',
created_at timestamptz not null default now()
);
create table if not exists media_assets (
id text primary key default gen_random_uuid()::text,
tenant_id text not null references tenants(id) on delete cascade,
title text not null,
type text not null, -- image | video | pdf | web
storage_path text null, -- set for uploads
original_url text null, -- set for web/remote
mime_type text null,
size_bytes bigint null,
enabled boolean not null default true,
created_at timestamptz not null default now()
);
create table if not exists playlists (
id text primary key default gen_random_uuid()::text,
tenant_id text not null references tenants(id) on delete cascade,
screen_id text 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,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (screen_id) -- one active playlist per screen (simplified)
);
create table if not exists playlist_items (
id text primary key default gen_random_uuid()::text,
playlist_id text not null references playlists(id) on delete cascade,
media_asset_id text null references media_assets(id) on delete set null,
order_index integer not null default 0,
type text not null, -- image | video | pdf | web
src text not null, -- URL or served path
title text null,
duration_seconds integer not null default 20,
valid_from timestamptz null,
valid_until timestamptz null,
enabled boolean not null default true,
created_at timestamptz not null default now()
);
create index if not exists idx_screens_tenant_id on screens(tenant_id);
create index if not exists idx_media_assets_tenant_id on media_assets(tenant_id);
create index if not exists idx_playlists_screen_id on playlists(screen_id);
create index if not exists idx_playlist_items_order on playlist_items(playlist_id, order_index);
-- Schema-Versions-Tabelle
create table if not exists schema_migrations (
version integer primary key,
applied_at timestamptz not null default now()
);
-- Seed: Standard-Tenant und erster Screen (idempotent)
insert into tenants (id, slug, name)
values ('tenant-morz', 'morz', 'MORZ Schule')
on conflict (slug) do nothing;