From 6dd5854eab070466ab03ed0bdd9bbce80f88dcbe Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Tue, 3 Nov 2026 14:02:17 +0000 Subject: [PATCH] docs(catalog): address re-review of KAT-05 spec and record clarification N15 Visibility is immutable, variant copying is derived from visibility, startbestand idempotency and vocabulary handling are defined, and the edition deviation from KAT-04 criterion 4 is recorded as N15. Refs #2 Co-Authored-By: Claude Sonnet 5.5 --- docs/specs/2-katalog-daten.md | 194 +++++++++++++++++++++------------- requirements/r1-klaerungen.md | 6 ++ 2 files changed, 125 insertions(+), 75 deletions(-) diff --git a/docs/specs/2-katalog-daten.md b/docs/specs/2-katalog-daten.md index f115555..356eddd 100644 --- a/docs/specs/2-katalog-daten.md +++ b/docs/specs/2-katalog-daten.md @@ -3,14 +3,14 @@ | | | |---|---| | Issue | #2 (KAT-05) | -| Status | freigegeben (Chris, 2026-11-03); Spec-F1–Spec-F9 entschieden wie empfohlen (§ 9.2); Review PR #51 eingearbeitet (§ 10), Designwahlen mit „bitte bestätigen“ markiert (§ 9.3) | +| 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 warten auf Einzelbestätigung durch Chris | | 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), `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 | +| 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). +**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 @@ -32,14 +32,15 @@ Ein Import-Job (BGG oder eine andere Quelle) ist **nicht** Teil dieser Spec. Es | 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. 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 `_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). **Bitte bestätigen.** | 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.4); im Startbestand gibt es ohnehin keine Quelle. | +| 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). **Bitte bestätigen (§ 9.3, 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. | 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, copy_variants)`. **Varianten werden nur beim Zusammenführen zweier Katalogspiele kopiert; beim Umhängen eines privaten Eintrags nie** (Entscheidung Chris, B1); dort wird die Edition auf eine gleichnamige Variante des Ziels abgebildet, sonst geleert. | 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. | +| 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 @@ -61,13 +62,15 @@ Ein Import-Job (BGG oder eine andere Quelle) ist **nicht** Teil dieser Spec. Es - **Krit. 7 gilt als erfüllt**, wenn die Prüfabfrage `supabase/catalog/checks.sql` auf 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) | – | +| 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. @@ -75,8 +78,8 @@ Krit. 3 hat keinen Anteil am Datenmodell und wird vollständig mit dem Import sp **Relevante Querschnittskriterien** - **Sicherheit der Daten Kriterium 2:** Katalog ohne Anmeldung lesbar (`catalog_games`, `catalog_sources`, `publishers` für `anon`), nie durch Nutzer veränderbar (O6). -- **Sicherheit der Daten Kriterium 1 und 3:** `game_reports` enthält Nutzerdaten; nur der Meldende liest seine Meldungen, pgTAP prüft fremden Zugriff. -- **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. +- **Sicherheit der Daten Kriterium 1 und 3:** `game_reports` enthä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. @@ -99,12 +102,13 @@ Krit. 3 hat keinen Anteil am Datenmodell und wird vollständig mit dem Import sp | 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/_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`. +**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)** @@ -141,7 +145,7 @@ Die Migration fügt eine Zeile ein: `key = 'bgg'`, `name = 'BoardGameGeek'`, `at |---|---|---| | `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` | +| `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 | @@ -152,6 +156,8 @@ Weitere Checks: `(catalog_source_id is null) = (source_ref is null)`; `source_ur 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 | @@ -183,7 +189,7 @@ Beide mit `id`, `created_at`, `updated_at` und Trigger `set_updated_at`. Ein Spi - `image_path` = gleiche Regel mit `'image_path'` und `s.images_allowed`, - `source_key`, `source_name`, `source_attribution_text`, `source_attribution_url`, `source_url` aus `catalog_sources` (Left Join). - Grants laut Tabelle oben. Weil `security_invoker` gesetzt ist, gelten die RLS-Regeln von `games` und `catalog_sources` für den Aufrufer. -- **Grenze (E6):** Die View ist für Krit. 6 nur die zweite Sicherung. `games` bleibt für `anon` tabellenweise lesbar; Daten ohne Anzeige-Erlaubnis dürfen deshalb gar nicht erst gespeichert werden (Pflicht für die Import-Spec, 9.4). +- **Grenze (E6):** Die View ist für Krit. 6 nur die zweite Sicherung. `games` bleibt für `anon` tabellenweise 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 `games` bleibt für eigene private Spiele und für bestehende Verweise (Sammlung, Partien) nötig. **Rechte auf `games` neu schneiden** (Krit. 9) @@ -198,18 +204,29 @@ grant update (title, description, description_locale, year, min_players, max_pla 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. +`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/_catalog_merge.sql` (R1.2) -**Interne Funktion `public.repoint_game_references(p_from uuid, p_to uuid, p_copy_variants boolean) returns jsonb`** +**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. Zuerst entsteht eine temporäre Zuordnung „alte Variante → neue Variante“ (`variant_map`): +`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. -| Fall | `p_copy_variants` | Zuordnung der Varianten | +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`, beide öffentlich) | `true` | 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 (`lower(trim(name))`); dann wird auf diese abgebildet. Danach werden die alten Varianten gelöscht. | -| **Privaten Eintrag auf ein Katalogspiel umhängen** (`admin_resolve_report`) | `false` | **Es wird nichts kopiert** (Entscheidung Chris, B1): Hat `p_to` eine Variante mit gleichem Namen (`lower(trim(name))`), 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). | +| **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. @@ -218,13 +235,13 @@ Danach, in dieser Reihenfolge: 1. **`collection_items`:** Hat derselbe Nutzer schon einen Eintrag zu `p_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 und `variant_id`, wobei `variant_id` **immer über `variant_map`** übersetzt wird, nie direkt übernommen); bei unterschiedlicher Liste gilt `owned` vor `wishlist` vor `shopping` (Klärung 34); danach wird der Quell-Eintrag gelöscht. Ohne Kollision: `game_id := p_to`, `variant_id := variant_map(variant_id)`. 2. **`favorite_games`:** Hat der Nutzer `p_to` schon als Favorit, wird die Zeile zu `p_from` gelöscht (die Lücke in `position` ist zulässig). Sonst `game_id := p_to`. 3. **`plays`:** `game_id := p_to`, `variant_id := variant_map(variant_id)`. -4. **`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.4). -5. Nur bei `p_copy_variants = true`: alte Varianten von `p_from` löschen (sie sind jetzt unreferenziert). +4. **`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). +5. Nur wenn beide Katalogspiele sind: alte Varianten von `p_from` löschen (sie sind jetzt unreferenziert). 6. Zuordnungen (Genre, Mechanik, Kategorie, Verlag) von `p_from` bleiben 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“): 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.4): Der Ergebnishinweis in der App sollte in diesem Fall sagen, dass die Edition neu gewählt werden muss; die Rückgabe von `repoint_game_references` (`variants_cleared`) bzw. ein vergleichbarer Hinweis an der Meldung macht das erkennbar. +**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`** @@ -241,8 +258,8 @@ Rückgabe: `jsonb` mit Zählern je Tabelle, z. B. `{"collection_items": 3, "coll Ablauf: -1. `repoint_game_references(p_source, p_target, true)`. -2. **BGG-Kennung und Quelle übernehmen** (W6, **bitte bestätigen**): Hat das Ziel kein `bgg_id`, aber die Quelle, wird der Wert übertragen; ebenso `catalog_source_id`/`source_ref`/`source_url`/`source_synced_at` als 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 `bgg_id_conflict` bzw. `source_conflict` zur manuellen Klärung. Begründung: Sonst würde der BGG-Abgleich (SAM-08) später den ausgeblendeten Quell-Eintrag treffen. Die übertragenen Felder werden am Ziel wie jede Admin-Änderung in `manual_fields` vermerkt, soweit sie in der Schlüsselliste stehen (`bgg_id` und `source_*` stehen nicht darin). +1. `repoint_game_references(p_source, p_target)` (beide Katalog → Varianten werden kopiert). +2. **BGG-Kennung und Quelle übernehmen** (W6, **bitte bestätigen**): Hat das Ziel kein `bgg_id`, aber die Quelle, wird der Wert übertragen; ebenso `catalog_source_id`/`source_ref`/`source_url`/`source_synced_at` als 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_id` und `source_*` stehen nicht in der `manual_fields`-Schlüsselliste. 3. `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`). 4. Ab 2c zusätzlich: `game_reports.target_game_id` von `p_source` auf `p_target`. @@ -252,6 +269,8 @@ Die Quelle wird **nicht** gelöscht: Ihre Shuffle-ID bleibt gültig, die App lei ### 3.4 Migration 2c `supabase/migrations/_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 | @@ -296,19 +315,19 @@ Rechte und Rollenprüfung wie `admin_merge_games`. Ablauf: `status`, `target_game_id`, `resolved_at = now()` setzen. Hat die Meldung einen vorläufigen Eintrag, dann (Spec-F3) -- bei `approved` und bei `merged`: `repoint_game_references(provisional, target, false)` (keine Varianten kopieren, B1); der vorläufige Eintrag bekommt `merged_into_id = target`, `hidden_at = now()` (KAT-04 Krit. 4, mit der Einschränkung zur Edition aus 3.3), +- bei `approved` und bei `merged`: `repoint_game_references(provisional, target)` (privat → Katalog, keine Varianten kopiert, RB1); der vorläufige Eintrag bekommt `merged_into_id = target`, `hidden_at = now()` (KAT-04 Krit. 4, mit der Einschränkung zur Edition aus 3.3), - bei `rejected` mit Ziel: nur `target_game_id` wird 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 `rejected` ohne 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`). -„Freigeben“ heißt in R1: Der Admin legt im Studio zuerst den Katalogeintrag an (bzw. wählt ihn aus) und ruft dann die Funktion mit `approved` auf. Unterschied zu `merged`: `approved` = der Eintrag ist wegen dieser Meldung neu entstanden, `merged` = er war schon vorhanden. +„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. +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, 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. +`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 @@ -318,9 +337,10 @@ Die Migration ersetzt `admin_merge_games` per `create or replace`, sodass beim Z |---|---|---|---| | `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` | Trigger | 2a | automatisch | +| `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` | @@ -365,21 +385,27 @@ Keine Edge Functions, keine RPCs für die App in dieser Spec. - Name je Objekt: Spiel `game:`, Genre `genre:`, Mechanik `mechanic:` (`vocab_key` ist 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 über `lower(name)`. +**Vokabular** + +- Maschinenlesbare Quelle ist `supabase/catalog/vocabulary.csv` (Spalten `kind` = `genre`/`mechanic`, `vocab_key`, `name_de`, `name_en`); das Skript erzeugt daraus `vocabulary.sql`. +- **Kollision mit im Studio angelegten Begriffen** (gleicher `name_de` oder `name_en` unter anderer ID): `vocabulary.sql` fügt mit `on conflict do nothing` (ohne Konfliktziel, fängt ID- und Namenskonflikte) ein; es entsteht kein Fehler 23505. Der vorhandene Begriff gewinnt. `startbestand.sql` löst Genres und Mechaniken deshalb **über `name_de`** auf, nicht über die abgeleitete UUID. `checks.sql` meldet solche Begriffe als Information („Begriff mit abweichender ID“), damit Chris sie prüfen kann. + **Idempotenz von `startbestand.sql`** 1. Verlage: `insert into publishers (name) … on conflict ((lower(name))) do nothing`. -2. Spiele: `insert … on conflict (id) do update set` alle Fakten-Spalten, `description`, `description_locale`, `hidden_at` (nur nach Regel 5 unten). -3. Zuordnungen je Startbestandsspiel neu aufbauen: `delete from game_genres / game_mechanics / game_publishers where game_id in ()`, danach `insert … on conflict (game_id, genre_id) do nothing` (bzw. `mechanic_id`, `publisher_id`). Kategorien (`game_categories`) fasst die Datei nicht an. -4. **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 vorhandener `key` in der CSV, bricht das Skript ab („Schlüssel entfernt oder umbenannt“). Umbenennen ist nicht erlaubt; ein falscher Schlüssel bleibt stehen, nur der Titel wird korrigiert. +2. 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_at` und `manual_fields` bleiben stabil). +3. 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 mit `on 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. +4. **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 vorhandener `key` in der CSV, bricht das Skript ab („Schlüssel entfernt oder umbenannt“). Zusätzlich prüft ein CI-Schritt gegen `main`, dass jede Zeile von `startbestand-keys.csv` aus `main` unverändert erhalten ist (die Liste wächst nur; D1). Umbenennen ist nicht erlaubt; ein falscher Schlüssel bleibt stehen, nur der Titel wird korrigiert. 5. **Spiel entfernen:** Zeile bleibt in der CSV, Spalte `hidden` = `ja` → das Skript erzeugt `hidden_at = coalesce(hidden_at, now())`. Es wird nichts gelöscht (Verweise aus Sammlungen bleiben gültig). +6. **`hidden` leer:** Die Datei fasst `hidden_at` nicht an. Ein im Studio ausgeblendetes Startbestandsspiel bleibt also ausgeblendet; wieder einblenden geht nur im Studio (`hidden_at = null`). **Dateien** -- `supabase/catalog/vocabulary.sql`: Genres und Mechaniken (de/en) mit IDs nach UUID v5, `insert … on conflict (id) do update set name_de = excluded.name_de, name_en = excluded.name_en`. Inhalt: Vorschlag durch Agents (B3a), gültig erst nach Freigabe durch Chris (C0). `seed.sql` legt danach keine kollidierenden Begriffe mehr an (Unique auf `name_de`/`name_en`). +- `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.sql` legt 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 die CSV und erzeugt `startbestand.sql`, `startbestand-keys.csv` und `checks.sql`. Die erzeugten Dateien werden nie von Hand bearbeitet. +- `supabase/catalog/build_startbestand.ts`: Deno, nur Standardbibliothek; prüft beide CSV-Dateien und erzeugt `vocabulary.sql`, `startbestand.sql`, `startbestand-keys.csv` und `checks.sql`. Die erzeugten Dateien werden nie von Hand bearbeitet. -**CSV-Format** +**CSV-Format `startbestand.csv`** | Spalte | Pflicht | Inhalt | |---|---|---| @@ -394,12 +420,12 @@ Keine Edge Functions, keine RPCs für die App in dieser Spec. | `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) | +| `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`. Es gibt **keine** Spalte für Cover, Quelle oder BGG-Kennung (Krit. 10). +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) @@ -408,7 +434,8 @@ Das Skript bricht mit Zeilennummer und Grund ab bei: fehlender Pflichtangabe, un - SB1: Anzahl `sb`-IDs in `catalog_games` ≥ 180, - SB2: `sb`-Spiele mit `year`, `min_players`, `max_players`, `min_minutes`, `max_minutes` oder `min_age` leer, oder ohne Verlag, Genre oder Mechanik = 0, - SB4: `sb`-Spiele ohne `description` oder mit `description_locale` ≠ `de` = 0, - - SB6: `sb`-Spiele mit `image_path`, `catalog_source_id` oder `bgg_id` = 0. + - SB6: `sb`-Spiele mit `image_path`, `catalog_source_id` oder `bgg_id` = 0, + - Information: Vokabularbegriffe mit abweichender ID (siehe Vokabular). - 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). @@ -420,11 +447,11 @@ 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; CSV bearbeiten → Skript → Commit; Schlüssel nie umbenennen, Entfernen nur über `hidden`; 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 | +| 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('', '');`, Ergebnis-JSON lesen (Kollisionen nach Spec-F1, Varianten, `bgg_id_conflict`), Fehlertexte | -| Meldungen bearbeiten | 2c | offene Meldungen abfragen (älteste zuerst), Ähnliche im Katalog suchen, Entscheidung: freigeben (erst Katalogeintrag anlegen, dann `admin_resolve_report(…, 'approved', '')`), zusammenführen (`'merged'`), ablehnen mit oder ohne Verweis (`'rejected'`); Hinweis: Editionen werden nicht kopiert, ggf. passende Variante am Katalogeintrag vorher anlegen; Fehlertexte | +| Zusammenführen | 2b | Duplikat finden (normalisierter Titel + Jahr), `select public.admin_merge_games('', '');`, 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', '')`. **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. @@ -439,7 +466,7 @@ KAT-05 hat in R1 keine eigenen Screens (keine Admin-UI, Klärung 44). Die App-An | 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) | KAT-04 (#13) | +| 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/`). @@ -462,9 +489,9 @@ Werkzeuge: pgTAP (`supabase test db`), Deno-Tests für das Erzeugungsskript, `fl **Neue pgTAP-Dateien** -- `06_catalog_foundation.test.sql` (2a): Spalten, Checks, Grants, Spaltenrechte, Trigger `manual_fields` (inkl. Import-Ausnahme mit einer im Test angelegten Rolle `catalog_importer`), View `catalog_games`, `catalog_sources`, Verlage, Ausblenden, Runbook-Bausteine aus 2a. -- `07_catalog_merge.test.sql` (2b): `repoint_game_references` (beide Variantenfälle), `admin_merge_games`; Fremdschlüssel-Wächter. -- `08_game_reports.test.sql` (2c): Tabelle, RLS, `admin_resolve_report`. +- `06_catalog_foundation.test.sql` (2a): Spalten, Checks, Grants, Spaltenrechte, Trigger `manual_fields` (inkl. Import-Ausnahme mit einer im Test angelegten Rolle `catalog_importer`), Trigger `games_merged_into_catalog`, View `catalog_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`, Trigger `games_visibility_immutable`, Rechte von `admin_merge_games` nach dem `create or replace`, Konto-Löschen nach Freigabe. - `00_schema.test.sql` wird 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. @@ -483,12 +510,12 @@ Legt eine spätere Story eine neue Tabelle mit Verweis auf `games` an, wird der | 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. `07`: Zusammenführen zweier Katalogspiele kopiert Varianten (gleichnamige werden abgebildet) 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. `bgg_id_conflict` gemeldet, 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 löschen → Meldungen weg; Fremdschlüssel-Wächter erweitert | B8, B9 | +| 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 einer Fixture-CSV die erwartete SQL, Schlüsselliste und `checks.sql` (Snapshot); gleicher `key` → gleiche UUID (Namespace aus 4.3); entfernter `key` → Fehler; `hidden = ja` → `hidden_at`; `facts_source` landet nicht in der SQL | – | CI: `supabase db reset` mit `vocabulary.sql` + `startbestand.sql` läuft durch, zweimaliges Einspielen ergibt denselben Stand (Idempotenz, inkl. Verlag, den es schon mit anderer Schreibweise gibt); CI-Schritt „erzeugte Dateien aktuell“ (`git diff --exit-code`); `checks.sql` läuft ohne Fehler | – | B3, T1 | +| 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); `game_reports`: Nutzer B liest A's Meldungen nicht, niemand außer `postgres` ändert oder löscht Meldungen, `anon` liest nichts | B2, B6, B9 | +| 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 | @@ -504,15 +531,15 @@ Alle PRs: `Refs #2` (E1). | # | Agent | Aufgabe | abhängig von | |---|---|---|---| -| B1 | backend-dev | Migration `_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`) und `game_links_mark_manual` (insert/update/delete), View `catalog_games`, Spaltenrechte auf `games` (ohne `id`), RLS, Policies und Grants laut Grant-Tabelle | – | +| B1 | backend-dev | Migration `_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.sql` mit IDs nach UUID v5; 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, Schlüsselliste, Idempotenz-Regeln 1–5, UUID-v5-Namespace, Erzeugung von `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 | +| 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, entfernter Schlüssel); CI-Schritt „erzeugte Dateien aktuell“ mit devops abstimmen | B2, B3 | +| 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, Schritt „erzeugte Dateien aktuell“; Release-Checkliste um `checks.sql` (SB1: mindestens 180) ergänzen | B3 | +| 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 | @@ -520,17 +547,17 @@ Alle PRs: `Refs #2` (E1). | # | Agent | Aufgabe | abhängig von | |---|---|---|---| -| B5 | backend-dev | Migration `_catalog_merge.sql` nach 3.3: `repoint_game_references` (beide Variantenfälle, Kollisionsregel Spec-F1 über `variant_map`), `admin_merge_games` (Rollenprüfung, Übertragung `bgg_id`/`source_*`), Rechte (`revoke execute` inkl. `service_role`) | 2a gemergt | -| B6 | backend-dev | pgTAP `07_catalog_merge.test.sql` inkl. Kollisionsfällen, Variantenfällen, `bgg_id`-Übertragung und -Konflikt, Fremdschlüssel-Wächter | B5 | +| B5 | backend-dev | Migration `_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 `_game_reports.sql` nach 3.4: 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` erweitert | 2b gemergt | -| B9 | backend-dev | pgTAP `08_game_reports.test.sql`; 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 | B8, B9 | +| B8 | backend-dev | Migration `_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 @@ -541,7 +568,7 @@ R1.2 B5 → B6 → B7 (2b) → B8 → B9 → B10 (2c) → #13 KAT-04 Umsetz 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.sql` wird erst nach C0 endgültig. 2c muss vor der Umsetzung von #13 gemergt sein (E12). Merge nur durch Chris. +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 @@ -549,16 +576,16 @@ B3 kann mit einem vorläufigen Fixture-Vokabular für die Tests beginnen; `vocab | # | 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 | +| 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 (Upsert, Zuordnungen neu aufgebaut) | Entschieden (Spec-F6): Datei ab Go-live eingefroren, wird nicht mehr eingespielt | +| 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.4 | +| 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 (B1) | KAT-04 Krit. 4 nur eingeschränkt erfüllt (Edition) | Bewusst entschieden (Chris); Runbook: passende Variante am Katalogeintrag vorher anlegen; Hinweis in der App (#13) | +| 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) @@ -577,27 +604,31 @@ Alle neun Fragen hat Chris am 2026-11-03 entschieden („Alle wie empfohlen“). | 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 dem Review zu PR #51 (2026-11-03): +Weitere Entscheidungen von Chris aus den Reviews zu PR #51 (2026-11-03): | # | Thema | Entscheidung | Wirkung | |---|---|---|---| -| B1 | 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. | E11, 3.3, 3.4, R10, 7.1, 9.4 (#13) | +| 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 – bitte bestätigen +### 9.3 Designwahlen aus dem Review – bitte einzeln bestätigen + +Chris bestätigt diese Punkte einzeln; bis dahin bleiben sie unverändert. | # | Wahl | Begründung | Alternative | |---|---|---|---| | W1 | 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 | `admin_merge_games` überträgt `bgg_id` und `source_*` auf das Ziel, wenn es keine hat, leert sie an der Quelle und meldet Konflikte (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 | +| W6 | `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 | 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 | 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 | 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 | `merged_into_id`, `target_game_id`, `catalog_source_id`: `on delete restrict`. | Katalogspiele und Quellen werden ausgeblendet, nicht gelöscht; versehentliches Löschen fällt sofort auf. | `on delete set null` | +| H7 | `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 und Review PR #51 eingearbeitet. Bitte die Freigabe und die Bestätigung der Designwahlen aus 9.3 als Kommentar im Issue #2 festhalten (Chris). +- Spec freigegeben am 2026-11-03, Spec-F1–Spec-F9 sowie Review und Re-Review PR #51 eingearbeitet. Chris hält die Freigabe und die Einzelbestätigungen aus 9.3 im Issue #2 fest (Koordination über den Orchestrator). - 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). @@ -608,12 +639,12 @@ Weitere Entscheidungen von Chris aus dem Review zu PR #51 (2026-11-03): - **KAT-01 (#3):** Suche über `catalog_games` (bzw. eine RPC darauf); Tabelle für alternative Titel (Klärung 3) mit neuem `manual_fields`-Schlüssel; Spalte `alt_titles` der Startbestands-CSV auswerten. - **KAT-02 (#4):** Weiterleitung bei `merged_into_id`; bei `hidden_at` Detailseite weiter anzeigen mit Hinweis „nicht mehr im Katalog“ (Spec-F2); Quellenangabe aus `source_*` (KAT-02 Krit. 5); Sprachhinweis über `description_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_key` der 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** (B1): 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). **Bitte als Hinweis ins Issue #13 übernehmen.** +- **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.** - **PAR-04 (#18):** Sobald ein Wertungsbogen je Spiel und Nutzer erzwungen wird, Kollisionsregel in `repoint_game_references` ergä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_importer` mit `set local shuffle.catalog_import = 'on'` (E4); pflegt `catalog_source_id`, `source_*`, `source_synced_at`; **speichert Cover und Texte einer Quelle ohne Anzeige-Erlaubnis gar nicht** (E6, Pflicht); ignoriert beim Abgleich über `bgg_id` ausgeblendete und zusammengeführte Einträge; legt Import-Meldungen mit `origin = 'import'` an (N12, N12a); bekommt gezielt Rechte, die in R1 allen API-Rollen entzogen sind. +- **SAM-08 (#25) und Import-Spec:** Import läuft als Rolle `catalog_importer` mit `set local shuffle.catalog_import = 'on'` (E4). **Achtung:** Läuft der Import über eine `security definer`-Funktion, ist `current_user` deren Eigentümer, nicht `catalog_importer`; die Ausnahme greift dann nicht. Der Import muss sich deshalb direkt als `catalog_importer` anmelden oder Funktionen nutzen, die `catalog_importer` gehören. Weiter: pflegt `catalog_source_id`, `source_*`, `source_synced_at`; **speichert Cover und Texte einer Quelle ohne Anzeige-Erlaubnis gar nicht** (E6, Pflicht); ignoriert beim Abgleich über `bgg_id` ausgeblendete und zusammengeführte Einträge; legt Import-Meldungen mit `origin = '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 auf `games` und Trigger-Rechte berücksichtigen (R8). -- **KTO-05 (#23):** `game_reports` wird per `on delete cascade` mit dem Profil gelöscht; im Export enthalten. Vorläufige Spiele mit `merged_into_id` auf ein Katalogspiel werden mit dem Konto gelöscht (sie sind die verweisende Seite, `restrict` greift nicht). +- **KTO-05 (#23):** `game_reports` wird per `on delete cascade` mit dem Profil gelöscht; im Export enthalten. Vorläufige Spiele mit `merged_into_id` auf ein Katalogspiel werden mit dem Konto gelöscht (sie sind die verweisende Seite, `restrict` greift nicht; Test in `08`). ## 10. Änderungsprotokoll @@ -629,12 +660,25 @@ Chris hat die Spec freigegeben und alle Fragen wie empfohlen entschieden. Eingea ### 2026-11-03 – Review PR #51 -- **B1 (Chris):** Beim Umhängen eines privaten Eintrags werden keine Varianten kopiert; Abbildung auf gleichnamige Zielvariante, sonst `null`; `repoint_game_references` mit Parameter `p_copy_variants`; Spec-F1-Füllregel über `variant_map`; Auswirkung auf KAT-04 Krit. 4 (R10, 9.5). +- **RB1 (Chris):** Beim Umhängen eines privaten Eintrags werden keine Varianten kopiert; Abbildung auf gleichnamige Zielvariante, sonst `null`; Spec-F1-Füllregel über `variant_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 (bitte bestätigen). - **W2:** Grant-Tabelle je Objekt in 3.2. -- **W4:** Idempotenz von `startbestand.sql` (Verlage über `lower(name)`, Zuordnungen neu aufbauen, Konfliktziele), unveränderliche Schlüssel mit Schlüsselliste, Entfernen über `hidden`, UUID-v5-Namespace. +- **W4:** Idempotenz von `startbestand.sql`, unveränderliche Schlüssel mit Schlüsselliste, Entfernen über `hidden`, UUID-v5-Namespace. - **W5:** `checks.sql` erzeugt, mit eingebetteten Startbestands-IDs, alle SB2-Felder, SB6 inkl. `bgg_id`, Beispielspiel ausgenommen. - **W6:** Übertragung von `bgg_id`/`source_*` beim Zusammenführen (bitte bestätigen). - **W7:** Begründung zum Import-Schalter korrigiert; Ausnahme an Rolle `catalog_importer` gebunden; Runbook „nie `set` ohne `local`“. -- **H1–H14:** `is_admin()`-Pfad aus den Funktionen gestrichen, `service_role` entzogen; Spaltenrechte ohne `id`; Trigger auch für Update, R8 ergänzt; Zuordnung der Kriterien zu PRs (Ausblenden und Runbook-Abschnitt nach 2a, pgTAP `06` für Runbook-Bausteine); Fremdschlüssel-Wächter mit `game_reports`; `on delete restrict` für `merged_into_id`, `target_game_id`, `catalog_source_id`; Umbenennung in SB1–SB8 und Spec-F1–Spec-F9, Klarstellung „F2“ in Krit. 2; Abhängigkeit #4 im Kopf; Hinweis zur Freigabe im Issue; Pooler und `/pg/` in der Zugangsvorgabe; CLI-Annahmen als unverifiziert markiert (R11); KI-Entwürfe nur aus Fakten (SB4); `terms_url`-Check, `description_locale` nur mit `description`, Fremdschlüssel `catalog_source_id`, README-Hinweis. +- **H1–H14:** `is_admin()`-Pfad aus den Funktionen gestrichen, `service_role` entzogen; Spaltenrechte ohne `id`; Trigger auch für Update, R8 ergänzt; Zuordnung der Kriterien zu PRs; Fremdschlüssel-Wächter mit `game_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ür `postgres`), Test in `08`; 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.md` festgehalten; Verweise in § 2, 3.3, R10, 9.5. +- **W-b:** Runbook: beim Freigeben nur Fakten aus den Meldungsfeldern übernehmen, nie `description`/`image_path` des vorläufigen Eintrags; Varianten am Katalogeintrag nur aus belegbaren Fakten anlegen. +- **W-c:** Parameter `p_copy_variants` gestrichen; Kopieren folgt aus der Sichtbarkeit; `repoint_game_references` prüft `p_from <> p_to` und sichtbares Katalogziel; Tests in `07`. +- **W4 a–e:** Upsert mit `is distinct from` und Abgleich der Zuordnungen, präzisierter Idempotenz-Test; Vokabular-Kollision ohne 23505, Auflösung über `name_de`; `vocabulary.csv` als Quelle; CI-Schritt „`startbestand-keys.csv` wächst nur“; `hidden` leer lässt `hidden_at` unverändert. +- Feste Auswahlregel bei mehreren gleichnamigen Zielvarianten (älteste, dann kleinste `id`), Test in `07`. +- 2c: `revoke` für `admin_merge_games` nach `create or replace` ausdrücklich wiederholt, Prüfung in `08`; Konflikt-JSON mit alten Werten. +- **H7:** Trigger `games_merged_into_catalog` (2a) begründet; Test in `08`: 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 definer` und `current_user` für die Import-Spec (9.5). +- Designwahlen in 9.3 unverändert, warten auf Einzelbestätigung durch Chris. diff --git a/requirements/r1-klaerungen.md b/requirements/r1-klaerungen.md index c3872d5..19a7d79 100644 --- a/requirements/r1-klaerungen.md +++ b/requirements/r1-klaerungen.md @@ -80,3 +80,9 @@ Stand: 2026-11-02 – **alle 48 Fragen beantwortet**. Als Nächstes werden Akzep | N12a | SAM-08 #25 | Deduplizierung der Import-Meldungen **je Nutzer** und BGG-ID; Meldungen verschiedener Nutzer bleiben getrennt. | | N8a | QUE-09 #29 | Kurzstatistik: Partienzahl im laufenden Monat, **Siegquote gesamt**. | | N3a | KTO-11 #28 | Bestätigt: Bei der Registrierung wird die aktive Oberflächensprache zur Profilsprache. | + +## Klärungen aus Spec-Reviews (ab 2026-11-03) + +| Nr | Story/Issue | Entscheidung | +|---|---|---| +| N15 | KAT-04 #13, KAT-05 #2 | **Edition beim Ersetzen eines vorläufigen Eintrags** (Meldung freigegeben oder zusammengeführt): Private Varianten (Editionen) des Nutzers werden **nicht** in den öffentlichen Katalog kopiert. Die Edition an Sammlungseintrag und Partien bleibt nur erhalten, wenn der Katalogeintrag eine gleichnamige Variante hat; sonst wird sie geleert, und die App weist darauf hin, sie neu zu wählen. Das ist eine bewusste **Abweichung von KAT-04 Kriterium 4** („ohne dass meine eigenen Angaben verloren gehen“), nur für die Edition. Begründung: Private Nutzerdaten sollen nicht ungeprüft in den öffentlichen Katalog gelangen (Datensparsamkeit, Katalogqualität). Entschieden von Chris am 2026-11-03 im Review zu PR #51. Verweise: `docs/specs/2-katalog-daten.md` § 3.3, Risiko R10, Hinweis für Issue #13 in § 9.5. |