admin_resolve_report weist rejected mit Ziel ab (report_reject_has_target), Check game_reports_rejected_no_target, Tests und Docs angepasst. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
103 KiB
Spec #2 – KAT-05 Katalogdaten laden und aktuell halten
| Issue | #2 (KAT-05) |
| Status | freigegeben (Chris, 2026-11-03); Spec-F1–Spec-F9 entschieden wie empfohlen (§ 9.2); Review und Re-Review PR #51 eingearbeitet (§ 10); Designwahlen in § 9.3 einzeln bestätigt (Chris, 2026-11-03, Issue #2) |
| Autor | architect |
| Stand | 2026-11-03 |
| Etappe | R1.1 (Teil 2a: Datenmodell, Rechte, Ausblenden, Startbestand), R1.2 (Teil 2b: Zusammenführen; Teil 2c: Meldungen bearbeiten); Import (Krit. 1, 3, 6, Quellenteil von 2) erst mit Lizenz |
| Abhängigkeiten | keine (Fundament). Von #2 hängen ab: #3 KAT-01, #13 KAT-04, #25 SAM-08; #4 KAT-02 über #3. #2 wird erst geschlossen, wenn der Startbestand auf Produktion abgenommen ist (C1) und KAT-02 (#4) das Platzhalter-Cover und den Kurztext zeigt (Krit. 10). |
| Quellen | requirements/20-r1-akzeptanzkriterien.md § KAT-05 und § Querschnitt, requirements/r1-klaerungen.md (3, 4, 5, 6, 7, 10, 24, 25/44, 26, 27, 39, 48, N11, N12, N12a, N15), requirements/backlog.md (Abschnitt „Datenmodell-Änderungen für architect“), docs/datenmodell-r1.md, supabase/migrations/20261102100000_base.sql, …100200_catalog.sql, …100300_collection.sql, …100400_plays.sql, supabase/tests/database/00_schema.test.sql, …/02_catalog.test.sql, supabase/seed.sql, supabase/config.toml, ADR-0001, ADR-0002 |
Namenskonvention in dieser Spec: „Spec-F1“ bis „Spec-F9“ sind die Fragen an Chris aus § 9.2. „F2“, „O6“ usw. ohne Präfix sind Entscheidungen aus den Anforderungen (20-r1-akzeptanzkriterien.md). „SB1“ bis „SB8“ sind die Regeln für den Startbestand (4.3). „RB1“, „W3“ usw. sind Punkte aus dem Review zu PR #51; „B1“ bis „B10“ sind Aufgaben (§ 8).
1. Ziel
Der Katalog ist das Fundament aller anderen Stories: Suche (KAT-01), Detailseite (KAT-02), Sammlung, Partien und Meldungen verweisen auf games. KAT-05 sorgt in R1 dafür, dass
- das Datenmodell alles enthält, was Admin-Pflege und ein späterer Import brauchen (ausgeblendet, zusammengeführt, manuell gepflegte Felder, Quelle mit Anzeige-Erlaubnis; Klärung 7), ohne dass der Import später eine Migration braucht,
- nur der Admin den Katalog und Meldungen ändern kann, und zwar in R1 ausschließlich über Supabase Studio/SQL (Klärungen 25/44),
- zum Start von R1 ein manuell gepflegter Startbestand von etwa 200 Spielen (mindestens 180, Spec-F8) vorliegt, der nur Fakten enthält, ohne fremde Cover und fremde Beschreibungstexte (Klärung 24, N11),
- der Ablauf der Admin-Pflege im Repo dokumentiert ist (Krit. 8).
Ein Import-Job (BGG oder eine andere Quelle) ist nicht Teil dieser Spec. Es gibt keine Edge Function und keine Admin-Oberfläche in der App.
Kernentscheidungen (Kurzfassung)
| # | Entscheidung | Begründung |
|---|---|---|
| E1 | Ein Issue #2, drei PRs: 2a (R1.1) Datenmodell, Rechte, Ausblenden, Startbestand-Werkzeug, Platzhalter-Cover; 2b (R1.2) Zusammenführen; 2c (R1.2) Meldungstabelle und Bearbeiten von Meldungen. Alle drei PRs referenzieren nur Refs #2, keiner schließt das Issue. Chris schließt #2 nach der Go-live-Abnahme des Startbestands (C1) und nach KAT-02 (#4). |
Entspricht dem Etappenschnitt im Issue. Krit. 7 und 10 lassen sich erst mit den echten Daten bzw. der Detailseite abnehmen. 2a blockiert KAT-01 (#3), 2c muss vor der Umsetzung von KAT-04 (#13) gemergt sein. |
| E2 | Größe neu bewertet: L statt XL. 2a ≈ 2–3 Tage (davon der Großteil Migration und pgTAP), 2b ≈ 1–2 Tage, 2c ≈ 1 Tag. Das Einpflegen der ~200 Spiele durch Chris ist nicht eingerechnet. | Import, Edge Function und Admin-UI entfallen (Klärungen 24, 44). Das Label size/xl ändert nur Chris. |
| E3 | Ausblenden über games.hidden_at, Zusammenführen über games.merged_into_id. Die Lese-Policy von games bleibt unverändert. „Nicht mehr auffindbar“ (Krit. 4) wird über die neue View catalog_games umgesetzt, die ausgeblendete und zusammengeführte Einträge nicht enthält. Suche und Entdecken (KAT-01) lesen nur diese View. |
Würde RLS ausgeblendete Spiele verbergen, liefen bestehende Sammlungs-, Favoriten- und Partie-Zeilen ins Leere. Entschieden (Spec-F2): Ausgeblendete Einträge bleiben per ID lesbar; ein Status „gesperrt“ kommt erst bei Bedarf. |
| E4 | Manuell gepflegte Felder als Liste games.manual_fields text[], gepflegt von einem Trigger: Jedes Feld eines Katalogspiels, das ein Mensch (Studio/SQL) setzt oder ändert, landet automatisch in der Liste. Ausgenommen ist nur ein späterer Import, und nur wenn beides gilt: Er läuft als eigene Datenbankrolle catalog_importer und setzt in seiner Transaktion set local shuffle.catalog_import = 'on'. Zuordnungen zählen als Pseudo-Felder (genres, mechanics, categories, publishers). |
Der Admin muss in Studio nichts zusätzlich pflegen, die Kennzeichnung kann nicht vergessen werden (Krit. 4). Die Bindung an eine eigene Rolle verhindert, dass ein versehentliches set im Studio die Kennzeichnung abschaltet (W7, bestätigt). Entschieden (Spec-F4): Im Startbestand gelten alle gesetzten Felder als manuell gepflegt. |
| E5 | Quellen als eigene Tabelle catalog_sources (Schlüssel, Name, Quellenangabe, Anzeige-Erlaubnis für Bilder und Texte je Quelle) und je Spiel catalog_source_id, source_ref (Kennung in der Quelle), source_url, source_synced_at. Die Migration legt die Quelle bgg mit images_allowed = false, texts_allowed = false an. |
Klärung 7 und Krit. 2, 6: Felder und Erlaubnis je Quelle ab R1 vorhanden. games.bgg_id bleibt unverändert der Abgleichsschlüssel für SAM-08 (auch bei privaten Spielen); source_* beschreibt, woher die Katalogdaten stammen. Die Doppelung bei BGG ist bewusst; die Konsistenz stellt der Import sicher. Der Fremdschlüssel heißt nach Konvention <Zieltabelle>_id. |
| E6 | Anzeige-Erlaubnis je Quelle (Krit. 6): Daten ohne Erlaubnis werden gar nicht gespeichert. Die View catalog_games blendet description und image_path zusätzlich aus, wenn die Quelle die Anzeige nicht erlaubt (zweite Sicherung). Bestätigt (Chris, 2026-11-03, W1). |
Die View allein schützt nicht: games ist für anon tabellenweise lesbar, die Rohspalten wären über die API abrufbar. Spaltenweises select für anon würde auch eigene private Spiele und bestehende Abfragen treffen und wäre invasiv. Deshalb wird die Pflicht „nicht speichern“ der Import-Spec mitgegeben (9.5); im Startbestand gibt es ohnehin keine Quelle. |
| E7 | Verlage (Klärung 4) entstehen in dieser Spec (publishers, game_publishers), nicht erst in KAT-01. Alternative Titel (Klärung 3, #3) und Basisspiel-Verweis (Klärung 5, #12) bleiben in ihren Stories; das Pflegeformat des Startbestands hat dafür aber schon Spalten (4.3), damit Chris die ~200 Spiele nicht zweimal anfassen muss. |
Krit. 10 verlangt den Verlag im Startbestand. |
| E8 | Sprache des Kurztexts als Spalte games.description_locale (de/en, nur zusammen mit description), schon in 2a. |
Der Startbestand füllt sie (immer de, Spec-F5); KAT-02 Krit. 7 (Klärung 48) zeigt damit den Sprachhinweis. |
| E9 | Startbestand ist keine Migration und nicht Teil von seed.sql, sondern eine erzeugte, idempotente SQL-Datei supabase/catalog/startbestand.sql mit festen IDs. Lokal und in CI lädt sie supabase db reset über [db.seed] sql_paths; auf Staging und Produktion spielt Chris sie nach den Migrationen per Studio-SQL-Editor oder psql ein. Entschieden (Spec-F6): Chris pflegt eine CSV, ein Skript prüft sie und erzeugt die SQL-Datei; nach dem Go-live ist die Datei eingefroren. |
Migrationen sind unveränderlich und nur additiv, Katalogdaten werden bis zum Start aber laufend korrigiert. seed.sql ist ausdrücklich nur für die lokale Entwicklung. |
| E10 | In R1 gibt es kein App-Konto mit Admin-Rolle. Chris pflegt ausschließlich über Studio bzw. psql als Datenbankrolle postgres bzw. supabase_admin (umgeht RLS). Die bestehenden is_admin()-Policies (JWT app_metadata.role = 'admin') bleiben für QUE-08 (R4) stehen. Die neuen Admin-Funktionen prüfen nur die Datenbankrolle (kein is_admin()-Pfad) und sind für anon, authenticated und service_role nicht ausführbar (H1/H2, bestätigt). |
Klärung 44: Admin-Rolle nur als Datenbankrolle. Kein Admin-JWT und kein Funktionsaufruf über die API heißt weniger Angriffsfläche. QUE-08 (R4) ergänzt bei Bedarf einen eigenen Pfad. |
| E11 | Umhängen, Zusammenführen und Meldungsergebnis laufen über SQL-Funktionen, nicht über Hand-SQL: admin_merge_games(source, target) (zwei Katalogspiele) und admin_resolve_report(report, status, target) (vorläufiges privates Spiel → Katalogspiel), beide nutzen die interne Funktion repoint_game_references(from, to). Ob Varianten kopiert werden, ergibt sich aus der Sichtbarkeit: nur wenn Quelle und Ziel Katalogspiele sind. Beim Umhängen eines privaten Eintrags wird nie kopiert (Entscheidung Chris, RB1); die Edition wird dort auf eine gleichnamige Variante des Ziels abgebildet, sonst geleert (Klärung N15). |
Zusammenführen ist kein einzelnes UPDATE: Varianten mit zusammengesetzten Fremdschlüsseln, unique (owner_id, game_id) in collection_items, unique (profile_id, game_id) in favorite_games und künftig ein Wertungsbogen je Spiel (Klärung 39). Private Varianten eines Nutzers dürfen nicht in den öffentlichen Katalog wandern. |
| E12 | Die Meldungstabelle game_reports entsteht in 2c (diese Spec), vollständig mit den Spalten, die KAT-04 und SAM-08 brauchen (Herkunft Nutzer/Import, BGG-Kennung, vorläufiger Eintrag). Anlegen durch Nutzer (Insert-Policy, Tageslimit 10, Ähnlichkeitsprüfung) liefert #13. |
#13 hängt in Forgejo von #2 ab. Läge die Tabelle in #13, wäre 2c von #13 blockiert. Die Spalten ergeben sich aus den Klärungen 6, 26, 27, N12 und N12a. |
| E13 | Keine neuen Pakete, keine Edge Function, keine Supabase-Cloud-Funktionen. Alles ist reines PostgreSQL 17 (Trigger, Views mit security_invoker, Spaltenrechte, Custom-GUC), PostgREST und Studio, die es im Self-Hosting gibt (ADR-0002). [db.seed] sql_paths ist eine Funktion der lokalen CLI. |
Vorgabe aus CLAUDE.md und ADR-0002. |
| E14 | Sichtbarkeit und Eigentümer eines Spiels sind unveränderlich (Entscheidung Chris, W-a): Ein Trigger lehnt jede Änderung von visibility oder owner_id an einer bestehenden games-Zeile ab, für alle Rollen einschließlich postgres. Ein privater Eintrag wird nie „öffentlich geschaltet“; freigegebene Meldungen bekommen immer einen neuen Katalogeintrag. |
Sonst könnten private Angaben (Beschreibung, Bild, Varianten) eines Nutzers durch einen Handgriff im Studio öffentlich werden. Die Policies verhindern das für Nutzer bereits; der Trigger schützt auch vor Admin-Versehen. |
2. Akzeptanzkriterien
Übernommen aus Issue #2 / 20-r1-akzeptanzkriterien.md § KAT-05. Referenz in Tests und PRs: „KAT-05 Kriterium n“. „F2“ in Krit. 2 ist die Anforderungs-Entscheidung „eigener Katalog mit eigener ID“, nicht Spec-F2.
- Zurückgestellt bis zur Lizenz (Klärung 24): Gegeben eine zugelassene Quelle, wenn der Import läuft, dann werden neue Spiele angelegt und geänderte aktualisiert, ohne manuell gepflegte Felder zu überschreiben.
- Gegeben ein Katalogeintrag, dann hat er eine eigene Shuffle-ID (F2). Zurückgestellt bis zur Lizenz (Klärung 24): optional die Kennung der Quelle (zum Beispiel BGG) samt Quellenangabe; die Felder dafür sind ab R1 im Datenmodell vorhanden (Klärung 7).
- Zurückgestellt bis zur Lizenz (Klärung 24): Gegeben die Quelle ist nicht erreichbar oder liefert fehlerhafte Daten, dann bleibt der Katalog unverändert, der Fehler wird protokolliert und die App funktioniert normal weiter.
- Gegeben der Admin bearbeitet, blendet aus oder führt Einträge zusammen (in R1 über Supabase Studio/SQL), dann sind ausgeblendete Einträge in der App nicht mehr auffindbar, und manuell gepflegte Felder sind als solche gekennzeichnet, damit ein späterer Import sie nicht überschreibt (Klärungen 7, 44).
- Gegeben Meldungen aus KAT-04, dann kann der Admin sie über Supabase Studio/SQL freigeben, mit einem Eintrag zusammenführen oder ablehnen; Status (offen, freigegeben, abgelehnt, zusammengeführt) und gegebenenfalls der Zieleintrag werden gespeichert (Klärungen 6, 44).
- Zurückgestellt bis zur Lizenz (Klärung 24): Gegeben Cover und Texte einer Quelle, dann werden sie nur angezeigt, wenn deren Nutzungsbedingungen es erlauben; die Erlaubnis ist je Quelle vermerkt. Das Feld für die Erlaubnis je Quelle ist ab R1 im Datenmodell vorhanden (Klärung 7).
- Gegeben R1 geht live, dann enthält der Katalog einen manuell gepflegten Startbestand von etwa 200 Spielen, den der Admin (Chris) per Supabase Studio bzw. Seed einpflegt (Klärung 24, N11).
- Gegeben die Admin-Pflege über Supabase Studio/SQL, dann ist der Ablauf für Bearbeiten, Ausblenden, Zusammenführen und das Bearbeiten von Meldungen im Repo dokumentiert (Klärung 44).
- Gegeben ein Nutzer ohne Admin-Rolle, wenn er über die App oder direkt über die Schnittstelle Katalogeinträge oder fremde Meldungen ändern will, dann wird der Zugriff verweigert; die Admin-Rolle existiert in R1 nur als Datenbankrolle (Klärung 44).
- Gegeben ein Spiel aus dem Startbestand, dann enthält es nur Fakten (Titel, Verlag, Erscheinungsjahr, Spielerzahl, Spieldauer, Mindestalter, Genre, Mechanik); fremde Cover und fremde Beschreibungstexte werden nicht übernommen, stattdessen zeigt die App ein Platzhalter-Cover und einen eigenen Kurztext (N11).
Abnahme von Krit. 7 und 10 (konkretisiert durch Spec-F5, Spec-F7, Spec-F8)
- Krit. 7 gilt als erfüllt, wenn die Prüfabfrage
supabase/catalog/checks.sqlauf Produktion für die Startbestandsspiele mindestens 180 sichtbare Katalogspiele meldet und 0 Verstöße gegen SB2, SB4 und SB6 zeigt. Chris nimmt das Ergebnis vor dem Go-live ab (C1). - Krit. 10 gilt als erfüllt, wenn jedes Startbestandsspiel alle Pflicht-Fakten hat (SB2), einen eigenen Kurztext auf Deutsch (SB4), kein Cover, keine Quelle und keine BGG-Kennung (SB6), die Fakten nach SB3 erhoben wurden (Bestätigung durch Chris) und die App für diese Spiele das Platzhalter-Cover und den Kurztext zeigt (KAT-01/KAT-02).
Abweichung bei KAT-04 Krit. 4 (Klärung N15): Beim Ersetzen eines vorläufigen Eintrags bleibt die Edition nur erhalten, wenn der Katalogeintrag eine gleichnamige Variante hat (3.3, R10).
Zuordnung zu den PRs (alle PRs Refs #2, siehe E1)
| PR | Etappe | Kriterien | Bei zurückgestellten Kriterien: nur Datenmodell |
|---|---|---|---|
| 2a | R1.1 | 2; 4 (Bearbeiten mit Kennzeichnung, Ausblenden, catalog_games); 7 (Lieferweg, Werkzeug, Prüfabfrage; der Datenbestand selbst kommt von Chris); 8 (Runbook: Zugang, Startbestand, Bearbeiten, Ausblenden); 9 (Katalog); 10 (Daten- und Widget-Anteil) |
1 (Kennzeichnung manual_fields), 2 (Quellenteil), 6 (Erlaubnis je Quelle) |
| 2b | R1.2 | 4 (Zusammenführen); 8 (Runbook: Zusammenführen); 9 (Admin-Funktionen) | – |
| 2c | R1.2 | 5; 8 (Runbook: Meldungen); 9 (fremde Meldungen, unveränderliche Sichtbarkeit) | – |
| außerhalb | – | 7 (Abnahme auf Produktion, C1); 10 (Anzeige in KAT-01/KAT-02, #3/#4) | – |
Krit. 3 hat keinen Anteil am Datenmodell und wird vollständig mit dem Import spezifiziert.
Relevante Querschnittskriterien
- Sicherheit der Daten Kriterium 2: Katalog ohne Anmeldung lesbar (
catalog_games,catalog_sources,publishersfüranon), nie durch Nutzer veränderbar (O6). - Sicherheit der Daten Kriterium 1 und 3:
game_reportsenthält Nutzerdaten; nur der Meldende liest seine Meldungen, pgTAP prüft fremden Zugriff. Private Einträge können nicht öffentlich werden (E14). - Datenschutz (QUE-04): Meldungen werden beim Konto-Löschen hart gelöscht (
on delete cascade, Klärung 10). Der Startbestand enthält keine personenbezogenen Daten. Freigegebene Katalogeinträge übernehmen nur Fakten aus den Meldungsfeldern, nie Beschreibung oder Bild des vorläufigen Eintrags (4.4). - Sprachen (QUE-05) Kriterium 2: Die Semantics-Beschriftung des Platzhalter-Covers kommt aus ARB (de/en). Begriffe (Genre, Mechanik) haben
name_de/name_en. - Barrierefreiheit (QUE-06) Kriterien 1, 2: Platzhalter-Cover mit sinnvoller Beschriftung und Kontrast AA (Kontrast der Grafik zum Hintergrund mindestens 3:1).
- Plattformen Kriterium 1: Das Platzhalter-Cover ist ein reines Flutter-Widget ohne Plattformcode.
3. Datenmodell
3.1 Bestand gegen die Kriterien
| Bedarf | Bestand | Abgleich | Handlung |
|---|---|---|---|
| Eigene Shuffle-ID (Krit. 2) | games.id uuid primary key default gen_random_uuid(), set_updated_at schützt id |
passt | keine; pgTAP-Test als Nachweis |
| Kennung + Quellenangabe (Krit. 2, Quellenteil) | nur games.bgg_id |
Quellenangabe fehlt | catalog_sources, games.catalog_source_id + source_* (3.2) |
| Ausblenden (Krit. 4) | – | fehlt | games.hidden_at, View catalog_games (2a) |
| Zusammenführen (Krit. 4) | – | fehlt | games.merged_into_id (2a), Funktion admin_merge_games (2b) |
| Manuell gepflegte Felder (Krit. 1, 4) | – | fehlt | games.manual_fields, Trigger |
| Erlaubnis je Quelle (Krit. 6) | – | fehlt | catalog_sources.images_allowed, texts_allowed |
| Verlag (Krit. 10) | – | fehlt (Klärung 4) | publishers, game_publishers |
| Genre, Mechanik (Krit. 10) | genres, mechanics, Zuordnungstabellen |
passt | Vokabular für den Startbestand: Vorschlag durch Agents, Freigabe durch Chris (Spec-F8) |
| Kurztext + Sprache (Krit. 10, KAT-02 Krit. 7) | games.description (≤ 5000), keine Sprache |
Sprache fehlt | games.description_locale |
| Kein fremdes Cover (Krit. 10) | games.image_path nullable |
passt | Startbestand setzt kein image_path; Platzhalter in der App |
| Meldungen (Krit. 5) | – | fehlt (Klärung 6) | game_reports (2c) |
| Schreibrechte (Krit. 9) | Katalog nur is_admin(); Nutzer dürfen eigene private Spiele vollständig ändern (Tabellenrecht update für authenticated) |
Nutzer könnten an eigenen privaten Spielen hidden_at, manual_fields, source_*, merged_into_id setzen |
Spaltenrechte statt Tabellenrechte für insert/update auf games (3.2) |
| Sichtbarkeit unveränderlich (E14) | Policy verhindert für Nutzer das Heben in den Katalog; postgres könnte visibility ändern |
Lücke für Admin-Versehen | Trigger games_visibility_immutable (2c, 3.4) |
3.2 Migration 2a supabase/migrations/<ts>_catalog_foundation.sql (R1.1)
Nur additiv. Bestehende Dateien bleiben unverändert; die Änderung der Rechte auf games erfolgt über revoke/grant in der neuen Datei.
Rechte-Regel für alle neuen Objekte (2a–2c): Supabase vergibt für neue Tabellen, Views und Funktionen standardmäßig alle Rechte an anon, authenticated und service_role. Jede neue Tabelle und View bekommt deshalb zuerst revoke all … from anon, authenticated und danach nur die Grants aus der Tabelle unten (wie catalog.sql und supabase/README.md). Jede neue Funktion bekommt revoke execute … from public, anon, authenticated, service_role; service_role braucht in R1 keine dieser Funktionen (keine Edge Function), der Import ergänzt später gezielt Rechte für catalog_importer. Nach jedem create or replace function wird der revoke in derselben Migration ausdrücklich wiederholt.
Grants je Objekt (verbindlich)
| Objekt | anon |
authenticated |
Policies (Kurzform) |
|---|---|---|---|
catalog_sources |
select |
select, insert, update, delete |
select für alle using (true); Schreiben nur is_admin() (für R4, in R1 ungenutzt) |
publishers |
select |
select, insert, update, delete |
wie catalog_sources (Muster genres in catalog.sql) |
game_publishers |
select |
select, insert, update, delete |
Lesen: wer das Spiel sehen darf (exists (select 1 from games g where g.id = game_id)); Schreiben: is_admin() oder Ersteller des privaten Spiels (Muster game_genres_write, catalog.sql Z. 141–143 und 216–227) |
catalog_games (View) |
select |
select |
über security_invoker die Policies von games und catalog_sources |
games |
unverändert select |
select, delete unverändert; insert/update nur spaltenweise (unten) |
unverändert |
game_reports (2c) |
– | select |
eigene Zeilen; Admin-Policy für R4 |
| neue Funktionen | – | – | execute nur für den Eigentümer (postgres) |
Neue Tabelle catalog_sources
| Spalte | Typ | Hinweis |
|---|---|---|
id |
uuid | PK, gen_random_uuid() |
key |
text | not null, unique, ^[a-z0-9_]{2,30}$ (z. B. bgg) |
name |
text | not null, 1–100 Zeichen (z. B. „BoardGameGeek“) |
attribution_text |
text | nullable, ≤ 200 (z. B. „Powered by BGG“, KAT-02 Krit. 5) |
attribution_url |
text | nullable, ≤ 500, Check ^https:// |
images_allowed |
boolean | not null, default false (Krit. 6) |
texts_allowed |
boolean | not null, default false (Krit. 6) |
terms_url |
text | nullable, ≤ 500, Check ^https://; Nutzungsbedingungen der Quelle |
terms_checked_at |
date | nullable, wann die Erlaubnis zuletzt geprüft wurde |
created_at, updated_at |
timestamptz | Standard, Trigger set_updated_at |
Die Migration fügt eine Zeile ein: key = 'bgg', name = 'BoardGameGeek', attribution_text = 'Powered by BGG', attribution_url = 'https://boardgamegeek.com', beide Erlaubnisse false. Das ist Referenzdaten-Pflege, keine Katalogdaten.
Neue Spalten in games
| Spalte | Typ | Hinweis |
|---|---|---|
description_locale |
text | nullable, Check in ('de','en'); Check description_locale is null or description is not null (E8) |
hidden_at |
timestamptz | nullable; gesetzt = ausgeblendet (Krit. 4) |
merged_into_id |
uuid | nullable, FK → games (id) on delete restrict (ein Zieleintrag kann nicht gelöscht werden, solange etwas auf ihn verweist; Katalogspiele werden ausgeblendet, nicht gelöscht). Check merged_into_id <> id; Check merged_into_id is null or hidden_at is not null; Ziel muss ein Katalogspiel sein (Trigger games_merged_into_catalog, unten) |
manual_fields |
text[] | not null, default '{}'. Check manual_fields <@ array['title','description','description_locale','year','min_players','max_players','min_minutes','max_minutes','min_age','image_path','genres','mechanics','categories','publishers']. Neue Schlüssel (z. B. alternative Titel in #3, Basisspiel in #12) ergänzt die jeweilige Story per neuer Migration (Check ersetzen). |
catalog_source_id |
uuid | nullable, FK → catalog_sources (id) on delete restrict |
source_ref |
text | nullable, 1–100, Kennung des Spiels in der Quelle |
source_url |
text | nullable, ≤ 500, Check ^https:// |
source_synced_at |
timestamptz | nullable, letzter erfolgreicher Abgleich (Import) |
Weitere Checks: (catalog_source_id is null) = (source_ref is null); source_url und source_synced_at nur mit catalog_source_id.
Indizes: unique (catalog_source_id, source_ref) where visibility = 'catalog' and catalog_source_id is not null; games_merged_into_id_idx auf merged_into_id; games_catalog_listed_idx auf (lower(title)) where visibility = 'catalog' and hidden_at is null and merged_into_id is null (Suche baut KAT-01 darauf aus).
Trigger games_merged_into_catalog (H7): before insert or update of merged_into_id on public.games; ist merged_into_id gesetzt, muss das Ziel visibility = 'catalog' haben, sonst Fehler merged_into_not_catalog (P0001). Entschieden als Trigger statt nur als Runbook-Regel, weil Studio-Handgriffe sonst einen Verweis auf ein privates Spiel erzeugen könnten; dessen Löschen beim Konto-Löschen würde dann an on delete restrict scheitern (KTO-05). Ein Check-Constraint geht nicht, weil er keine andere Zeile lesen darf.
Neue Tabellen publishers und game_publishers (Klärung 4, E7)
| Tabelle | Spalte | Typ | Hinweis |
|---|---|---|---|
publishers |
name |
text | not null, 1–200; eindeutig über unique index publishers_name_key on (lower(name)) |
game_publishers |
game_id |
uuid | not null, FK → games on delete cascade |
game_publishers |
publisher_id |
uuid | not null, FK → publishers on delete cascade; unique (game_id, publisher_id); Index auf publisher_id |
Beide mit id, created_at, updated_at und Trigger set_updated_at. Ein Spiel kann mehrere Verlage haben (KAT-01 Krit. 3).
Trigger games_mark_manual_fields (E4)
before insert or update on public.games, nur wennnew.visibility = 'catalog'.- Ausnahme Import:
current_user = 'catalog_importer' and coalesce(current_setting('shuffle.catalog_import', true), '') = 'on'. Die Rolle gibt es in R1 nicht; der Vergleich ist ein reiner Namensvergleich und funktioniert auch ohne die Rolle. Sonst wird markiert:- bei INSERT: jedes Feld aus der Schlüsselliste, das nicht
nullist (Spec-F4: alle gesetzten Felder), - bei UPDATE: jedes Feld aus der Schlüsselliste, dessen Wert sich ändert (
is distinct from).
- bei INSERT: jedes Feld aus der Schlüsselliste, das nicht
- Eine Änderung nur an
manual_fieldsselbst wird nicht verändert, damit der Admin eine Markierung bewusst entfernen kann. - Sortiert und ohne Duplikate speichern (
array(select distinct unnest(...) order by 1)), damit Tests stabil vergleichen können.
Trigger game_links_mark_manual auf game_genres, game_mechanics, game_categories, game_publishers: after insert or update or delete (auch Update, z. B. wenn genre_id geändert wird). Ist das betroffene Spiel (bei Update altes und neues) ein Katalogspiel und greift die Import-Ausnahme nicht, ergänzt er genres / mechanics / categories / publishers in manual_fields. Funktion security invoker, search_path = ''.
Warum ein Nutzer die Kennzeichnung nicht umgehen kann: Er darf Katalogspiele und deren Zuordnungen nicht schreiben (Policies, Krit. 9); bei privaten Spielen markiert der Trigger nichts. Zusätzlich greift die Import-Ausnahme nur für die Rolle catalog_importer. Regel fürs Runbook: im Studio niemals set shuffle.… ohne local (sonst gilt die Einstellung für die ganze Sitzung).
View catalog_games (with (security_invoker = true), E3, E6)
- Zeilen:
gamesmitvisibility = 'catalog' and hidden_at is null and merged_into_id is null. - Spalten:
id,title,year,min_players,max_players,min_minutes,max_minutes,min_age,bgg_id,description_locale,created_at,updated_at, dazudescription=case when g.catalog_source_id is null or 'description' = any(g.manual_fields) or s.texts_allowed then g.description end,image_path= gleiche Regel mit'image_path'unds.images_allowed,source_key,source_name,source_attribution_text,source_attribution_url,source_urlauscatalog_sources(Left Join).
- Grants laut Tabelle oben. Weil
security_invokergesetzt ist, gelten die RLS-Regeln vongamesundcatalog_sourcesfür den Aufrufer. - Grenze (E6): Die View ist für Krit. 6 nur die zweite Sicherung.
gamesbleibt füranontabellenweise lesbar; Daten ohne Anzeige-Erlaubnis dürfen deshalb gar nicht erst gespeichert werden (Pflicht für die Import-Spec, 9.5). - KAT-01 und KAT-02 lesen Katalogdaten nur über diese View (bzw. über eine RPC, die darauf aufbaut). Der Direktzugriff auf
gamesbleibt für eigene private Spiele und für bestehende Verweise (Sammlung, Partien) nötig.
Rechte auf games neu schneiden (Krit. 9)
revoke insert, update on public.games from authenticated;
grant insert (owner_id, title, description, description_locale, year, min_players, max_players,
min_minutes, max_minutes, min_age, image_path, bgg_id, visibility)
on public.games to authenticated;
grant update (title, description, description_locale, year, min_players, max_players,
min_minutes, max_minutes, min_age, image_path, bgg_id, visibility)
on public.games to authenticated;
delete und select bleiben wie bisher. Die Policies regeln weiterhin, welche Zeilen (eigene private Spiele bzw. Admin). Damit kann kein Nutzer hidden_at, merged_into_id, manual_fields, catalog_source_id oder source_* setzen, auch nicht an eigenen privaten Spielen (Fehler 42501). id ist bewusst nicht enthalten: Clients können die ID eines Spiels nicht selbst wählen, sie bekommen sie per returning. Bisher gibt es keinen Client-Code, der eine ID mitschickt; KAT-04 (#13) legt vorläufige Spiele ohne eigene ID an. visibility bleibt im Update-Recht, ab 2c lehnt aber der Trigger games_visibility_immutable jede Änderung ab (E14).
3.3 Migration 2b supabase/migrations/<ts>_catalog_merge.sql (R1.2)
Interne Funktion public.repoint_game_references(p_from uuid, p_to uuid) returns jsonb
language plpgsql, security invoker, search_path = ''; execute nur für den Eigentümer. Hängt alle Verweise von p_from auf p_to um, in einer Transaktion.
Prüfungen zu Beginn:
| Prüfung | Fehler (P0001) |
|---|---|
p_from = p_to |
repoint_same_game |
p_to ist kein sichtbares Katalogspiel (visibility = 'catalog' and hidden_at is null and merged_into_id is null) |
repoint_target_not_listed |
Dann entsteht eine temporäre Zuordnung „alte Variante → neue Variante“ (variant_map). Ob kopiert wird, folgt aus der Sichtbarkeit (kein Parameter, W-c):
| Fall | Erkennung | Zuordnung der Varianten |
|---|---|---|
Zwei Katalogspiele zusammenführen (admin_merge_games) |
p_from und p_to haben visibility = 'catalog' |
Für jede Variante von p_from wird eine Kopie (name, year, language, bgg_id) an p_to angelegt, außer p_to hat schon eine Variante mit gleichem Namen; dann wird auf diese abgebildet. Danach werden die alten Varianten gelöscht. |
Privaten Eintrag auf ein Katalogspiel umhängen (admin_resolve_report) |
p_from hat visibility = 'private' |
Es wird nichts kopiert (Entscheidung Chris, RB1; Klärung N15): Hat p_to eine Variante mit gleichem Namen, wird darauf abgebildet, sonst auf null. Die privaten Varianten bleiben am ausgeblendeten vorläufigen Eintrag (sie gehören dem Nutzer und werden mit seinem Konto gelöscht). |
Namensabgleich: gleich heißt lower(trim(name)) gleich. Gibt es am Ziel mehrere gleichnamige Varianten, wird immer dieselbe gewählt: die älteste nach created_at, bei Gleichstand die kleinste id.
Grund für die Zuordnung: Die zusammengesetzten Fremdschlüssel (variant_id, game_id) in collection_items und plays verlangen, dass die Variante zum neuen Spiel gehört. Ein UPDATE, das game_id und variant_id in derselben Zeile gemeinsam setzt (variant_id := variant_map(variant_id)), ist zulässig.
Danach, in dieser Reihenfolge:
collection_items: Hat derselbe Nutzer schon einen Eintrag zup_to, gilt die Kollisionsregel (Spec-F1): Der Zieleintrag bleibt; seine leeren Felder werden aus dem Quell-Eintrag gefüllt (note,acquired_on, die Felder aus SAM-02/SAM-04 undvariant_id, wobeivariant_idimmer übervariant_mapübersetzt wird, nie direkt übernommen); bei unterschiedlicher Liste giltownedvorwishlistvorshopping(Klärung 34); danach wird der Quell-Eintrag gelöscht. Ohne Kollision:game_id := p_to,variant_id := variant_map(variant_id).favorite_games: Hat der Nutzerp_toschon als Favorit, wird die Zeile zup_fromgelöscht (die Lücke inpositionist zulässig). Sonstgame_id := p_to.plays:game_id := p_to,variant_id := variant_map(variant_id).score_sheets:game_id := p_to. Sobald PAR-04 (#18) „ein Bogen je Spiel und Nutzer“ (Klärung 39) erzwingt, muss #18 hier eine Kollisionsregel ergänzen (9.5).- Nur wenn beide Katalogspiele sind: alte Varianten von
p_fromlöschen (sie sind jetzt unreferenziert). - Zuordnungen (Genre, Mechanik, Kategorie, Verlag) von
p_frombleiben unverändert am ausgeblendeten Quell-Eintrag; der Zieleintrag behält seine eigenen.
Rückgabe: jsonb mit Zählern je Tabelle, z. B. {"collection_items": 3, "collection_conflicts": 1, "plays": 7, "variants_copied": 2, "variants_mapped": 1, "variants_cleared": 0, …}, damit der Admin im Studio sieht, was passiert ist.
Auswirkung auf KAT-04 Krit. 4 („eigene Angaben gehen nicht verloren“, Klärung N15): Für alle Angaben gilt das, für die Edition nur eingeschränkt. Hat der Katalogeintrag keine gleichnamige Variante, wird die Edition am Sammlungseintrag und an Partien geleert; die private Variante selbst bleibt am ausgeblendeten vorläufigen Eintrag erhalten. Hinweis für #13 (9.5): Der Ergebnishinweis in der App sollte in diesem Fall sagen, dass die Edition neu gewählt werden muss; variants_cleared in der Rückgabe macht das für den Admin erkennbar.
Funktion public.admin_merge_games(p_source uuid, p_target uuid) returns jsonb
security invoker, search_path = ''; execute nur für den Eigentümer (E10). Prüft zu Beginn current_user in ('postgres', 'supabase_admin'), sonst Fehler admin_only. (Kein is_admin()-Pfad; QUE-08 in R4 ergänzt bei Bedarf einen eigenen.)
| Prüfung | Fehler (raise exception … using errcode = 'P0001') |
|---|---|
Aufrufer ist nicht postgres/supabase_admin |
admin_only |
p_source = p_target |
merge_same_game |
| Quelle oder Ziel existiert nicht | merge_game_not_found |
Quelle oder Ziel ist kein Katalogspiel (private Einträge nur über admin_resolve_report) |
merge_not_catalog |
| Ziel ist ausgeblendet oder selbst zusammengeführt | merge_target_not_listed |
| Quelle ist bereits zusammengeführt | merge_source_already_merged |
Ablauf:
repoint_game_references(p_source, p_target)(beide Katalog → Varianten werden kopiert).- BGG-Kennung und Quelle übernehmen (W6, bestätigt von Chris am 2026-11-03): Hat das Ziel kein
bgg_id, aber die Quelle, wird der Wert übertragen; ebensocatalog_source_id/source_ref/source_url/source_synced_atals Gruppe. Reihenfolge wegen der eindeutigen Indizes: Werte merken → an der Quelle leeren → am Ziel setzen. Haben beide unterschiedliche Werte, bleibt das Ziel unverändert, an der Quelle werden die Werte trotzdem geleert, und das Ergebnis-JSON meldet den Konflikt mit den alten Werten, z. B."bgg_id_conflict": {"source": 13, "target": 42}bzw."source_conflict": {"source": {"catalog_source_id": …, "source_ref": …, "source_url": …}, "target": {…}}, damit der Admin den geleerten Wert nicht verliert. Begründung: Sonst würde der BGG-Abgleich (SAM-08) später den ausgeblendeten Quell-Eintrag treffen.bgg_idundsource_*stehen nicht in dermanual_fields-Schlüsselliste. p_source.merged_into_id := p_target,hidden_at := coalesce(hidden_at, now()); Ketten glätten (update games set merged_into_id = p_target where merged_into_id = p_source).- Ab 2c zusätzlich:
game_reports.target_game_idvonp_sourceaufp_target.
Die Quelle wird nicht gelöscht: Ihre Shuffle-ID bleibt gültig, die App leitet alte Links (Web-URLs, Deep Links) auf den Zieleintrag weiter (KAT-02).
Ausblenden (schon 2a) braucht keine Funktion: update public.games set hidden_at = now() where id = … (Wieder einblenden: hidden_at = null). Das Runbook (4.4) enthält die Anweisungen.
3.4 Migration 2c supabase/migrations/<ts>_game_reports.sql (R1.2)
Trigger games_visibility_immutable (E14, Entscheidung Chris W-a): before update on public.games; ist new.visibility is distinct from old.visibility oder new.owner_id is distinct from old.owner_id, Fehler visibility_immutable (P0001). Gilt für alle Rollen einschließlich postgres. Das Konto-Löschen ist nicht betroffen (private Spiele werden per on delete cascade gelöscht, nicht geändert).
Neue Tabelle game_reports (Klärungen 6, 10, 26, 27, N12, N12a; E12)
| Spalte | Typ | Hinweis |
|---|---|---|
id |
uuid | PK |
owner_id |
uuid | not null, FK → profiles on delete cascade (meldender Nutzer; Klärung 10: hart löschen) |
origin |
text | not null, default 'user', Check in ('user','import') (N12) |
title |
text | not null, 1–200 (KAT-04 Krit. 1) |
publisher_name |
text | nullable, ≤ 200 (Freitext des Nutzers) |
year |
smallint | nullable, 1800–2100 |
link |
text | nullable, ≤ 500, Check ^https?:// |
bgg_id |
integer | nullable (N12, Deduplizierung) |
provisional_game_id |
uuid | nullable, FK → games on delete set null (vorläufiger privater Eintrag, KAT-04 Krit. 3) |
status |
text | not null, default 'open', Check in ('open','approved','rejected','merged') |
target_game_id |
uuid | nullable, FK → games on delete restrict (wie merged_into_id) |
resolved_at |
timestamptz | nullable |
editions_cleared |
integer | not null, default 0, Check >= 0 (Spec-F6, Option A, ergänzt bei der Umsetzung von 2c): Anzahl der Sammlungseinträge und Partien, deren Edition beim Umhängen geleert wurde |
created_at, updated_at |
timestamptz | Standard, Trigger set_updated_at |
Checks: (status = 'open') = (resolved_at is null); status <> 'open' or target_game_id is null; status not in ('approved','merged') or target_game_id is not null.
Indizes: (owner_id, created_at) für das Tageslimit (#13); unique (owner_id, bgg_id) where origin = 'import' and status = 'open' and bgg_id is not null (N12a); (created_at) where status = 'open' für die Admin-Liste.
RLS in Klartext
enable row level security;revoke all … from anon, authenticated;grant select on game_reports to authenticated.- Lesen: Der Meldende sieht seine eigenen Meldungen (für den Ergebnishinweis, KAT-04 Krit. 4–6); Admin-Policy
is_admin()für R4.anonsieht nichts. - Anlegen: in 2c nicht möglich (kein Insert-Recht). #13 ergänzt Insert-Recht, Insert-Policy (
owner_id = auth.uid(),origin = 'user',status = 'open',target_game_id is null,provisional_game_idnur ein eigenes privates Spiel) und den Limit-Trigger (10 je Nutzer und Tag, nurorigin = 'user'). - Ändern, Löschen: kein Recht für
anonundauthenticated(Krit. 9). Der Admin ändert in R1 nur überadmin_resolve_reportim Studio.
Funktion public.admin_resolve_report(p_report uuid, p_status text, p_target uuid default null) returns jsonb
Rechte und Rollenprüfung wie admin_merge_games.
| Prüfung | Fehler |
|---|---|
Aufrufer ist nicht postgres/supabase_admin |
admin_only |
| Meldung existiert nicht | report_not_found |
Meldung ist nicht open |
report_not_open |
p_status nicht in approved, merged, rejected |
report_status_invalid |
approved/merged ohne p_target |
report_target_required |
p_target ist kein sichtbares Katalogspiel |
report_target_not_listed |
Ablauf: status, target_game_id, resolved_at = now() setzen. Hat die Meldung einen vorläufigen Eintrag, dann (Spec-F3) [Geändert 2026-11-04, Entscheidung Chris: „abgelehnt mit Verweis“ entfällt; rejected nur ohne Ziel, Verweis = merged. Siehe Nachtrag am Ende und docs/specs/13-umsetzung.md.]
- bei
approvedund beimerged:repoint_game_references(provisional, target)(privat → Katalog, keine Varianten kopiert, RB1); der vorläufige Eintrag bekommtmerged_into_id = target,hidden_at = now()(KAT-04 Krit. 4, mit der Einschränkung zur Edition aus 3.3), - bei
rejectedmit Ziel: nurtarget_game_idwird gespeichert, der vorläufige Eintrag bleibt unverändert; ob der Nutzer auf den Zieleintrag wechselt, entscheidet er in der App (KAT-04 Krit. 5, #13), - bei
rejectedohne Ziel: keine Änderung am vorläufigen Eintrag (KAT-04 Krit. 6).
Die Rückgabe enthält die Zähler aus repoint_game_references (u. a. variants_cleared). Dasselbe variants_cleared speichert die Funktion in game_reports.editions_cleared (0 bei rejected und bei Meldungen ohne Umhängen), damit die App (#13) den Nutzer zum Neuwählen der Edition auffordern kann.
„Freigeben“ heißt in R1: Der Admin legt im Studio zuerst einen neuen Katalogeintrag an (bzw. wählt einen vorhandenen aus) und ruft dann die Funktion mit approved auf. Der vorläufige Eintrag wird nie öffentlich geschaltet (E14). Unterschied zu merged: approved = der Eintrag ist wegen dieser Meldung neu entstanden, merged = er war schon vorhanden.
Die Migration ersetzt admin_merge_games per create or replace, sodass beim Zusammenführen auch game_reports.target_game_id umgehängt wird, und wiederholt danach ausdrücklich revoke execute on function public.admin_merge_games(uuid, uuid) from public, anon, authenticated, service_role; 08 prüft die Rechte erneut.
3.5 Doku zum Datenmodell
docs/datenmodell-r1.md wird in jedem der drei PRs nachgezogen (neue Tabellen, Spalten, View, Funktionen, Trigger, Regeln). Dabei wird der doppelte Abschnitt „Tabellenstruktur (aus den Migrationen)“ zusammengeführt. supabase/README.md bekommt einen Abschnitt „Katalog (KAT-05)“; die Regel „Admin-Rolle kommt aus app_metadata.role = 'admin'“ erhält den Zusatz „in R1 niemandem vergeben, Pflege über Studio (KAT-05 E10)“; die Zahlen „17 Tabellen, 2 Views“ werden aktualisiert.
4. Backend
4.1 Überblick
| Objekt | Art | PR | Aufrufbar von |
|---|---|---|---|
catalog_sources, publishers, game_publishers, neue Spalten games |
Tabellen | 2a | Lesen: alle; Schreiben: Studio (postgres) bzw. Ersteller privater Spiele (game_publishers) |
catalog_games |
View | 2a | anon, authenticated (lesen) |
games_mark_manual_fields, game_links_mark_manual, games_merged_into_catalog |
Trigger | 2a | automatisch |
repoint_game_references |
interne Funktion | 2b | nur postgres |
admin_merge_games |
Funktion | 2b | nur postgres/supabase_admin (Studio/psql) |
games_visibility_immutable |
Trigger | 2c | automatisch |
game_reports |
Tabelle | 2c | Lesen: eigene; Schreiben: Studio |
admin_resolve_report |
Funktion | 2c | nur postgres/supabase_admin |
Keine Edge Functions, keine RPCs für die App in dieser Spec.
4.2 Admin-Zugang (self-hosted)
- Studio läuft als Datenbankrolle
postgresbzw.supabase_admin(je nach Self-Hosting-Setup; D1 bestätigt das mitselect current_userim Studio-SQL-Editor) und umgeht RLS. Die Admin-Funktionen akzeptieren genau diese beiden Rollen. Dieis_admin()-Policies sind nicht der Studio-Pfad; Schutz bietet allein der Zugang. - Nicht öffentlich erreichbar (Vorgabe für devops, D1): Studio, der Postgres-Port (5432), der Pooler (Supavisor, Standard 6543) und die Kong-Route
/pg/(pg-meta, darüber spricht Studio mit der Datenbank) sind nur über SSH-Tunnel oder VPN erreichbar. Im Self-Hosting gibt es in Studio keine Benutzerverwaltung, nur die Basic-Auth des Gateways (DASHBOARD_USERNAME/DASHBOARD_PASSWORD); sie bleibt als zweite Schicht mit starkem Passwort. Gilt ebenso für Staging. app_metadata.role = 'admin'wird in R1 niemandem gesetzt (E10). Falls doch nötig (R4), geht das nur serverseitig:update auth.users set raw_app_meta_data = raw_app_meta_data || '{"role":"admin"}' where id = …alspostgres; der Nutzer muss sich danach neu anmelden. Das Runbook dokumentiert das als „nicht in R1“.
4.3 Startbestand (Krit. 7, 10; E9)
Verbindliche Regeln SB1–SB8 (Spec-F4, Spec-F5, Spec-F7, Spec-F8)
| # | Regel | Prüfung |
|---|---|---|
| SB1 | Zum Go-live mindestens 180 sichtbare Katalogspiele aus dem Startbestand (Ziel etwa 200). | checks.sql |
| SB2 | Jedes Spiel hat alle Pflicht-Fakten: Titel, mindestens ein Verlag, Jahr, min_players, max_players, min_minutes, max_minutes, min_age, mindestens ein Genre, mindestens eine Mechanik. |
Skript und checks.sql |
| SB3 | Herkunft der Fakten (Spec-F7): Verlagsseiten, Spielschachtel und eigene Kenntnis, ergänzend Wikidata (CC0). Fakten werden nicht systematisch aus BGG oder einer anderen fremden Datenbank übernommen. Endgültige Bewertung mit der rechtlichen Prüfung vor dem Release. | Bestätigung durch Chris; optionale Spalte facts_source als Nachweis |
| SB4 | Kurztext (Spec-F5): jedes Spiel hat einen eigenen Kurztext auf Deutsch (description_locale = 'de'), geschrieben von Chris oder als KI-Entwurf, den Chris geprüft und freigegeben hat. KI-Entwürfe entstehen nur aus den Fakten; sie dürfen keine umformulierten Texte von BGG, Verlagen oder anderen Quellen sein (keine fremden Texte als Eingabe). |
Skript und checks.sql (Pflicht, Sprache); Herkunft: Chris |
| SB5 | Vokabular (Spec-F8): Genres und Mechaniken nur aus der von Chris freigegebenen Liste. | Skript |
| SB6 | Kein Cover (image_path leer), keine Quelle (catalog_source_id leer), keine BGG-Kennung (bgg_id leer) (N11, Krit. 10). |
CSV hat keine solchen Spalten; checks.sql |
| SB7 | Erweiterungen (Spec-F8) sind erlaubt; das Basisspiel wird in base_game_key vorgemerkt, die Verknüpfung entsteht mit KAT-03 (#12). Bis dahin erscheinen Erweiterungen als eigenständige Katalogeinträge. |
Skript (base_game_key zeigt auf vorhandenen key) |
| SB8 | Alle gesetzten Felder gelten als manuell gepflegt (Spec-F4); das erledigt der Trigger beim Einspielen. | pgTAP 06 |
Lieferweg
| Umgebung | Weg |
|---|---|
| lokal, CI | supabase/config.toml: [db.seed] sql_paths = ["./seed.sql", "./catalog/vocabulary.sql", "./catalog/startbestand.sql"]; supabase db reset spielt alles ein |
| Staging, Produktion | Nach den Migrationen spielt Chris vocabulary.sql und startbestand.sql per Studio-SQL-Editor oder psql ein (Runbook). Die Dateien sind idempotent und dürfen wiederholt laufen, bis zum Go-live; danach ist startbestand.sql eingefroren, Korrekturen nur noch im Studio (Spec-F6). |
Unverifizierte Annahmen (B3 prüft gegen CLI 3.x, Ergebnis ins Runbook):
supabase db resetlädt alle Dateien aussql_pathsin der angegebenen Reihenfolge;supabase startlädt Seeds nur beim ersten Start einer leeren Datenbank.begin;/commit;innerhalb einer Seed-Datei sind zulässig. Falls die CLI Seeds selbst in eine Transaktion packt oder die Anweisungen stören: Die Datei enthält keinbegin/commit, und das Runbook lässt sie auf Staging/Prod inbegin; \i …; commit;bzw. im Studio in einer Transaktion ausführen.
Feste IDs: UUID v5
- Namespace (fest, nie ändern; steht im Skript und in
docs/katalog-pflege.md):6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4. - Name je Objekt: Spiel
game:<key>, Genregenre:<vocab_key>, Mechanikmechanic:<vocab_key>(vocab_keyist ein stabiler englischer Kurzschlüssel im Vokabular, z. B.strategy,worker-placement). - Verlage bekommen keine abgeleitete ID, weil Chris im Studio auch selbst Verlage anlegen kann: Einfügen mit
on conflict ((lower(name))) do nothing, danach Auflösen der ID überlower(name).
Vokabular
- Maschinenlesbare Quelle ist
supabase/catalog/vocabulary.csv(Spaltenkind=genre/mechanic,vocab_key,name_de,name_en); das Skript erzeugt darausvocabulary.sql. - Kollision mit im Studio angelegten Begriffen (gleicher
name_deodername_enunter anderer ID):vocabulary.sqlfügt miton conflict do nothing(ohne Konfliktziel, fängt ID- und Namenskonflikte) ein; es entsteht kein Fehler 23505. Der vorhandene Begriff gewinnt.startbestand.sqllöst Genres und Mechaniken deshalb übername_deauf, nicht über die abgeleitete UUID.checks.sqlmeldet solche Begriffe als Information („Begriff mit abweichender ID“), damit Chris sie prüfen kann.
Idempotenz von startbestand.sql
- Verlage:
insert into publishers (name) … on conflict ((lower(name))) do nothing. - Spiele:
insert … on conflict (id) do update set … where (games.title, games.year, …, games.description, games.description_locale) is distinct from (excluded.title, excluded.year, …); unveränderte Zeilen werden damit nicht angefasst (updated_atundmanual_fieldsbleiben stabil). - Zuordnungen je Startbestandsspiel als Abgleich: Paare, die nicht mehr in der CSV stehen, werden gelöscht (
delete … where game_id = … and genre_id not in (…)); fehlende Paare werden miton conflict (game_id, genre_id) do nothing(bzw.mechanic_id,publisher_id) eingefügt. Unveränderte Paare bleiben unberührt. Kategorien (game_categories) fasst die Datei nicht an. - Schlüssel sind unveränderlich. Das Skript führt eine committete Liste
supabase/catalog/startbestand-keys.csv(key,uuid, erzeugt und fortgeschrieben). Fehlt ein früher vorhandenerkeyin der CSV, bricht das Skript ab („Schlüssel entfernt oder umbenannt“). Zusätzlich prüft ein CI-Schritt gegenmain, dass jede Zeile vonstartbestand-keys.csvausmainunverändert erhalten ist (die Liste wächst nur; D1). Umbenennen ist nicht erlaubt; ein falscher Schlüssel bleibt stehen, nur der Titel wird korrigiert. - Spiel entfernen: Zeile bleibt in der CSV, Spalte
hidden=ja→ das Skript erzeugthidden_at = coalesce(hidden_at, now()). Es wird nichts gelöscht (Verweise aus Sammlungen bleiben gültig). hiddenleer: Die Datei fassthidden_atnicht an. Ein im Studio ausgeblendetes Startbestandsspiel bleibt also ausgeblendet; wieder einblenden geht nur im Studio (hidden_at = null).
Dateien
supabase/catalog/vocabulary.csv(Quelle) →vocabulary.sql(erzeugt): Genres und Mechaniken (de/en) mit IDs nach UUID v5. Inhalt: Vorschlag durch Agents (B3a), gültig erst nach Freigabe durch Chris (C0).seed.sqllegt danach keine kollidierenden Begriffe mehr an.supabase/catalog/startbestand.csv: von Chris gepflegt (z. B. aus einer Tabellenkalkulation exportiert).supabase/catalog/build_startbestand.ts: Deno, nur Standardbibliothek; prüft beide CSV-Dateien und erzeugtvocabulary.sql,startbestand.sql,startbestand-keys.csvundchecks.sql. Die erzeugten Dateien werden nie von Hand bearbeitet.
CSV-Format startbestand.csv
| Spalte | Pflicht | Inhalt |
|---|---|---|
key |
ja | stabiler Schlüssel, ^[a-z0-9-]{2,60}$, eindeutig, unveränderlich |
title |
ja | Haupttitel |
publishers |
ja | ein oder mehrere Verlage, getrennt durch ; |
year |
ja | Erscheinungsjahr |
min_players, max_players |
ja | Spielerzahl |
min_minutes, max_minutes |
ja | Spieldauer |
min_age |
ja | Mindestalter |
genres |
ja | ein oder mehrere Genres aus dem freigegebenen Vokabular (name_de), getrennt durch ; |
mechanics |
ja | eine oder mehrere Mechaniken aus dem freigegebenen Vokabular, getrennt durch ; |
description |
ja (SB4) | eigener Kurztext |
description_locale |
ja (SB4) | immer de |
hidden |
nein | ja = ausgeblendet (Regel 5); leer = hidden_at bleibt unverändert (Regel 6) |
facts_source |
nein | Herkunft der Fakten als Nachweis für SB3 (z. B. „Verlag“, „Schachtel“, „Wikidata Q…“); wird nicht in die Datenbank übernommen |
alt_titles |
nein | reserviert für KAT-01 (#3), Format de:Titel;en:Title; wird ignoriert, bis #3 die Tabelle hat |
base_game_key |
nein | reserviert für KAT-03 (#12), key des Basisspiels (SB7); wird nur auf Existenz geprüft, bis #12 die Spalte hat |
Das Skript bricht mit Zeilennummer und Grund ab bei: fehlender Pflichtangabe, unbekanntem Genre/Mechanik, doppeltem oder entferntem key, max < min, Werten außerhalb der DB-Checks, fehlendem Kurztext, description_locale ≠ de, base_game_key ohne passenden key, doppeltem vocab_key oder Namen im Vokabular. Es gibt keine Spalte für Cover, Quelle oder BGG-Kennung (Krit. 10).
Prüfabfrage supabase/catalog/checks.sql (erzeugt, Teil der Release-Checkliste)
- Kennzeichnung der Startbestandsspiele: Das Skript bettet die IDs aus
startbestand-keys.csvein (with sb(id) as (values …)). Alle Prüfungen beziehen sich nur auf diese IDs; Spiele, die Chris später im Studio anlegt, und das Beispielspiel ausseed.sql(nur lokal) sind damit ausgenommen. - Je Regel eine Zeile mit Sollwert, Istwert und „ok“/„Verstoß“:
- SB1: Anzahl
sb-IDs incatalog_games≥ 180, - SB2:
sb-Spiele mityear,min_players,max_players,min_minutes,max_minutesodermin_ageleer, oder ohne Verlag, Genre oder Mechanik = 0, - SB4:
sb-Spiele ohnedescriptionoder mitdescription_locale≠de= 0, - SB6:
sb-Spiele mitimage_path,catalog_source_idoderbgg_id= 0, - Information: Vokabularbegriffe mit abweichender ID (siehe Vokabular).
- SB1: Anzahl
- Lokal und in CI ist SB1 nur Information (kein Abbruch), solange Chris noch einpflegt.
Daten werden nicht von Agents erfunden. Agents liefern Werkzeug, Format, Prüfungen, den Vokabular-Vorschlag und höchstens offensichtlich fiktive Testzeilen in Fixtures (z. B. „Testspiel A“). Den Inhalt liefert Chris (Krit. 7).
4.4 Runbook docs/katalog-pflege.md (Krit. 8)
Ein Dokument, wachsend über die PRs:
| Abschnitt | PR | Inhalt |
|---|---|---|
| Zugang | 2a | SSH-Tunnel/VPN (Studio, 5432, Pooler, /pg/), Rolle postgres bzw. supabase_admin; immer in einer Transaktion arbeiten (begin; … commit;); niemals set ohne local; vor größeren Änderungen Backup-Stand prüfen |
| Startbestand pflegen, einspielen und abnehmen | 2a | Regeln SB1–SB8; Vokabular- und Startbestands-CSV bearbeiten → Skript → Commit; Schlüssel nie umbenennen, Entfernen nur über hidden, Einblenden nur im Studio; UUID-v5-Namespace; Reihenfolge Migrationen → vocabulary.sql → startbestand.sql; checks.sql; ab Go-live ist die Datei eingefroren (Spec-F6); Ergebnis der Prüfung der unverifizierten Annahmen (4.3) |
| Spiel anlegen und bearbeiten | 2a | Tabelleneditor oder SQL; Zuordnungen zu Verlag, Genre, Mechanik; jede Änderung wird automatisch in manual_fields vermerkt; Markierung bewusst entfernen nur per SQL; Katalogspiele nie löschen, sondern ausblenden; merged_into_id nie von Hand setzen, nur über admin_merge_games |
| Ausblenden und wieder einblenden | 2a | hidden_at setzen bzw. leeren; Wirkung: nicht mehr in Suche/Entdecken, Detailseite und bestehende Sammlungen und Partien bleiben (Spec-F2) |
| Zusammenführen | 2b | Duplikat finden (normalisierter Titel + Jahr), select public.admin_merge_games('<quelle>', '<ziel>');, Ergebnis-JSON lesen (Kollisionen nach Spec-F1, Varianten, bgg_id_conflict/source_conflict mit den alten Werten), Fehlertexte |
| Meldungen bearbeiten | 2c | offene Meldungen abfragen (älteste zuerst), Ähnliche im Katalog suchen, Entscheidung: freigeben, zusammenführen ('merged'), ablehnen mit oder ohne Verweis ('rejected'); Fehlertexte. Verboten: einen privaten Eintrag öffentlich schalten (der Trigger lehnt es ab, E14). Freigeben: neuen Katalogeintrag anlegen und dabei nur Fakten aus den Meldungsfeldern übernehmen (title, publisher_name, year; link nur als Recherchehilfe), nie description oder image_path des vorläufigen Eintrags; Kurztext selbst schreiben (SB4); dann admin_resolve_report(…, 'approved', '<ziel>'). Editionen: werden nicht kopiert; soll die Edition des Nutzers erhalten bleiben, vorher am Katalogeintrag eine Variante anlegen, und zwar ebenfalls nur aus belegbaren Fakten (Name und Jahr der Edition aus Verlags- oder Schachtelangaben), nie durch Übernahme der privaten Variante |
| Admin-Rolle in der App | 2a | in R1 nicht vergeben (E10), Vorgehen für R4 |
Jeder SQL-Baustein im Runbook wird in pgTAP mindestens einmal genau so ausgeführt (06 für die Abschnitte aus 2a, 07 für 2b, 08 für 2c; siehe 7.1), damit das Runbook nicht veraltet.
5. Frontend
KAT-05 hat in R1 keine eigenen Screens (keine Admin-UI, Klärung 44). Die App-Anteile der Kriterien entstehen in den Stories, die Katalogdaten anzeigen. Krit. 10 wird deshalb erst mit KAT-02 (#4) vollständig abgenommen; erst danach schließt Chris #2 (E1).
| Kriterium | App-Anteil | Wo umgesetzt |
|---|---|---|
| Krit. 4 („nicht auffindbar“) | Suche und Entdecken lesen nur catalog_games |
KAT-01 (#3), Spec #3 |
| Krit. 4 (zusammengeführt, ausgeblendet) | Detailseite eines zusammengeführten Eintrags leitet auf den Zieleintrag weiter; ein ausgeblendeter Eintrag bleibt per Direktaufruf und aus Sammlung/Partien erreichbar und zeigt den Hinweis „nicht mehr im Katalog“ (Spec-F2) | KAT-02 (#4) |
| Krit. 10 (Platzhalter-Cover) | Platzhalter, wenn image_path leer |
Widget in 2a, Einsatz in #3/#4 |
| Krit. 10 (eigener Kurztext) | Anzeige von description, Sprachhinweis über description_locale (englische Oberfläche: „Beschreibung auf Deutsch“) |
KAT-02 (#4) |
| Krit. 5 (Ergebnis einer Meldung) | Hinweis am vorläufigen Eintrag; bei „abgelehnt mit Verweis“ Angebot, auf den Zieleintrag zu wechseln (Spec-F3); bei geleerter Edition Hinweis, sie neu zu wählen (3.3, N15) | KAT-04 (#13) |
In 2a: Widget GameCover in app/lib/core/widgets/game_cover.dart (wird von Katalog, Sammlung, Dashboard und Partien genutzt, deshalb core/).
- Eingaben:
imageUrl(nullable; die Auflösung des Storage-Pfads liefert später die Dateizugriffsschicht),title(für die Beschriftung),size(klein für Listen, groß für die Detailseite). - Zustände: kein Bild → Platzhalter; Bild lädt → Platzhalter mit dezenter Ladeanzeige; Fehler beim Laden → Platzhalter (kein Fehlerzustand, ein fehlendes Cover ist kein Fehler).
- Platzhalter (Spec-F9): ein einheitlicher, neutraler Platzhalter für alle Spiele, mit Flutter gezeichnet (Fläche in Theme-Farbe, neutrales Symbol), keine Bilddatei von Dritten, kein neues Paket.
- Semantics: Bild ausgeblendet aus dem Semantics-Baum, wenn der Titel daneben steht (
excludeSemantics-Variante); sonst Beschriftung aus ARBgameCoverPlaceholderLabel(„Kein Cover für {title}“ / „No cover for {title}“). - Ergänzung bei der Umsetzung: zusätzlicher ARB-Schlüssel
gameCoverLabel(„Cover von {title}“ / „Cover of {title}“) für ein tatsächlich geladenes Bild; die Beschriftung folgt dem, was sichtbar ist (Platzhalter beim Laden und nach einem Ladefehler, sonstgameCoverLabel). Der Parameter für dieexcludeSemantics-Variante heißttitleVisibleNearby.imageUrlmuss aus der Dateizugriffsschicht (eigener Storage) stammen, nie aussource_urloder Fremd-URLs (Datenschutz); Hinweise für #3/#4 (animierte Ladeanzeige undpumpAndSettle, signierte URLs bei privatem Bucket, CORS beim Self-Hosting) stehen im Doc-Kommentar des Widgets. - Keine Geschäftslogik, keine Domänenmodelle in 2a. Das Domänenmodell
CatalogGame(freezed) und das Repository legt KAT-01 (#3) an, auf Basis der Spalten voncatalog_games.
6. Plattform-Besonderheiten
- iOS, Android, Web: keine Unterschiede. Das Platzhalter-Cover ist Flutter-Code, keine Plattform-API.
- Offline: nicht betroffen. Katalogsuche braucht eine Verbindung (PAR-01 Krit. 6).
- Dateien/Kamera/Push: nicht betroffen. Cover-Uploads gibt es in R1 nicht (N11).
- Self-Hosting: Zugang nur über Tunnel/VPN (4.2); Startbestand per SQL-Datei (4.3).
7. Teststrategie
Werkzeuge: pgTAP (supabase test db), Deno-Tests für das Erzeugungsskript, flutter_test für das Widget. Testnamen enthalten die Referenz („KAT-05 Kriterium n“).
Neue pgTAP-Dateien
06_catalog_foundation.test.sql(2a): Spalten, Checks, Grants, Spaltenrechte, Triggermanual_fields(inkl. Import-Ausnahme mit einer im Test angelegten Rollecatalog_importer), Triggergames_merged_into_catalog, Viewcatalog_games,catalog_sources, Verlage, Ausblenden, Runbook-Bausteine aus 2a.07_catalog_merge.test.sql(2b):repoint_game_references(Prüfungen, beide Variantenfälle, Namensabgleich),admin_merge_games; Fremdschlüssel-Wächter.08_game_reports.test.sql(2c): Tabelle, RLS,admin_resolve_report, Triggergames_visibility_immutable, Rechte vonadmin_merge_gamesnach demcreate or replace, Konto-Löschen nach Freigabe.00_schema.test.sqlwird angepasst (fest „17 Tabellen“ → 20 in 2a, 21 in 2c).
Die Tests müssen auch mit eingespieltem Startbestand grün sein: Sie legen eigene Fixtures mit festen Test-UUIDs an und zählen nur diese. 02_catalog.test.sql löscht weiterhin alles (delete from public.games innerhalb der zurückgerollten Transaktion); das funktioniert auch mit dem Startbestand, weil er keine Verweise aus Nutzertabellen hat.
Fremdschlüssel-Wächter (2b, erweitert in 2c): Ein Test liest aus pg_constraint alle Fremdschlüssel, die auf public.games zeigen, und vergleicht sie mit einer festen Liste in zwei Gruppen:
- behandelt von
repoint_game_references/admin_merge_games:collection_items.game_id,favorite_games.game_id,plays.game_id,score_sheets.game_id,game_variants.game_id,games.merged_into_id(Ketten glätten), ab 2cgame_reports.target_game_id, - bewusst ausgelassen:
game_genres,game_mechanics,game_categories,game_publishers(bleiben am Quell-Eintrag), ab 2cgame_reports.provisional_game_id(zeigt auf ein privates Spiel; das Zusammenführen zweier Katalogspiele betrifft ihn nicht).
Legt eine spätere Story eine neue Tabelle mit Verweis auf games an, wird der Test rot und erzwingt eine Entscheidung.
7.1 Abdeckungsmatrix
| AK | Unit | Widget | Integration/E2E | RLS/pgTAP | Task |
|---|---|---|---|---|---|
| KAT-05 Krit. 1 (nur Modell) | – | – | – | 06: Update eines Katalogspiels als postgres → Feld in manual_fields; set local shuffle.catalog_import = 'on' als postgres → trotzdem markiert; als Rolle catalog_importer mit dieser Einstellung → nicht markiert; Insert markiert alle gesetzten Felder (Spec-F4); Zuordnung Genre/Verlag anlegen, ändern, löschen → genres/publishers markiert; unbekannter Schlüssel → 23514 |
B1, B2 |
| Krit. 2 | – | – | – | 06: games.id ist uuid-PK mit Standard; ID bleibt bei Update unverändert; catalog_source_id ohne source_ref → 23514; (catalog_source_id, source_ref) im Katalog eindeutig → 23505; Quelle bgg vorhanden; terms_url ohne https:// → 23514 |
B1, B2 |
| Krit. 3 | – | – | – | – (zurückgestellt, kein Modellanteil) | – |
| Krit. 4 | – | – | (KAT-01: Suche findet ausgeblendete Spiele nicht, E2E in #3) | 06: Ausblenden/Einblenden per Runbook-SQL; catalog_games ohne ausgeblendete und zusammengeführte Spiele für anon und authenticated; ausgeblendetes Spiel bleibt per ID lesbar (Spec-F2); Sammlungseintrag zu einem ausgeblendeten Spiel bleibt lesbar; merged_into_id auf ein privates Spiel → merged_into_not_catalog. 07: repoint_game_references mit p_from = p_to → repoint_same_game, mit ausgeblendetem/zusammengeführtem/privatem Ziel → repoint_target_not_listed; Zusammenführen zweier Katalogspiele kopiert Varianten (gleichnamige werden abgebildet; bei mehreren gleichnamigen Zielvarianten immer die älteste, bei Gleichstand kleinste id) und hängt Sammlung, Favoriten, Partien (mit Variante) und Wertungsbögen um; Kollision (Spec-F1) inkl. Variante über die Zuordnung; bgg_id/source_* übertragen bzw. Konflikt mit alten Werten im JSON, Unique-Indizes verletzt nicht; Quelle danach merged_into_id/hidden_at, Ketten geglättet; Löschen eines Ziels mit Verweis → 23503; alle Fehlerfälle (admin_only … merge_source_already_merged, merge_not_catalog für private Quelle); Fremdschlüssel-Wächter |
B1, B2, B5, B6 |
| Krit. 5 | – | – | – | 08: Checks der Status-Kombinationen; admin_resolve_report für approved und merged (vorläufiger Eintrag umgehängt und zusammengeführt; keine Variante kopiert, gleichnamige abgebildet, sonst variant_id leer, private Varianten bleiben am vorläufigen Eintrag), rejected mit Ziel (nur Verweis), rejected ohne Ziel (unverändert); report_not_open, report_target_required, report_target_not_listed; N12a-Index; Konto des Meldenden nach Freigabe löschen gelingt (vorläufiger Eintrag und Meldung weg), Katalogspiel und die umgehängten Daten anderer Nutzer bleiben unverändert; Fremdschlüssel-Wächter erweitert |
B8, B9 |
| Krit. 6 (nur Modell) | – | – | – | 06: catalog_games.description/image_path sind null bei Quelle ohne Erlaubnis, sichtbar mit Erlaubnis, bei manuell gepflegtem Feld und ohne Quelle (zweite Sicherung, E6) |
B1, B2 |
| Krit. 7 | Deno: Skript erzeugt aus Fixture-CSVs die erwartete SQL, Schlüsselliste, vocabulary.sql und checks.sql (Snapshot); gleicher key → gleiche UUID (Namespace aus 4.3); entfernter key → Fehler; hidden = ja → hidden_at, hidden leer → hidden_at unverändert; facts_source landet nicht in der SQL |
– | CI: supabase db reset mit vocabulary.sql + startbestand.sql läuft durch; Idempotenz: zweites Einspielen ändert nichts – Vergleich der Spiele ohne updated_at, Zuordnungen als Schlüsselpaare (game_id, genre_id usw.), dazu updated_at unverändert dank is distinct from; Verlag mit anderer Schreibweise und im Studio angelegter Begriff mit gleichem name_de führen zu keinem Fehler; CI-Schritte „erzeugte Dateien aktuell“ (git diff --exit-code) und „startbestand-keys.csv wächst nur“ (gegen main); checks.sql läuft ohne Fehler |
– | B3, T1, D1 |
| Krit. 8 | – | – | – | 06/07/08: jeder SQL-Baustein aus docs/katalog-pflege.md wird im Test genau so ausgeführt (Kommentar mit Abschnittsverweis) |
B4, B7, B10 |
| Krit. 9 | – | – | API-Test optional (PostgREST PATCH /rest/v1/games mit Nutzer-JWT → 0 Zeilen), nur falls test-engineer ihn für nötig hält |
06: Grants laut 3.2 (has_table_privilege je Rolle); anon und Nutzer (auch mit gefälschtem user_metadata.role = 'admin') können Katalogspiele, catalog_sources, publishers, game_publishers (Katalog) nicht ändern; Nutzer darf Verlage seinem privaten Spiel zuordnen, fremden nicht; Nutzer kann an eigenem privaten Spiel id, hidden_at, merged_into_id, manual_fields, catalog_source_id nicht setzen (42501), wohl aber title; View nicht schreibbar. 07/08: Admin-Funktionen für anon, authenticated (auch mit app_metadata.role = 'admin') und service_role nicht ausführbar (42501), in 08 erneut für admin_merge_games nach dem create or replace; game_reports: Nutzer B liest A's Meldungen nicht, niemand außer postgres ändert oder löscht Meldungen, anon liest nichts; 08: Änderung von visibility (privat → Katalog, Katalog → privat) und owner_id an bestehenden Spielen → visibility_immutable, auch als postgres |
B2, B6, B9 |
| Krit. 10 | Deno: CSV ohne Pflicht-Fakten, ohne Kurztext oder mit description_locale ≠ de → Fehler; unbekannter Begriff → Fehler; keine Cover-/Quellen-/BGG-Spalte |
GameCover: ohne imageUrl → einheitlicher Platzhalter; Ladefehler → Platzhalter; Semantics-Label de/en; Kontrast und textScaler 2.0 ohne Überlauf |
(KAT-02: Detailseite zeigt Platzhalter, Kurztext und Sprachhinweis, in #4) | 06: Fixture-Spiele im Startbestands-Format erfüllen SB6 und SB4; checks.sql meldet für die Fixtures 0 Verstöße und für ein absichtlich fehlerhaftes Fixture (fehlende Spieldauer, gesetzte bgg_id) den erwarteten Verstoß; ein Nicht-Startbestandsspiel wird ignoriert |
B3, U1, T1, T2 |
| Sicherheit Krit. 2 | – | – | – | 06: anon liest catalog_games, catalog_sources, publishers, game_publishers (Katalog), schreibt nichts |
B2 |
| Sicherheit Krit. 3 | – | – | – | 08: fremder Zugriff auf game_reports |
B9 |
| QUE-05 Krit. 2, QUE-06 Krit. 1–3 | – | GameCover in de/en, a11y-Guidelines |
– | – | U1, T2 |
Nicht automatisiert testbar, manuelle Prüfung: Zugang nur über Tunnel/VPN (Studio, 5432, Pooler, /pg/; D1); Einspielen des Startbestands auf Staging/Prod und checks.sql mit mindestens 180 Spielen (SB1, Abnahme durch Chris, C1); Herkunft der Fakten und Kurztexte nach SB3/SB4 (Chris, rechtliche Prüfung vor dem Release); inhaltliche Richtigkeit (Chris).
8. Aufgaben
Alle PRs: Refs #2 (E1).
8.1 PR 2a – Fundament (R1.1), Branch feature/2-katalog-fundament
| # | Agent | Aufgabe | abhängig von |
|---|---|---|---|
| B1 | backend-dev | Migration <ts>_catalog_foundation.sql nach 3.2: catalog_sources (+ Zeile bgg), neue Spalten in games mit Checks, on delete-Regeln und Indizes, publishers, game_publishers, Trigger games_mark_manual_fields (Import-Ausnahme nur für Rolle catalog_importer + set local), game_links_mark_manual (insert/update/delete) und games_merged_into_catalog, View catalog_games, Spaltenrechte auf games (ohne id), RLS, Policies und Grants laut Grant-Tabelle |
– |
| B2 | backend-dev | pgTAP 06_catalog_foundation.test.sql nach 7.1 (Krit. 1, 2, 4, 6, 8, 9, 10, Sicherheit Krit. 2) inkl. Runbook-Bausteine aus 2a; bestehende Tests mit Startbestand lauffähig halten; 00_schema.test.sql auf 20 Tabellen; README-Zahlen (3 Views mit catalog_games) |
B1 |
| B3a | backend-dev | Vokabular-Vorschlag (ca. 12–15 Genres, 20–30 Mechaniken, je vocab_key, name_de, name_en) als supabase/catalog/vocabulary.csv; im PR als eigener Abschnitt zur Freigabe. Gilt erst nach C0. |
B1 |
| B3 | backend-dev | Startbestand-Werkzeug nach 4.3: CSV-Vorlage, Skript build_startbestand.ts (Prüfregeln SB2, SB4–SB7, Vokabular-Erzeugung und -Kollision, Schlüsselliste, Idempotenz-Regeln 1–6 mit is distinct from, UUID-v5-Namespace, Erzeugung von vocabulary.sql, startbestand.sql, startbestand-keys.csv, checks.sql) mit Deno-Tests; config.toml [db.seed] sql_paths; seed.sql bereinigen; unverifizierte Annahmen aus 4.3 prüfen und Ergebnis dokumentieren |
B1, B3a |
| B4 | backend-dev | Doku: docs/katalog-pflege.md Abschnitte Zugang, Startbestand, Spiel anlegen/bearbeiten, Ausblenden, Admin-Rolle (4.4); docs/datenmodell-r1.md (neue Objekte, doppelten Abschnitt zusammenführen); supabase/README.md (3.5) |
B1, B3 |
| U1 | ui-designer | GameCover in core/widgets/ nach 5: ein einheitlicher neutraler Platzhalter (Spec-F9), Größen klein/groß, Ladezustand, Semantics, ARB-Schlüssel de/en |
– |
| T1 | test-engineer | Review der pgTAP-Abdeckung gegen 7.1; Deno-Tests des Skripts ergänzen (jede Prüfregel mit Positiv- und Negativfall, Idempotenz wie in 7.1 präzisiert, entfernter Schlüssel, Vokabular-Kollision); CI-Schritte mit devops abstimmen | B2, B3 |
| T2 | test-engineer | Widget-Tests GameCover (Platzhalter, Ladefehler, de/en, a11y-Guidelines, textScaler 2.0) |
U1 |
| D1 | devops | Zugang auf Staging/Prod nur über SSH-Tunnel/VPN für Studio, 5432, Pooler und Kong-Route /pg/; Basic-Auth gesetzt; Studio-Rolle per select current_user bestätigen (Doku in docs/, kein Eingriff ohne Freigabe); CI: Deno bereitstellen, Schritte „erzeugte Dateien aktuell“ und „startbestand-keys.csv wächst nur“ (Vergleich mit main); Release-Checkliste um checks.sql (SB1: mindestens 180) ergänzen |
B3 |
| C0 | Chris | Vokabular-Vorschlag aus B3a freigeben oder korrigieren | B3a |
| C1 | Chris | Startbestand in der CSV pflegen (mindestens 180, Ziel ~200; Fakten nach SB3, deutsche Kurztexte nach SB4), Skript ausführen, committen; vor dem Go-live auf Produktion einspielen und checks.sql abnehmen; danach #2 schließen, sobald #4 fertig ist |
B3, C0 |
8.2 PR 2b – Zusammenführen (R1.2), Branch feature/2-katalog-pflege
| # | Agent | Aufgabe | abhängig von |
|---|---|---|---|
| B5 | backend-dev | Migration <ts>_catalog_merge.sql nach 3.3: repoint_game_references(p_from, p_to) (Prüfungen, Variantenfall aus der Sichtbarkeit, Namensabgleich mit fester Auswahlregel, Kollisionsregel Spec-F1 über variant_map), admin_merge_games (Rollenprüfung, Übertragung bgg_id/source_*, Konflikte mit alten Werten), Rechte (revoke execute inkl. service_role) |
2a gemergt |
| B6 | backend-dev | pgTAP 07_catalog_merge.test.sql inkl. Prüfungen von repoint_game_references, Kollisionsfällen, Variantenfällen (auch mehrere gleichnamige Zielvarianten), bgg_id-Übertragung und -Konflikt, Fremdschlüssel-Wächter |
B5 |
| B7 | backend-dev | Runbook: Abschnitt Zusammenführen (SQL-Bausteine identisch zu den Tests) | B5, B6 |
8.3 PR 2c – Meldungen (R1.2), Branch feature/2-katalog-meldungen
| # | Agent | Aufgabe | abhängig von |
|---|---|---|---|
| B8 | backend-dev | Migration <ts>_game_reports.sql nach 3.4: Trigger games_visibility_immutable, Tabelle, Checks, on delete-Regeln, Indizes, RLS (nur Lesen der eigenen), admin_resolve_report (Umhängen bei approved/merged ohne Varianten-Kopie; nur Verweis bei rejected mit Ziel), admin_merge_games per create or replace erweitert und revoke execute danach ausdrücklich wiederholt |
2b gemergt |
| B9 | backend-dev | pgTAP 08_game_reports.test.sql (inkl. visibility_immutable auch als postgres, Rechte von admin_merge_games erneut, Konto-Löschen nach Freigabe); Fremdschlüssel-Wächter um game_reports.target_game_id (behandelt) und game_reports.provisional_game_id (bewusst ausgelassen) erweitern; 00_schema.test.sql auf 21 Tabellen; README-Zahlen |
B8 |
| B10 | backend-dev | Runbook: Abschnitt Meldungen bearbeiten (Verbot „öffentlich schalten“, nur Fakten aus Meldungsfeldern, Editionen nur aus belegbaren Fakten) | B8, B9 |
8.4 Reihenfolge und Parallelität
R1.1 B1 → B2 | B3a → C0 → B3 → B4, T1, D1 parallel: U1 → T2 danach: C1 (Chris, laufend bis Go-live)
2a gemergt → #3 KAT-01 kann starten
R1.2 B5 → B6 → B7 (2b) → B8 → B9 → B10 (2c) → #13 KAT-04 Umsetzung
Abschluss #2: C1 abgenommen und #4 KAT-02 gemergt → Chris schließt #2
B3 kann mit einem vorläufigen Fixture-Vokabular für die Tests beginnen; vocabulary.csv wird erst nach C0 endgültig. 2c muss vor der Umsetzung von #13 gemergt sein (E12). Merge nur durch Chris.
9. Risiken & offene Fragen
9.1 Risiken
| # | Risiko | Auswirkung | Gegenmaßnahme |
|---|---|---|---|
| R1 | Studio umgeht RLS und hat im Self-Hosting keine eigene Benutzerverwaltung | Wer Studio, Postgres, Pooler oder /pg/ erreicht, kann alles lesen und ändern, auch private Nutzerdaten |
Zugang nur über SSH-Tunnel/VPN (D1), Basic-Auth als zweite Schicht; Runbook: Transaktionen, nie set ohne local, Funktionen statt Hand-SQL; Trigger gegen Öffentlich-Schalten (E14) |
| R2 | EU-Datenbankrecht und Nutzungsbedingungen bei massenhafter Übernahme von Fakten aus BGG oder einer anderen Datenbank; ebenso umformulierte fremde Texte | Rechtliches Risiko für den Start | Entschieden (Spec-F7): SB3, SB4 (KI-Entwürfe nur aus Fakten); optionaler Herkunftsnachweis facts_source; rechtliche Prüfung vor dem Release (R11 in Spec #1) |
| R3 | Spätere Tabellen mit Verweis auf games werden beim Zusammenführen vergessen |
Verwaiste Verweise auf ausgeblendete Quell-Einträge | Fremdschlüssel-Wächter (7) |
| R4 | Ausgeblendete Spiele bleiben über die rohe API per ID lesbar (E3) | Ein aus rechtlichen Gründen ausgeblendeter Eintrag wäre weiter abrufbar | Akzeptiert (Spec-F2); im Ernstfall inhaltlich leeren; Status „gesperrt“ mit RLS-Filter erst bei Bedarf |
| R5 | Startbestand weicht zwischen Datei und Produktion ab, sobald Chris nach dem Go-live im Studio korrigiert | Erneutes Einspielen überschreibt Studio-Korrekturen an Fakten und Zuordnungen | Entschieden (Spec-F6): Datei ab Go-live eingefroren, wird nicht mehr eingespielt; hidden_at fasst die Datei ohnehin nur bei hidden = ja an |
| R6 | manual_fields markiert den ganzen Startbestand als manuell (Spec-F4) |
Ein späterer Import ergänzt nur leere Felder | Bewusst entschieden; Markierungen lassen sich per SQL gezielt entfernen |
| R7 | game_reports wird in #2 angelegt, die Regeln zum Anlegen kommen erst mit #13 |
Zwischen 2c und #13 gibt es Meldungen nur per Studio | Kein Insert-Recht für Nutzer in 2c |
| R8 | Spaltenrechte auf games und die Rollenprüfung der Funktionen schränken auch einen künftigen Admin über die API ein; der Trigger game_links_mark_manual (security invoker) müsste dann manual_fields schreiben dürfen |
QUE-08 (R4) braucht eigene RPCs (security definer) bzw. angepasste Rechte | Bewusst; Hinweis für QUE-08 in 9.5 |
| R9 | ~180–200 Spiele mit deutschen Kurztexten einpflegen ist viel Handarbeit für Chris | Startbestand zum R1-Start unvollständig | CSV mit Prüfungen, KI-Entwürfe aus Fakten zulässig (Spec-F5), Mindestzahl 180 (Spec-F8); checks.sql zeigt den Fortschritt |
| R10 | Beim Umhängen eines vorläufigen Eintrags geht die Edition verloren, wenn der Katalogeintrag keine gleichnamige Variante hat (RB1) | KAT-04 Krit. 4 für die Edition nur eingeschränkt erfüllt | Bewusst entschieden (Chris), festgehalten als Klärung N15; Runbook: passende Variante am Katalogeintrag vorher aus belegbaren Fakten anlegen; Hinweis in der App (#13) |
| R11 | Unverifizierte CLI-Annahmen zu sql_paths und Transaktionen (4.3) |
Seed bricht lokal/CI ab oder lädt in falscher Reihenfolge | B3 prüft vorab; dokumentierter Rückfall ohne begin/commit |
9.2 Entscheidungen zu den Spec-Fragen (Chris, 2026-11-03)
Alle neun Fragen hat Chris am 2026-11-03 entschieden („Alle wie empfohlen“). Keine Frage blockiert mehr einen PR.
| # | Frage | Status | Entscheidung | Wirkung in der Spec |
|---|---|---|---|---|
| Spec-F1 | Zusammenführen bei Doppelbesitz | entschieden 2026-11-03 | Option A: Zieleintrag bleibt; leere Felder werden aus dem Quell-Eintrag gefüllt (Edition über die Variantenzuordnung); Sammlung vor Wunsch- vor Einkaufsliste (Klärung 34); Quell-Eintrag wird gelöscht. | 3.3 Schritt 1, 7.1 Krit. 4, B5, B6 |
| Spec-F2 | Sichtbarkeit ausgeblendeter Einträge | entschieden 2026-11-03 | Option A: nur aus Suche und Entdecken; Detailseite, Sammlungen und Partien zeigen sie weiter mit Hinweis „nicht mehr im Katalog“. Status „gesperrt“ erst bei Bedarf. | E3, 5, R4, KAT-02 (#4) |
| Spec-F3 | Umhängen bei „zusammengeführt“ / „abgelehnt mit Verweis“ | entschieden 2026-11-03 | Option A: „zusammengeführt“ hängt um (wie „freigegeben“); „abgelehnt mit Verweis“ speichert nur den Verweis. | 3.4, 4.4, 5, 7.1 Krit. 5, B8 |
| Spec-F4 | Kennzeichnung des Startbestands | entschieden 2026-11-03 | Option A: alle gesetzten Felder gelten als manuell gepflegt. | E4, 3.2, SB8, R6, B1 |
| Spec-F5 | Kurztexte | entschieden 2026-11-03 | Deutsch; Chris schreibt oder prüft KI-Entwürfe (nur aus Fakten, SB4); Pflicht für alle Startbestandsspiele. | E8, § 2, SB4, B3 |
| Spec-F6 | Pflegeformat | entschieden 2026-11-03 | Option A: CSV + Deno-Skript; Datei nach Go-live eingefroren. | E9, 4.3, 4.4, R5, B3 |
| Spec-F7 | Herkunft der Fakten | entschieden 2026-11-03 | Option A, ergänzend C: Verlagsseiten, Schachtel, eigene Kenntnis, Wikidata (CC0); keine systematische Übernahme aus BGG. | § 2, SB3, R2, C1 |
| Spec-F8 | Umfang, Abnahme, Vokabular, Erweiterungen | entschieden 2026-11-03 | Mindestens 180 Spiele (Ziel ~200); Vokabular schlagen Agents vor, Chris gibt frei; Erweiterungen erlaubt und vorgemerkt. | § 1, § 2, SB1, SB5, SB7, B3a, C0, C1, D1 |
| Spec-F9 | Platzhalter-Cover | entschieden 2026-11-03 | Option A: ein einheitlicher, neutraler Platzhalter. | 5, U1, T2 |
Weitere Entscheidungen von Chris aus den Reviews zu PR #51 (2026-11-03):
| # | Thema | Entscheidung | Wirkung |
|---|---|---|---|
| RB1 | Varianten beim Umhängen eines privaten Eintrags | Nicht kopieren, auf null: Abbildung auf gleichnamige Variante des Ziels, sonst null. Beim Zusammenführen zweier Katalogspiele werden Varianten weiter kopiert. Festgehalten als Klärung N15 (requirements/r1-klaerungen.md). |
E11, 3.3, 3.4, R10, 7.1, 9.5 (#13) |
| W3 | Issue-Abschluss | Alle drei PRs nur Refs #2; Chris schließt #2 nach C1 und KAT-02 (#4). |
E1, § 2, § 5, 8 |
| W-a | Privaten Eintrag öffentlich schalten | Trigger plus Runbook-Regel: Der Trigger games_visibility_immutable lehnt jede Änderung von visibility und owner_id ab (auch als postgres), das Runbook verbietet es ausdrücklich. |
E14, 3.4, 4.4, 7.1 Krit. 9, B8, B9, B10 |
| N15 | KAT-04 Krit. 4 (Edition) | Abweichung als neue Klärung N15 in requirements/r1-klaerungen.md festgehalten. |
§ 2, 3.3, R10, 9.5 |
9.3 Designwahlen aus dem Review – bestätigt (Chris, 2026-11-03)
Chris hat jede Wahl einzeln und ohne Änderung bestätigt (Kommentar im Issue #2 vom 2026-11-03).
| # | Status | Wahl | Begründung | Verworfene Alternative |
|---|---|---|---|---|
| W1 | bestätigt (Chris, 2026-11-03) | Krit. 6: Daten ohne Anzeige-Erlaubnis werden gar nicht gespeichert (Pflicht für die Import-Spec); die View ist nur zweite Sicherung (E6). | Am wenigsten invasiv: keine Änderung an den Leserechten von games, keine Auswirkung auf private Spiele und bestehende Abfragen. |
Spaltenweises select für anon auf games |
| W6 | bestätigt (Chris, 2026-11-03) | admin_merge_games überträgt bgg_id und source_* auf das Ziel, wenn es keine hat, leert sie an der Quelle und meldet Konflikte mit den alten Werten (3.3). |
Sonst träfe der BGG-Abgleich (SAM-08) den ausgeblendeten Quell-Eintrag; die Unique-Indizes bleiben gewahrt. | Nichts übertragen, SAM-08 muss ausgeblendete Treffer selbst auflösen |
| W7 | bestätigt (Chris, 2026-11-03) | Import-Ausnahme beim Kennzeichnen nur für die Rolle catalog_importer und set local (E4). |
Ein versehentliches set im Studio schaltet die Kennzeichnung nicht ab. |
nur die Sitzungseinstellung |
| W4 | bestätigt (Chris, 2026-11-03) | Verlage ohne abgeleitete ID, Konfliktziel lower(name); Schlüssel unveränderlich, Entfernen über hidden; UUID-v5-Namespace 6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4 (4.3). |
Studio-Verlage und Startbestand vertragen sich; keine verwaisten Spiele. | feste Verlags-IDs aus dem Skript |
| H1/H2 | bestätigt (Chris, 2026-11-03) | Admin-Funktionen ohne is_admin()-Pfad, execute auch service_role entzogen (E10). |
Klärung 44; in R1 gibt es keinen API-Aufrufer. | is_admin() als R4-Vorbereitung belassen |
| H7 | bestätigt (Chris, 2026-11-03) | merged_into_id, target_game_id, catalog_source_id: on delete restrict; merged_into_id zeigt per Trigger nur auf Katalogspiele (3.2). |
Katalogspiele und Quellen werden ausgeblendet, nicht gelöscht; versehentliches Löschen fällt sofort auf; das Konto-Löschen bleibt möglich, weil private Spiele nie Ziel eines Verweises sind. | on delete set null |
9.4 Ausstehende Prozessschritte
- Spec freigegeben am 2026-11-03; Spec-F1–Spec-F9, Review und Re-Review PR #51 eingearbeitet; Designwahlen aus 9.3 einzeln bestätigt. Freigabe und Bestätigungen stehen als Kommentare im Issue #2.
- Nächste Schritte nach Merge der Spec: B1 (backend-dev), parallel U1 (ui-designer); B3a liefert den Vokabular-Vorschlag zur Freigabe durch Chris (C0).
- Label
size/xl→size/l(E2) ändert nur Chris. - Spec #13 (KAT-04) baut auf 3.4 auf (siehe 9.5).
- Kein ADR nötig: Die Entscheidungen bleiben innerhalb von ADR-0001/0002 und sind ohne großen Aufwand änderbar.
9.5 Hinweise für andere Stories, nicht Teil von #2
- KAT-01 (#3): Suche über
catalog_games(bzw. eine RPC darauf); Tabelle für alternative Titel (Klärung 3) mit neuemmanual_fields-Schlüssel; Spaltealt_titlesder Startbestands-CSV auswerten. - KAT-02 (#4): Weiterleitung bei
merged_into_id; beihidden_atDetailseite weiter anzeigen mit Hinweis „nicht mehr im Katalog“ (Spec-F2); Quellenangabe aussource_*(KAT-02 Krit. 5); Sprachhinweis überdescription_locale(Startbestand: Deutsch). Mit #4 wird Krit. 10 abgenommen; danach schließt Chris #2. - KAT-03 (#12): Basisspiel-Verweis (Klärung 5) mit
manual_fields-Schlüssel;base_game_keyder CSV auswerten (SB7); Fremdschlüssel-Wächter für die neue Spalte entscheiden. - KAT-04 (#13): Insert-Policy, Tageslimit, Ähnlichkeitsprüfung, Hinweis am vorläufigen Eintrag inkl. Wechsel auf den Zieleintrag bei „abgelehnt mit Verweis“ (Spec-F3), Herkunft „aus Import“ (N12). Krit. 4 („eigene Angaben bleiben erhalten“) gilt für die Edition nur eingeschränkt (RB1, Klärung N15): Ohne gleichnamige Variante am Katalogeintrag wird die Edition geleert; der Hinweis in der App sollte dann zum Neuwählen auffordern. Vorläufige Spiele ohne eigene ID anlegen (Spaltenrechte, 3.2); sie können nie öffentlich geschaltet werden (E14). Bitte als Hinweis ins Issue #13 übernehmen. [Überholt 2026-11-04: kein Wechsel auf den Zieleintrag mehr, siehe Nachtrag.]
- PAR-04 (#18): Sobald ein Wertungsbogen je Spiel und Nutzer erzwungen wird, Kollisionsregel in
repoint_game_referencesergänzen. - SAM-02/SAM-04 (#7, #8): Neue Felder an
collection_items(Zustand, Standort, Tags, Priorität) in die Kollisionsregel Spec-F1 aufnehmen. - SAM-08 (#25) und Import-Spec: Import läuft als Rolle
catalog_importermitset local shuffle.catalog_import = 'on'(E4). Achtung: Läuft der Import über einesecurity definer-Funktion, istcurrent_userderen Eigentümer, nichtcatalog_importer; die Ausnahme greift dann nicht. Der Import muss sich deshalb direkt alscatalog_importeranmelden oder Funktionen nutzen, diecatalog_importergehören. Weiter: pflegtcatalog_source_id,source_*,source_synced_at; speichert Cover und Texte einer Quelle ohne Anzeige-Erlaubnis gar nicht (E6, Pflicht); ignoriert beim Abgleich überbgg_idausgeblendete und zusammengeführte Einträge; legt Import-Meldungen mitorigin = 'import'an (N12, N12a); bekommt gezielt Rechte, die in R1 allen API-Rollen entzogen sind. - QUE-08 (R4): Admin-Pfad über die API braucht eigene RPCs (security definer) statt der Rollenprüfung
postgres/supabase_admin; Spaltenrechte aufgamesund Trigger-Rechte berücksichtigen (R8). - KTO-05 (#23):
game_reportswird peron delete cascademit dem Profil gelöscht; im Export enthalten. Vorläufige Spiele mitmerged_into_idauf ein Katalogspiel werden mit dem Konto gelöscht (sie sind die verweisende Seite,restrictgreift nicht; Test in08).
10. Änderungsprotokoll
2026-11-03 – Entwurf
Erste Fassung durch architect, wartet auf Freigabe und Antworten zu Spec-F1–Spec-F9.
Nachtrag am selben Tag: Rechte-Regel für neue Objekte (Standardrechte von Supabase erst entziehen) in 3.2 ausdrücklich ergänzt; Studio-Rolle postgres bzw. supabase_admin (4.2); B2/B9 um die Anpassung von 00_schema.test.sql (fest „17 Tabellen“) und der README-Zahlen ergänzt.
2026-11-03 – Freigabe und Entscheidungen Spec-F1–Spec-F9
Chris hat die Spec freigegeben und alle Fragen wie empfohlen entschieden. Eingearbeitet: Status, Entscheidungstabelle, E3/E4/E8/E9, Abnahme von Krit. 7 und 10 in § 2, verbindliche Startbestandsregeln, CSV-Format, Kollisions- und Umhäng-Regeln, einheitlicher Platzhalter, Tests und Aufgaben (B3a, C0), Hinweis für SAM-02/SAM-04.
2026-11-03 – Review PR #51
- RB1 (Chris): Beim Umhängen eines privaten Eintrags werden keine Varianten kopiert; Abbildung auf gleichnamige Zielvariante, sonst
null; Spec-F1-Füllregel übervariant_map; Auswirkung auf KAT-04 Krit. 4 (R10, 9.5). - W3 (Chris): Alle PRs
Refs #2; Abschluss von #2 nach C1 und #4 (E1, § 2, § 5, 8). - W1: E6 eingeschränkt, Pflicht „nicht speichern“ für die Import-Spec.
- W2: Grant-Tabelle je Objekt in 3.2.
- W4: Idempotenz von
startbestand.sql, unveränderliche Schlüssel mit Schlüsselliste, Entfernen überhidden, UUID-v5-Namespace. - W5:
checks.sqlerzeugt, mit eingebetteten Startbestands-IDs, alle SB2-Felder, SB6 inkl.bgg_id, Beispielspiel ausgenommen. - W6: Übertragung von
bgg_id/source_*beim Zusammenführen. - W7: Begründung zum Import-Schalter korrigiert; Ausnahme an Rolle
catalog_importergebunden; Runbook „niesetohnelocal“. - H1–H14:
is_admin()-Pfad aus den Funktionen gestrichen,service_roleentzogen; Spaltenrechte ohneid; Trigger auch für Update, R8 ergänzt; Zuordnung der Kriterien zu PRs; Fremdschlüssel-Wächter mitgame_reports;on delete restrict; Umbenennung in SB1–SB8 und Spec-F1–Spec-F9; Abhängigkeit #4; Pooler und/pg/; CLI-Annahmen als unverifiziert markiert (R11); KI-Entwürfe nur aus Fakten (SB4); Kleinigkeiten (H14).
2026-11-03 – Re-Review PR #51
- W-a (Chris): Neue Kernentscheidung E14 und Trigger
games_visibility_immutable(2c, auch fürpostgres), Test in08; Runbook verbietet ausdrücklich, private Einträge öffentlich zu schalten. - N15 (Chris): Abweichung bei KAT-04 Krit. 4 (Edition) als Klärung N15 in
requirements/r1-klaerungen.mdfestgehalten; Verweise in § 2, 3.3, R10, 9.5. - W-b: Runbook: beim Freigeben nur Fakten aus den Meldungsfeldern übernehmen, nie
description/image_pathdes vorläufigen Eintrags; Varianten am Katalogeintrag nur aus belegbaren Fakten anlegen. - W-c: Parameter
p_copy_variantsgestrichen; Kopieren folgt aus der Sichtbarkeit;repoint_game_referencesprüftp_from <> p_tound sichtbares Katalogziel; Tests in07. - W4 a–e: Upsert mit
is distinct fromund Abgleich der Zuordnungen, präzisierter Idempotenz-Test; Vokabular-Kollision ohne 23505, Auflösung übername_de;vocabulary.csvals Quelle; CI-Schritt „startbestand-keys.csvwächst nur“;hiddenleer lässthidden_atunverändert. - Feste Auswahlregel bei mehreren gleichnamigen Zielvarianten (älteste, dann kleinste
id), Test in07. - 2c:
revokefüradmin_merge_gamesnachcreate or replaceausdrücklich wiederholt, Prüfung in08; Konflikt-JSON mit alten Werten. - H7: Trigger
games_merged_into_catalog(2a) begründet; Test in08: Konto des Meldenden nach Freigabe löschen gelingt, Katalogspiel und Daten anderer Nutzer bleiben. - Verweise auf die Hinweisliste auf 9.5 korrigiert; Review-Blocker B1 in RB1 umbenannt; Hinweis zu
security definerundcurrent_userfür die Import-Spec (9.5).
2026-11-03 – Bestätigung der Designwahlen
Chris hat W1, W4, W6, W7, H1/H2 und H7 (§ 9.3) einzeln und ohne Änderung bestätigt (Kommentar im Issue #2). § 9.3 trägt jetzt je Zeile den Status „bestätigt (Chris, 2026-11-03)“; die Hinweise „bitte bestätigen“ in Status, E6 und 3.3 sind entfernt; 9.4 entsprechend angepasst.
2026-11-03 – Umsetzung PR 2a, Flutter-Anteil
GameCoverumgesetzt; Review-Nacharbeit: Semantics-Label folgt dem sichtbaren Zustand, zusätzlicher ARB-SchlüsselgameCoverLabel, ParametertitleVisibleNearby,cacheWidthnach Gerätepixeln, Hinweise für #3/#4 (5).
2026-11-03 – Umsetzung PR 2a (backend-dev)
Umgesetzt: B1, B2, B3a, B3, B4 und die beiden CI-Schritte aus D1 (Deno-Tests und „erzeugte Dateien aktuell“, „startbestand-keys.csv wächst nur“). Abweichungen und Ergänzungen, jeweils zur Prüfung im PR:
- Branch:
feature/2a-katalog-datenmodellstattfeature/2-katalog-fundament(Vorgabe des Auftrags). - CLI-Annahmen (R11) geprüft mit Supabase CLI 3.0.0, Ergebnis in
docs/katalog-pflege.md2.7: Reihenfolge undbegin/commitfunktionieren; ein Fehler rollt die ganze Datei zurück (Exit 1); ein Pfad ohne Treffer ist nur eine Warnung (Exit 0). Die erzeugten Dateien enthalten keinbegin/commit. - Nicht in 2a:
games_visibility_immutable(2c, wie in 3.4). - API-Hinweis für das Frontend:
catalog_gamesist über PostgREST nicht schreibbar (Fehler 55000 „cannot insert/update view“, zusätzlich fehlen die Rechte). Clients dürfen beim Anlegen oder Ändern vongameskeineid,created_atundupdated_atmehr senden (42501; bisher setzte der Triggerset_updated_atdiese Werte nur bei UPDATE stillschweigend zurück, bei INSERT wurdenidundcreated_atvom Client übernommen). EintoJson()eines Modells darf deshalb nicht unverändert aninsert/updategehen, sondern nur die erlaubten Spalten (3.2); Merkpunkt für KAT-01 (#3) und KAT-04 (#13). Außerdem funktioniert deris_admin()-Zweig dergame_*_write-Policies für Katalogspiele faktisch nicht mehr (der Triggergame_links_mark_manualbraucht Schreibrecht aufmanual_fields; R8), relevant für QUE-08. Es gibt keine generierten Datenbanktypen im Repo. - Ergänzung
startbestand.sql: Ein Wächter am Anfang bricht ab, wenn ein Genre oder eine Mechanik aus der CSV in der Datenbank fehlt; sonst würde der Abgleich der Zuordnungen still Verknüpfungen löschen. - Ergänzung Testdaten:
supabase/catalog/testdata/(fiktives Vokabular und drei Testspiele) erzeugtsupabase/tests/support/*.inc, die06_catalog_foundation.test.sqlper\ireinbindet; so laufen Idempotenz undchecks.sqlim pgTAP-Test mit den echten erzeugten Skripten. game_links_mark_manual: prüft zuerst perselect, ob das Spiel ein Katalogspiel ist, und schreibt nur dannmanual_fields. Ohne diese Prüfung scheiterte jede Zuordnung zu einem privaten Spiel mit 42501, weil Nutzer kein Schreibrecht aufmanual_fieldshaben.- Werkzeug: Deno wird für das Skript gebraucht (lokal 2.9.8, in CI gepinnt über
DENO_VERSION). Keine externen Importe.
2026-11-03 – Umsetzung PR 2b (backend-dev)
Umgesetzt: B5 (Migration 20261103193928_catalog_merge.sql), B6 (pgTAP 07_catalog_merge.test.sql, 89 Tests), B7 (Runbook Abschnitt 5). Abweichungen und Ergänzungen, jeweils zur Prüfung im PR:
-
Branch:
feature/2b-katalog-zusammenfuehrenstattfeature/2-katalog-pflege(Vorgabe des Auftrags). -
Keine neue Tabelle:
00_schema.test.sqlbleibt unverändert (20 Tabellen). -
Ergebnis-JSON (festgelegt):
collection_items(ohne Kollision umgehängt),collection_conflicts,favorite_games,favorite_conflicts,plays,score_sheets,variants_copied,variants_mapped,variants_cleared;admin_merge_gamesergänztsource_id,target_id,bgg_id_transferred,source_transferred,chain_flattenedund bei Konfliktbgg_id_conflict/source_conflict.variants_clearedzählt Verweise (Sammlungseinträge und Partien, deren Edition geleert wurde), nicht Varianten; so erkennt der Admin und später #13, ob bei einem Nutzer eine Edition verloren ging. Beim Zusammenführen zweier Katalogspiele ist er immer 0.source_conflictenthält zusätzlichsource_synced_at(alle vier Werte der Gruppe bleiben erhalten). -
Zusätzliche Prüfung:
admin_merge_games(null, …)/(…, null)meldetmerge_game_not_found.admin_merge_gamessperrt beide Zeilen in fester Reihenfolge (for update), damit parallele Aufrufe nicht verklemmen. -
Variantenzuordnung ist ein jsonb-Wert in der Funktion, keine temporäre Tabelle (mehrere Aufrufe in einer Transaktion bleiben möglich). Der Namensabgleich läuft nur gegen die Zielvarianten, die vor dem Aufruf existierten; jede Quellvariante wird einzeln kopiert, auch wenn zwei Quellvarianten gleich heißen (wörtliche Auslegung von 3.3).
-
Kollisionsregel Spec-F1 füllt nur
note,acquired_onundvariant_id; die Felder aus SAM-02/SAM-04 gibt es noch nicht (Hinweis 9.5 bleibt). Eine leere Notiz (nulloder leerer Text) am Ziel gilt als leer. -
repoint_game_referencesprüft nicht, obp_fromexistiert (nur die beiden Fehler aus 3.3);admin_merge_gamesprüft es. Der Fall „privater Eintrag auf Katalogspiel“ ist in 2b nur direkt gegenrepoint_game_referencesgetestet;admin_resolve_reportkommt mit 2c. -
Schritt 4 aus 3.3 (
game_reports.target_game_idumhängen) entfällt bis 2c, wie vorgesehen. -
Hinweis für 2c (B9): Sobald
game_reportszwei Fremdschlüssel aufgamesbekommt, wird der Wächter in07rot. Die erwartete Liste in07muss dort umgame_reports.target_game_id(behandelt) undgame_reports.provisional_game_id(bewusst ausgelassen) erweitert werden, nicht nur der Wächter in08. -
Runbook: Alle SQL-Bausteine aus Abschnitt 5 laufen wörtlich in
07(einmalige Prüfung per Skript; keine CI-Prüfung). Abschnitt 5 enthält die Duplikatsuche, eine Vorschau (Verweise, Doppelbesitz), den Aufruf, die Nachprüfung, die Regeln, die Schlüssel des Ergebnis-JSON und die Fehlertexte. -
Nachtrag Review 2b: Tests ergänzt (Spec-F1-Edition am Ziel,
variants_clearedim Kollisionszweig, gleichnamige Quellvarianten, zweiter Fremdschlüssel-Wächter aufgame_variants, unbeteiligte Spiele, Zielnull); Runbook um Trockenlauf (begin; …; rollback;in einem Lauf, nur dieser Baustein läuft nicht wörtlich in pgTAP), Ketten-Nachprüfung und Nachschlagen vonmerged_into_idergänzt. Der Null-Check inadmin_merge_gamesist ein äquivalenter Mutant (das späterenot foundliefert denselben Fehler) und bleibt als ausdrückliche Absicherung. Offen für Chris: Verweise, die nach dem Merge noch auf die Quelle entstehen (W3). -
Nachtrag W3 (Chris): Nachträgliche Verweise auf eine bereits zusammengeführte Quelle sind eine bekannte Einschränkung, kein neuer Code. Runbook 5 beschreibt Erkennung, Behebung (
repoint_game_referenceserneut alspostgres) und Nachprüfung;07testet das (Quelle bleibt ausgeblendet, Kollisionen, nachträglich entstandene Variante wird am Ziel kopiert).
2026-11-03 – Umsetzung PR 2c (backend-dev)
Umgesetzt: B8 (Migration 20261103195151_game_reports.sql), B9 (pgTAP 08_game_reports.test.sql, 160 Tests; 07 und 00 angepasst), B10 (Runbook Abschnitt 6). Abweichungen und Ergänzungen, jeweils zur Prüfung im PR:
- Branch:
feature/2c-katalog-meldungenstattfeature/2-katalog-meldungen(Vorgabe des Auftrags), gestapelt auffeature/2b-katalog-zusammenfuehren. - Zusätzliche Indizes auf beide Fremdschlüssel nach
games(Regel: Index auf jeden Fremdschlüssel).provisional_game_idist eindeutig (where provisional_game_id is not null, 1:1: ein vorläufiger Eintrag gehört zu höchstens einer Meldung),target_game_idein einfacher Index. - Zusatzfehler
report_provisional_not_privateinadmin_resolve_report: Ist der vorläufige Eintrag kein privates Spiel (Datenfehler per Studio), wird nichts umgehängt; sonst würderepoint_game_referencesein Katalogspiel wie beim Zusammenführen behandeln (Varianten kopieren und löschen). - Ergebnis-JSON
admin_resolve_report:report_id,status,target_id,provisional_game_id,repointed(bool). Nur beirepointed = truezusätzlich die Zähler ausrepoint_game_references.rejectedund Meldungen ohne vorläufigen Eintrag liefernrepointed = falseund keine Zähler. - Ergebnis-JSON
admin_merge_games: neuer Schlüsselreports_repointed(Anzahl umgehängter Meldungen, immer vorhanden). Dadurch mussten zwei Assertions in07(exaktes JSON) und das Beispiel im Runbook Abschnitt 5 angepasst werden. Rumpf vonadmin_merge_gamessonst unverändert (perdiffgegen 2b geprüft);revoke execute … from public, anon, authenticated, service_rolenach demcreate or replaceausdrücklich wiederholt. - Sperren:
admin_resolve_reportsperrt die Meldung und danach Ziel und vorläufigen Eintrag inid-Reihenfolge (for update), wieadmin_merge_games. Dass der vorläufige Eintrag existiert, sichert der Fremdschlüssel (on delete set null);repoint_game_referencesselbst prüft die Existenz vonp_fromweiterhin nicht. - Ziel wird auch bei
rejectedgeprüft (report_target_not_listed), wenn eines angegeben ist. Reihenfolge der Prüfungen wie in 3.4;p_report = nullistreport_not_found,p_status = nullistreport_status_invalid. - Trigger
games_visibility_immutableläuft vor der RLS-Prüfung: Ein Nutzer, dervisibilityändert, bekommtP0001 visibility_immutablestatt42501(API-Änderung für Clients; bestehende Tests in02bleiben grün, weil dortowner_idohne Spaltenrecht vorher mit 42501 scheitert). Es gibt weiterhin keine generierten Datenbanktypen im Repo. - RLS: zwei Lese-Policies (eigene Zeilen;
is_admin()für R4). Bewusst kein Test mitis_admin()als Zugang in R1, aber08dokumentiert, dassapp_metadata.role = 'admin'lesen, nicht schreiben kann, unduser_metadatanichts öffnet. - Tests und #13:
08prüft Schreibrechte je Spalte: beim Anlegen sindstatus,target_game_id,resolved_at,origin,created_at,updated_atnicht setzbar (nicht pauschal „kein Insert“), ein Update-Recht gibt es auf keine Spalte außer (ab #13)result_seen_at. Dazu „Delete verboten“ und „fremde Meldung nicht sichtbar/änderbar“. Einzelne Insert-/Update-Assertions im Abschnitt „Lesen, RLS“ (z. B. Anna legt eine Meldung für Ben an) gehen von fehlenden Insert-Rechten aus; 13a muss diese anpassen,08ändert sich mit #13 ohnehin. - Review 2c:
admin_resolve_reportprüft zusätzlich, dass der vorläufige Eintrag existiert und dem Meldenden gehört (report_provisional_not_private, deckt Katalogspiel, fremdes Spiel und fehlende Zeile ab); Test mit fremdem Spiel in08. Hinweise:service_rolehat Supabase-Standardrechte auf Tabellen (Krit. 9 gilt füranon/authenticated);P0001→ HTTP 400 in PostgREST; der Fremdschlüssel-Wächter existiert in07und08: #3 (game_alt_titles.game_id) und jede weitere Tabelle mit Verweis aufgamesmuss beide anpassen. Optionaler Merkpunkt (nicht gebaut): ein Check-Trigger, dasstarget_game_idauf ein Katalogspiel zeigt (analoggames_merged_into_catalog); heute stellt das nuradmin_resolve_reportsicher. - Spec-F6 (Chris, Option A) nachträglich umgesetzt: Spalte
game_reports.editions_cleared integer not null default 0 check (>= 0);admin_resolve_reportsetzt sie im Abschluss-Update ausvariants_cleared(sonst 0). Die Migration von 2c wurde dafür in place ergänzt (noch nicht gemergt). Tests in08: Spalte, Default, Check, Wert 1 bei geleerter Edition (Meldung …01), 0 beimergedohne geleerte Edition, beirejectedund ohne Umhängen, keine Insert-/Update-Rechte (Mutanten erkannt). Nicht gebaut:result_seen_at(kommt mit 13a samtupdate (result_seen_at), Insert-Recht und -Policy; 13a setzteditions_clearedinadopt_report_targetgenauso). - Fremdschlüssel-Wächter: erweiterte Liste in
07und in08(game_reports.target_game_idbehandelt,game_reports.provisional_game_idbewusst ausgelassen).00_schema.test.sql: 21 Tabellen. - Runbook Abschnitt 6: Alle SQL-Bausteine laufen wörtlich in
08(einmalige Prüfung per Skript, keine CI-Prüfung). Der Trockenlauf-Baustein aus 2b gilt entsprechend (begin; …; rollback;in einem Lauf). - Mutationsprüfung: 25 Mutanten (u. a. Trigger entfernt/ohne
owner_id, Grants, Policies, RLS aus, Fremdschlüssel-Aktionen, N12a-Index, Funktionsrumpf-Änderungen) gegen08; alle werden erkannt. Bei der Prüfung fiel ein Scheintest auf (Löschen eines Ziels scheiterte an anderen Verweisen), jetzt mit eigenem Ziel. Nicht erkennbar: das Weglassen des wiederholtenrevokeallein (Postgres behält die Rechte beicreate or replace); erkannt wird stattdessen ein ausdrücklichesgrant execute.
2026-11-04 – Nachtrag: Entscheidung Chris (Option B), ändert Spec-F3
Entscheidung von Chris am 2026-11-04 (im Rahmen von KAT-04 PR 13a): Es gibt kein „abgelehnt mit Verweis“ mehr. Wer auf einen vorhandenen Eintrag verweisen will, setzt Status merged (hängt direkt um, ohne Aktion des Nutzers). rejected gibt es nur ohne Ziel.
- Spec-F3 (Umhängen bei „zusammengeführt“ / „abgelehnt mit Verweis“): Der erste Teil (Option A:
mergedhängt um) bleibt; der zweite Teil („abgelehnt mit Verweis“ speichert nur den Verweis) entfällt. - 3.4 / 4.4 (
admin_resolve_report): percreate or replace(Migrationgame_reports_user, PR 13a) um die Prüfungrejectedmit Ziel →report_reject_has_target(P0001) ergänzt, sonst unverändert; zusätzlich der Checkgame_reports_rejected_no_targetangame_reports. Das Ziel wird beirejectedalso nie gespeichert. - Krit. 5 / Hinweis in der App: nur noch pending / approved / merged / rejected (ohne Ziel); kein Wechsel-Angebot und kein
adopt_report_target(siehedocs/specs/13-fehlendes-spiel-melden.md, Spec-F10). - Runbook
docs/katalog-pflege.mdAbschnitt 6 und08_game_reports.test.sqlsind entsprechend angepasst (Fixtures „rejected mit Ziel“ →merged).