docs(catalog): Spec für KAT-05 Katalogdaten (freigegeben) #51

Merged
cschlaefke merged 6 commits from docs/2-kat05-spec into main 2026-11-03 14:15:11 +00:00
Showing only changes of commit c0f832fd1d - Show all commits

View file

@ -3,7 +3,7 @@
| | |
|---|---|
| Issue | #2 (KAT-05) |
| Status | Entwurf, wartet auf Freigabe durch Chris |
| Status | freigegeben (Chris, 2026-11-03); Fragen F1–F9 entschieden wie empfohlen (§ 9.2) |
| Autor | architect |
| Stand | 2026-11-03 |
| Etappe | R1.1 (Teil 2a: Datenmodell, Rechte, Startbestand), R1.2 (Teil 2b: Admin-Pflege per Studio; Teil 2c: Meldungen bearbeiten); Import (Krit. 1, 3, 6, Quellenteil von 2) erst mit Lizenz |
@ -16,7 +16,7 @@ Der Katalog ist das Fundament aller anderen Stories: Suche (KAT-01), Detailseite
- 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 vorliegt, der nur Fakten enthält, ohne fremde Cover und fremde Beschreibungstexte (Klärung 24, N11),
- zum Start von R1 ein manuell gepflegter Startbestand von etwa 200 Spielen (mindestens 180, 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.
@ -27,13 +27,13 @@ Ein Import-Job (BGG oder eine andere Quelle) ist **nicht** Teil dieser Spec. Es
|---|---|---|
| E1 | **Ein Issue #2, drei PRs:** 2a (R1.1) Datenmodell, Rechte, Startbestand-Lieferweg, Platzhalter-Cover; 2b (R1.2) Zusammenführen und Runbook; 2c (R1.2) Meldungstabelle und Bearbeiten von Meldungen. 2a und 2b referenzieren `Refs #2`, 2c `Closes #2`. | Entspricht dem Etappenschnitt im Issue. 2a blockiert KAT-01 (#3), 2c muss vor der Umsetzung von KAT-04 (#13) gemergt sein. Eigene Issues würden nur Abhängigkeiten verschieben. |
| 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 (Joins liefern `null`, die `with check`-Klausel von `collection_items` schlägt beim Bearbeiten fehl). Ob ausgeblendete Einträge zusätzlich über die rohe API gesperrt werden sollen, ist Frage F2. |
| 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. Nur ein späterer Import setzt in seiner Transaktion `set local shuffle.catalog_import = 'on'` und wird dann nicht markiert. Zuordnungen (Genre, Mechanik, Kategorie, Verlag) 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). Der Import (Krit. 1) überschreibt später genau die Felder nicht, die in der Liste stehen. Eine Liste statt einer Boolean-Spalte je Feld hält das Schema klein; ein Check begrenzt die erlaubten Schlüssel. Wie der Startbestand markiert wird, ist Frage F4. |
| 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 (Joins liefern `null`, die `with check`-Klausel von `collection_items` schlägt beim Bearbeiten fehl). Entschieden (F2): Ausgeblendete Einträge bleiben per ID lesbar; ein eigener Status „gesperrt“ für rechtliche Fälle 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. Nur ein späterer Import setzt in seiner Transaktion `set local shuffle.catalog_import = 'on'` und wird dann nicht markiert. Zuordnungen (Genre, Mechanik, Kategorie, Verlag) 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). Der Import (Krit. 1) überschreibt später genau die Felder nicht, die in der Liste stehen. Eine Liste statt einer Boolean-Spalte je Feld hält das Schema klein; ein Check begrenzt die erlaubten Schlüssel. Entschieden (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 `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 (Quellenangabe). Die Doppelung bei BGG ist bewusst; die Konsistenz (`source_ref = bgg_id::text` bei Quelle `bgg`) stellt der Import sicher (Spec des Imports). Eine Quelle je Spiel reicht, bis es eine zweite Quelle gibt. |
| E6 | **Die View `catalog_games` setzt die Anzeige-Erlaubnis schon jetzt serverseitig um:** `description` und `image_path` erscheinen nur, wenn das Feld manuell gepflegt ist, das Spiel keine Quelle hat oder die Quelle die Anzeige erlaubt. | Vorbereitung von Krit. 6 ohne Mehraufwand im Client. Im Startbestand hat kein Spiel eine Quelle, die Regel wirkt also erst mit dem Import. |
| 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 (siehe 4.3), damit Chris die ~200 Spiele nicht zweimal anfassen muss. | Krit. 10 verlangt den Verlag im Startbestand. Alternative Titel und Erweiterungen sind für den Startbestand nicht Pflicht und werden in #3 bzw. #12 modelliert und getestet. |
| E8 | **Sprache des Kurztexts als Spalte `games.description_locale`** (`de`/`en`, optional), schon in 2a. | Der Startbestand füllt sie; KAT-02 Krit. 7 (Klärung 48) zeigt damit den Sprachhinweis. Die Sprache des Titels gehört zu den alternativen Titeln (#3). |
| E9 | **Startbestand ist keine Migration und nicht Teil von `seed.sql`**, sondern eine versionierte, 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 einmalig per Studio-SQL-Editor oder `psql` ein (Runbook). | 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. Wie die Datei entsteht (Pflegeformat), ist Frage F6. |
| E8 | **Sprache des Kurztexts als Spalte `games.description_locale`** (`de`/`en`, optional), schon in 2a. | Der Startbestand füllt sie (immer `de`, F5); KAT-02 Krit. 7 (Klärung 48) zeigt damit den Sprachhinweis. Die Sprache des Titels gehört zu den alternativen Titeln (#3). |
| E9 | **Startbestand ist keine Migration und nicht Teil von `seed.sql`**, sondern eine versionierte, 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 einmalig per Studio-SQL-Editor oder `psql` ein (Runbook). | 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. Entschieden (F6): Chris pflegt eine CSV, ein Skript erzeugt daraus die SQL-Datei; nach dem Go-live ist die Datei eingefroren. |
| E10 | **In R1 gibt es kein App-Konto mit Admin-Rolle.** Chris pflegt ausschließlich über Studio bzw. `psql` als Datenbankrolle `postgres` (umgeht RLS). Die bestehenden `is_admin()`-Policies (JWT `app_metadata.role = 'admin'`) bleiben für QUE-08 (R4) stehen. Die neuen Admin-Funktionen sind für `anon` und `authenticated` **nicht** ausführbar, auch nicht mit Admin-Claim. | Klärung 44: Admin-Rolle nur als Datenbankrolle. Kein Admin-JWT heißt weniger Angriffsfläche. Krit. 9 wird dadurch gegen die Rollen `anon` und `authenticated` getestet (mit und ohne gefälschten Claim). |
| E11 | **Zusammenführen und Meldungsergebnis laufen über SQL-Funktionen**, nicht über Hand-SQL im Studio: `admin_merge_games(source, target)` und `admin_resolve_report(report, status, target)`, beide nutzen die interne Funktion `repoint_game_references(from, to)`. | 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). Hand-SQL wäre fehleranfällig. Freigabe einer Meldung (KAT-04 Krit. 4) braucht dasselbe Umhängen, das ist der zweite Anwendungsfall. |
| 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 und #2 nicht abschließbar. Die Spalten ergeben sich vollständig aus den Klärungen 6, 26, 27, N12 und N12a. |
@ -54,11 +54,16 @@ Ein Import-Job (BGG oder eine andere Quelle) ist **nicht** Teil dieser Spec. Es
9. 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).
10. 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 F5, F7, F8, entschieden 2026-11-03)**
- **Krit. 7 gilt als erfüllt**, wenn die Prüfabfrage `supabase/catalog/checks.sql` auf Produktion **mindestens 180 sichtbare Katalogspiele** (`catalog_games`) meldet und für die Startbestandsspiele 0 Verstöße gegen die Randbedingungen aus 4.3 zeigt. Chris nimmt das Ergebnis vor dem Go-live ab.
- **Krit. 10 gilt als erfüllt**, wenn jedes Startbestandsspiel alle Pflicht-Fakten hat (mindestens ein Verlag, ein Genre, eine Mechanik), einen eigenen Kurztext auf Deutsch (`description_locale = 'de'`) hat, kein `image_path` und keine Quelle (`source_id`) gesetzt ist, die Fakten nach F7 erhoben wurden (Bestätigung durch Chris) und die App für alle diese Spiele das Platzhalter-Cover zeigt (KAT-01/KAT-02).
**Zuordnung zu den PRs**
| PR | Etappe | Kriterien | Bei zurückgestellten Kriterien: nur Datenmodell |
|---|---|---|---|
| 2a | R1.1 | 2, 7 (Lieferweg und Werkzeug; der Datenbestand selbst kommt von Chris), 9 (Katalog), 10 | 1 (Kennzeichnung `manual_fields`), 2 (Quellenteil), 6 (Erlaubnis je Quelle) |
| 2a | R1.1 | 2, 7 (Lieferweg, Werkzeug, Prüfabfrage; der Datenbestand selbst kommt von Chris), 9 (Katalog), 10 | 1 (Kennzeichnung `manual_fields`), 2 (Quellenteil), 6 (Erlaubnis je Quelle) |
| 2b | R1.2 | 4, 8 (Bearbeiten, Ausblenden, Zusammenführen) | – |
| 2c | R1.2 | 5, 8 (Meldungen), 9 (fremde Meldungen) | – |
@ -86,7 +91,7 @@ Krit. 3 hat keinen Anteil am Datenmodell und wird vollständig mit dem Import sp
| 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 festlegen (F8) |
| Genre, Mechanik (Krit. 10) | `genres`, `mechanics`, Zuordnungstabellen | passt | Vokabular für den Startbestand: Vorschlag durch Agents, Freigabe durch Chris (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) |
@ -146,7 +151,7 @@ Beide mit `id`, `created_at`, `updated_at` und Trigger `set_updated_at`. Ein Spi
- `before insert or update on public.games`, nur wenn `new.visibility = 'catalog'`.
- Wenn `coalesce(current_setting('shuffle.catalog_import', true), '') <> 'on'`:
- bei INSERT: jedes Feld aus der Schlüsselliste, das nicht `null` ist, wird ergänzt (siehe F4, Option A; bei Option B nur `description` und `image_path`),
- bei INSERT: jedes Feld aus der Schlüsselliste, das nicht `null` ist, wird ergänzt (F4, entschieden: alle gesetzten Felder),
- bei UPDATE: jedes Feld aus der Schlüsselliste, dessen Wert sich ändert (`is distinct from`), wird ergänzt.
- Eine Änderung nur an `manual_fields` selbst 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.
@ -186,7 +191,7 @@ grant update (title, description, description_locale, year, min_players, max_pla
| `catalog_sources` | alle, auch ohne Anmeldung | nur Admin (`is_admin()`-Policy, für R4); in R1 nur Studio/`postgres` |
| `publishers` | alle, auch ohne Anmeldung | nur Admin |
| `game_publishers` | wie das Spiel (wer das Spiel sehen darf) | Admin oder Ersteller des privaten Spiels (gleiches Muster wie `game_genres`) |
| `games` | unverändert: Katalog für alle, private Spiele nur der Ersteller; ausgeblendete und zusammengeführte Katalogspiele bleiben per ID lesbar (E3, F2) | unverändert per Policy; zusätzlich Spaltenrechte |
| `games` | unverändert: Katalog für alle, private Spiele nur der Ersteller; ausgeblendete und zusammengeführte Katalogspiele bleiben per ID lesbar (E3, F2 entschieden) | unverändert per Policy; zusätzlich Spaltenrechte |
| `catalog_games` | alle, auch ohne Anmeldung, nur sichtbare Katalogspiele | nicht schreibbar (View ohne Insert/Update-Rechte) |
### 3.3 Migration 2b `supabase/migrations/<ts>_catalog_merge.sql` (R1.2)
@ -196,7 +201,7 @@ grant update (title, description, description_locale, year, min_players, max_pla
`language plpgsql`, `security invoker`, `search_path = ''`; `revoke execute ... from public, anon, authenticated`. Hängt alle Verweise von `p_from` auf `p_to` um, in einer Transaktion:
1. **Varianten:** Für jede Variante von `p_from` wird eine Kopie (`name`, `year`, `language`, `bgg_id`) an `p_to` angelegt; eine Zuordnungstabelle alt → neu (temporär) entsteht. Grund: Die zusammengesetzten Fremdschlüssel `(variant_id, game_id)` in `collection_items` und `plays` erlauben kein direktes Umhängen von `game_variants.game_id`. Ein UPDATE, das `game_id` und `variant_id` in derselben Zeile gemeinsam setzt, ist zulässig.
2. **`collection_items`:** Hat derselbe Nutzer schon einen Eintrag zu `p_to`, gilt die Kollisionsregel aus F1. Sonst `game_id := p_to`, `variant_id := neu`.
2. **`collection_items`:** Hat derselbe Nutzer schon einen Eintrag zu `p_to`, gilt die Kollisionsregel (F1, entschieden): Der Zieleintrag bleibt; seine leeren Felder (`note`, `acquired_on`, `variant_id` sowie die Felder, die SAM-02/SAM-04 ergänzen) werden aus dem Quell-Eintrag gefüllt; bei unterschiedlicher Liste gilt `owned` vor `wishlist` vor `shopping` (wie Klärung 34 „besessen und gewünscht → Sammlung“); danach wird der Quell-Eintrag gelöscht. Ohne Kollision: `game_id := p_to`, `variant_id := neu`.
3. **`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`.
4. **`plays`:** `game_id := p_to`, `variant_id := neu`.
5. **`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 (Hinweis in 9.4).
@ -264,10 +269,10 @@ Rechte wie `admin_merge_games`.
| `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
Ablauf: `status`, `target_game_id`, `resolved_at = now()` setzen. Hat die Meldung einen vorläufigen Eintrag, dann (F3, entschieden)
- bei `approved`: `repoint_game_references(provisional, target)`; der vorläufige Eintrag bekommt `merged_into_id = target`, `hidden_at = now()` (KAT-04 Krit. 4: der Katalogeintrag ersetzt ihn, eigene Angaben bleiben erhalten),
- bei `merged` und bei `rejected` mit Ziel: Verhalten nach F3,
- bei `approved` und bei `merged`: `repoint_game_references(provisional, target)`; der vorläufige Eintrag bekommt `merged_into_id = target`, `hidden_at = now()` (KAT-04 Krit. 4: der Katalogeintrag ersetzt ihn, eigene Angaben bleiben erhalten),
- 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).
„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.
@ -302,20 +307,33 @@ Keine Edge Functions, keine RPCs für die App in dieser Spec.
### 4.3 Startbestand (Krit. 7, 10; E9)
**Verbindliche Randbedingungen (F5, F7, F8, entschieden 2026-11-03)**
| # | Regel | Prüfung |
|---|---|---|
| S1 | Zum Go-live **mindestens 180 sichtbare Katalogspiele** (Ziel etwa 200). | `checks.sql` (Release-Checkliste) |
| S2 | Jedes Spiel hat alle Pflicht-Fakten: Titel, mindestens ein Verlag, Jahr, Spielerzahl, Spieldauer, Mindestalter, mindestens ein Genre, mindestens eine Mechanik. | Skript (CSV-Prüfung) und `checks.sql` |
| S3 | **Herkunft der Fakten (F7):** Verlagsseiten, Spielschachtel und eigene Kenntnis, ergänzend Wikidata (CC0). Fakten werden **nicht** systematisch aus BGG oder einer anderen fremden Datenbank übernommen. Die endgültige Bewertung erfolgt mit der rechtlichen Prüfung vor dem Release. | Bestätigung durch Chris; optional Spalte `facts_source` in der CSV als Nachweis |
| S4 | **Kurztext (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. Keine fremden Beschreibungstexte. | Skript (Pflicht, Sprache `de`) und `checks.sql` |
| S5 | **Vokabular (F8):** Genres und Mechaniken nur aus der von Chris freigegebenen Liste (Vorschlag durch Agents: ca. 12–15 Genres, 20–30 Mechaniken, de/en). | Skript (unbekannter Begriff → Fehler) |
| S6 | Kein Cover (`image_path` leer), keine Quelle (`source_id` leer), keine BGG-Kennung aus einem Abgleich (N11, Krit. 10). | CSV hat keine solchen Spalten; `checks.sql` |
| S7 | **Erweiterungen (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 prüft, dass `base_game_key` auf einen vorhandenen `key` zeigt |
| S8 | Alle gesetzten Felder gelten als manuell gepflegt (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 § „Startbestand einspielen“). Die Dateien sind idempotent und dürfen wiederholt laufen. |
| Staging, Produktion | Nach den Migrationen spielt Chris `vocabulary.sql` und `startbestand.sql` per Studio-SQL-Editor oder `psql` ein (Runbook § „Startbestand einspielen“). Die Dateien sind idempotent und dürfen wiederholt laufen, **bis zum Go-live**; danach ist die Datei eingefroren und Korrekturen erfolgen nur noch im Studio (F6). |
**Dateien**
- `supabase/catalog/vocabulary.sql`: Genres und Mechaniken (de/en) mit festen UUIDs, `insert … on conflict (id) do update set name_de = excluded.name_de, name_en = excluded.name_en`. Das Vokabular legt Chris fest (F8). Die Begriffe aus `seed.sql` sind nur Beispieldaten für die Entwicklung; `seed.sql` wird so angepasst, dass es keine Begriffe mehr anlegt, die mit dem Vokabular kollidieren (Unique auf `name_de`/`name_en`).
- `supabase/catalog/startbestand.sql`: je Spiel ein Eintrag in `games` (`visibility = 'catalog'`, `image_path = null`, `source_id = null`) mit fester UUID sowie Zuordnungen zu Verlagen, Genres, Mechaniken. Upsert auf `id` (`on conflict (id) do update`). Am Anfang `begin;`, am Ende `commit;`.
- Wie die Datei entsteht, entscheidet F6. Empfohlen ist eine Tabelle `supabase/catalog/startbestand.csv`, die Chris pflegt, und ein kleines Skript `supabase/catalog/build_startbestand.ts` (Deno, ohne Fremdpakete außer der Standardbibliothek), das CSV prüft und daraus `startbestand.sql` erzeugt. Die UUID eines Spiels ergibt sich deterministisch (UUID v5) aus einer Spalte `key` (z. B. `azul`), damit Korrekturen dieselbe Zeile treffen.
- `supabase/catalog/vocabulary.sql`: Genres und Mechaniken (de/en) mit festen UUIDs, `insert … on conflict (id) do update set name_de = excluded.name_de, name_en = excluded.name_en`. Inhalt: Vorschlag durch Agents (Task B3a), gültig erst nach Freigabe durch Chris (S5). Die Begriffe aus `seed.sql` sind nur Beispieldaten für die Entwicklung; `seed.sql` wird so angepasst, dass es keine Begriffe mehr anlegt, die mit dem Vokabular kollidieren (Unique auf `name_de`/`name_en`).
- `supabase/catalog/startbestand.sql`: je Spiel ein Eintrag in `games` (`visibility = 'catalog'`, `image_path = null`, `source_id = null`) mit fester UUID sowie Zuordnungen zu Verlagen, Genres, Mechaniken. Upsert auf `id` (`on conflict (id) do update`). Am Anfang `begin;`, am Ende `commit;`. Die Datei wird erzeugt, nie von Hand bearbeitet.
- Entschieden (F6): Chris pflegt `supabase/catalog/startbestand.csv` (z. B. aus einer Tabellenkalkulation exportiert). Das Skript `supabase/catalog/build_startbestand.ts` (Deno, ohne Fremdpakete außer der Standardbibliothek) prüft die CSV und erzeugt daraus `startbestand.sql`. Die UUID eines Spiels ergibt sich deterministisch (UUID v5) aus der Spalte `key` (z. B. `azul`), damit Korrekturen dieselbe Zeile treffen.
**CSV-Format (bei F6 Option A)**
**CSV-Format (F6)**
| Spalte | Pflicht | Inhalt |
|---|---|---|
@ -326,18 +344,19 @@ Keine Edge Functions, keine RPCs für die App in dieser Spec.
| `min_players`, `max_players` | ja | Spielerzahl |
| `min_minutes`, `max_minutes` | ja | Spieldauer |
| `min_age` | ja | Mindestalter |
| `genres` | ja | ein oder mehrere Genres aus dem Vokabular (`name_de`), getrennt durch `;` |
| `mechanics` | ja | eine oder mehrere Mechaniken aus dem Vokabular, getrennt durch `;` |
| `description` | nach F5 | eigener Kurztext |
| `description_locale` | wenn `description` | `de` oder `en` |
| `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 (S4) | eigener Kurztext |
| `description_locale` | ja (S4) | immer `de` |
| `facts_source` | nein | Herkunft der Fakten als Nachweis für S3 (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`; das Skript ignoriert die Spalte, bis #3 die Tabelle hat |
| `base_game_key` | nein | reserviert für KAT-03 (#12), `key` des Basisspiels; wird ignoriert, bis #12 die Spalte hat |
| `base_game_key` | nein | reserviert für KAT-03 (#12), `key` des Basisspiels (S7); 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 `key`, `max < min`, Werten außerhalb der DB-Checks, Kurztext ohne Sprache. Es gibt **keine** Spalte für ein Cover und keine Quellen-Spalten (Krit. 10). So kann ein unvollständiges Spiel gar nicht erst in die SQL-Datei gelangen; CI bleibt grün, auch während Chris noch einpflegt.
Das Skript bricht mit Zeilennummer und Grund ab bei: fehlender Pflichtangabe, unbekanntem Genre/Mechanik, doppeltem `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 ein Cover und keine Quellen-Spalten (Krit. 10). So kann ein unvollständiges Spiel gar nicht erst in die SQL-Datei gelangen; CI bleibt grün, auch während Chris noch einpflegt.
**Prüfabfrage vor dem Go-live** (`supabase/catalog/checks.sql`, Teil der Release-Checkliste): Anzahl sichtbarer Katalogspiele (Ziel nach F8), Spiele ohne Verlag, Genre, Mechanik oder Kurztext, Katalogspiele mit `image_path` oder `source_id` (müssen 0 sein, Krit. 10).
**Prüfabfrage vor dem Go-live** (`supabase/catalog/checks.sql`, Teil der Release-Checkliste): Anzahl sichtbarer Katalogspiele (S1, mindestens 180), Spiele ohne Verlag, Genre, Mechanik oder Kurztext (S2, S4), Kurztexte mit `description_locale` ≠ `de`, Katalogspiele mit `image_path` oder `source_id` (S6, müssen 0 sein). Die Abfrage gibt je Regel eine Zeile mit Sollwert, Istwert und „ok“/„Verstoß“ aus.
**Daten werden nicht von Agents erfunden.** Agents liefern Werkzeug, Format, Prüfungen und höchstens offensichtlich fiktive Testzeilen in Fixtures (z. B. „Testspiel A“). Den Inhalt liefert Chris (Krit. 7).
**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)
@ -346,11 +365,11 @@ Ein Dokument, wachsend über die PRs:
| Abschnitt | PR | Inhalt |
|---|---|---|
| Zugang | 2a | Studio über SSH-Tunnel/VPN, Rolle `postgres`, „immer in einer Transaktion arbeiten“ (`begin; … commit;`), vor größeren Änderungen Backup-Stand prüfen |
| Startbestand einspielen und korrigieren | 2a | Reihenfolge Migrationen → `vocabulary.sql` → `startbestand.sql`; Prüfabfrage `checks.sql`; Korrekturen vor dem Go-live in der CSV, danach im Studio (siehe F6) |
| Startbestand pflegen, einspielen und abnehmen | 2a | Randbedingungen S1–S8; CSV bearbeiten → Skript ausführen → Commit; Reihenfolge Migrationen → `vocabulary.sql` → `startbestand.sql`; Prüfabfrage `checks.sql`; ab dem Go-live ist die Datei eingefroren, Korrekturen nur noch im Studio (F6) |
| Spiel anlegen und bearbeiten | 2a | Tabelleneditor oder SQL; Zuordnungen zu Verlag, Genre, Mechanik; Hinweis: jede Änderung wird automatisch in `manual_fields` vermerkt; Markierung bewusst entfernen nur per SQL |
| Ausblenden und wieder einblenden | 2b | `hidden_at` setzen bzw. leeren; Wirkung: nicht mehr in Suche/Entdecken, bestehende Sammlungen und Partien bleiben |
| Zusammenführen | 2b | Duplikat finden (Abfrage nach normalisiertem Titel + Jahr), `select public.admin_merge_games('<quelle>', '<ziel>');`, Ergebnis-JSON lesen, Fehlertexte und ihre Bedeutung |
| Meldungen bearbeiten | 2c | offene Meldungen abfragen (älteste zuerst), Ähnliche im Katalog suchen, Entscheidung: freigeben (erst Katalogeintrag anlegen, dann `admin_resolve_report(…, 'approved', '<ziel>')`), zusammenführen (`'merged'`), ablehnen mit oder ohne Verweis (`'rejected'`); Fehlertexte |
| Ausblenden und wieder einblenden | 2b | `hidden_at` setzen bzw. leeren; Wirkung: nicht mehr in Suche/Entdecken, Detailseite und bestehende Sammlungen und Partien bleiben (F2) |
| Zusammenführen | 2b | Duplikat finden (Abfrage nach normalisiertem Titel + Jahr), `select public.admin_merge_games('<quelle>', '<ziel>');`, Ergebnis-JSON lesen (inkl. Kollisionen nach F1), Fehlertexte und ihre Bedeutung |
| Meldungen bearbeiten | 2c | offene Meldungen abfragen (älteste zuerst), Ähnliche im Katalog suchen, Entscheidung: freigeben (erst Katalogeintrag anlegen, dann `admin_resolve_report(…, 'approved', '<ziel>')`), zusammenführen (`'merged'`, hängt den vorläufigen Eintrag um), ablehnen mit Verweis (`'rejected'` mit Ziel, hängt nicht um) oder ohne Verweis (F3); Fehlertexte |
| 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 (siehe 7.1), damit das Runbook nicht veraltet.
@ -362,16 +381,16 @@ KAT-05 hat in R1 keine eigenen Screens (keine Admin-UI, Klärung 44). Die App-An
| 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) | Detailseite eines zusammengeführten Eintrags leitet auf den Zieleintrag weiter; ausgeblendete Einträge nach F2 | KAT-02 (#4) |
| 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“ (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` | KAT-02 (#4) |
| Krit. 5 (Ergebnis einer Meldung) | Hinweis am vorläufigen Eintrag | KAT-04 (#13) |
| 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 (F3) | 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: eigene, mit Flutter gezeichnete Grafik (Fläche in Theme-Farbe, neutrales Symbol), keine Bilddatei von Dritten, kein neues Paket. Variante nach F9.
- Platzhalter (F9, entschieden): **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, keine Variante je Genre.
- Semantics: Bild ausgeblendet aus dem Semantics-Baum, wenn der Titel daneben steht (`excludeSemantics`-Variante); sonst Beschriftung aus ARB `gameCoverPlaceholderLabel` („Kein Cover für {title}“ / „No cover for {title}“).
- 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 von `catalog_games`.
@ -384,7 +403,7 @@ KAT-05 hat in R1 keine eigenen Screens (keine Admin-UI, Klärung 44). Die App-An
## 7. Teststrategie
Werkzeuge: pgTAP (`supabase test db`), Deno-Tests für das Erzeugungsskript (falls F6 Option A), `flutter_test` für das Widget. Testnamen enthalten die Referenz („KAT-05 Kriterium n“).
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**
@ -400,21 +419,21 @@ Die Tests müssen auch mit eingespieltem Startbestand grün sein: Sie legen eige
| AK | Unit | Widget | Integration/E2E | RLS/pgTAP | Task |
|---|---|---|---|---|---|
| KAT-05 Krit. 1 (nur Modell) | – | – | – | `06`: Update eines Katalogspiels als `postgres` → Feld in `manual_fields`; mit `set local shuffle.catalog_import = 'on'` → nicht markiert; Insert markiert nach F4; Zuordnung Genre/Verlag → `genres`/`publishers` markiert; unbekannter Schlüssel in `manual_fields` → 23514 | B1, B2 |
| KAT-05 Krit. 1 (nur Modell) | – | – | – | `06`: Update eines Katalogspiels als `postgres` → Feld in `manual_fields`; mit `set local shuffle.catalog_import = 'on'` → nicht markiert; Insert markiert alle gesetzten Felder (F4); Zuordnung Genre/Verlag → `genres`/`publishers` markiert; unbekannter Schlüssel in `manual_fields` → 23514 | B1, B2 |
| Krit. 2 | – | – | – | `06`: `games.id` ist uuid-PK mit Standard; ID bleibt bei Update unverändert (bestehender Trigger); `source_id` ohne `source_ref` → 23514; `(source_id, source_ref)` im Katalog eindeutig → 23505; Quelle `bgg` vorhanden | B1, B2 |
| Krit. 3 | – | – | – | – (zurückgestellt, kein Modellanteil) | – |
| Krit. 4 | – | – | (KAT-01: Suche findet ausgeblendete Spiele nicht, E2E in #3) | `06`: `catalog_games` ohne ausgeblendete und zusammengeführte Spiele, für `anon` und `authenticated`; ausgeblendetes Spiel bleibt per ID in `games` lesbar (nach F2); Sammlungseintrag eines Nutzers zu einem ausgeblendeten Spiel bleibt lesbar. `07`: Ausblenden/Einblenden per Runbook-SQL; `admin_merge_games` hängt Sammlung, Favoriten, Partien (mit Variante) und Wertungsbögen um, Kollisionen nach F1, Quelle danach `merged_into_id`/`hidden_at` gesetzt, Ketten geglättet; alle Fehlerfälle (`merge_same_game` … `merge_source_already_merged`); Fremdschlüssel-Wächter | B1, B2, B5, B6 |
| Krit. 5 | – | – | – | `08`: Checks der Status-Kombinationen; `admin_resolve_report` für `approved` (vorläufiger Eintrag umgehängt und zusammengeführt), `merged` und `rejected` mit Ziel (nach F3), `rejected` ohne Ziel (vorläufiger Eintrag unverändert); `report_not_open` bei zweitem Aufruf; `report_target_required`, `report_target_not_listed`; N12a-Index: zweite offene Import-Meldung desselben Nutzers zur selben BGG-ID → 23505, bei anderem Nutzer erlaubt; Konto löschen → Meldungen weg | B8, B9 |
| Krit. 4 | – | – | (KAT-01: Suche findet ausgeblendete Spiele nicht, E2E in #3) | `06`: `catalog_games` ohne ausgeblendete und zusammengeführte Spiele, für `anon` und `authenticated`; ausgeblendetes Spiel bleibt per ID in `games` lesbar (F2); Sammlungseintrag eines Nutzers zu einem ausgeblendeten Spiel bleibt lesbar. `07`: Ausblenden/Einblenden per Runbook-SQL; `admin_merge_games` hängt Sammlung, Favoriten, Partien (mit Variante) und Wertungsbögen um; Kollision (F1): Ziel bleibt, leere Felder aus der Quelle gefüllt, `owned` schlägt `wishlist`/`shopping`, Quell-Eintrag gelöscht; Quelle danach `merged_into_id`/`hidden_at` gesetzt, Ketten geglättet; alle Fehlerfälle (`merge_same_game` … `merge_source_already_merged`); 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), `rejected` mit Ziel (nur Verweis gespeichert, vorläufiger Eintrag unverändert; F3), `rejected` ohne Ziel (vorläufiger Eintrag unverändert); `report_not_open` bei zweitem Aufruf; `report_target_required`, `report_target_not_listed`; N12a-Index: zweite offene Import-Meldung desselben Nutzers zur selben BGG-ID → 23505, bei anderem Nutzer erlaubt; Konto löschen → Meldungen weg | B8, B9 |
| Krit. 6 (nur Modell) | – | – | – | `06`: `catalog_games.description`/`image_path` sind `null` bei Quelle ohne Erlaubnis, sichtbar mit Erlaubnis, sichtbar bei manuell gepflegtem Feld, sichtbar ohne Quelle | B1, B2 |
| Krit. 7 | Deno: Skript erzeugt aus einer Fixture-CSV die erwartete SQL (Snapshot), gleiche `key` → gleiche UUID, Fehler mit Zeilennummer je Prüfregel | – | CI: `supabase db reset` mit `vocabulary.sql` + `startbestand.sql` läuft durch; CI-Schritt „erzeugte SQL ist aktuell“ (Skript erneut ausführen, `git diff --exit-code`) | – | B3, T1 |
| Krit. 7 | Deno: Skript erzeugt aus einer Fixture-CSV die erwartete SQL (Snapshot), gleiche `key` → gleiche UUID, Fehler mit Zeilennummer je Prüfregel, `facts_source` landet nicht in der SQL | – | CI: `supabase db reset` mit `vocabulary.sql` + `startbestand.sql` läuft durch; CI-Schritt „erzeugte SQL ist aktuell“ (Skript erneut ausführen, `git diff --exit-code`); `checks.sql` läuft gegen die lokale DB ohne Fehler (die Mindestzahl S1 ist dort nur Information, kein CI-Abbruch) | – | B3, T1 |
| Krit. 8 | – | – | – | `07`/`08`: jeder SQL-Baustein aus `docs/katalog-pflege.md` wird im Test genau so ausgeführt (Kommentar mit Abschnittsverweis); Review prüft Vollständigkeit | B4, B7, B10 |
| Krit. 9 | – | – | API-Test optional (PostgREST `PATCH /rest/v1/games` mit Nutzer-JWT → 0 Zeilen bzw. 401/403), nur falls test-engineer ihn für nötig hält; pgTAP deckt dieselben Rollen ab | `06`: `anon` und Nutzer (auch mit gefälschtem `user_metadata.role = 'admin'`) können Katalogspiele, `catalog_sources`, `publishers`, `game_publishers` (Katalog) nicht anlegen, ändern, löschen; Nutzer kann an eigenem privaten Spiel `hidden_at`, `merged_into_id`, `manual_fields`, `source_id` nicht setzen (42501), wohl aber `title`; View nicht schreibbar. `07`/`08`: `admin_merge_games`, `admin_resolve_report`, `repoint_game_references` für `anon`/`authenticated` nicht ausführbar (42501), auch mit `app_metadata.role = 'admin'`; `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. 10 | Deno: CSV ohne Pflicht-Fakten → Fehler; CSV hat keine Cover-/Quellen-Spalte | `GameCover`: ohne `imageUrl` → Platzhalter; Ladefehler → Platzhalter; Semantics-Label de/en; Kontrast und `textScaler` 2.0 ohne Überlauf | (KAT-02: Detailseite zeigt Platzhalter und Kurztext, in #4) | `06`: Fixture-Spiele aus dem Startbestands-Format haben `image_path is null`, `source_id is null`; Prüfabfrage `checks.sql` liefert 0 Verstöße auf dem lokalen Bestand | B3, U1, T1, T2 |
| Krit. 10 | Deno: CSV ohne Pflicht-Fakten, ohne Kurztext oder mit `description_locale` ≠ `de` → Fehler; unbekannter Begriff → Fehler; CSV hat keine Cover-/Quellen-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 aus dem Startbestands-Format haben `image_path is null`, `source_id is null`, `description_locale = 'de'`; Prüfabfrage `checks.sql` meldet für die Fixtures 0 Verstöße und für ein absichtlich fehlerhaftes Fixture den erwarteten Verstoß | 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:** Studio nicht öffentlich erreichbar (Staging/Prod, D1); Einspielen des Startbestands auf Staging/Prod und Prüfabfrage (Release-Checkliste); Umfang „etwa 200“ (Prüfabfrage, Abnahme durch Chris); inhaltliche Richtigkeit der Fakten und Kurztexte (Chris).
**Nicht automatisiert testbar, manuelle Prüfung:** Studio nicht öffentlich erreichbar (Staging/Prod, D1); Einspielen des Startbestands auf Staging/Prod und Prüfabfrage mit mindestens 180 Spielen (S1, Release-Checkliste, Abnahme durch Chris); Herkunft der Fakten nach S3 (Bestätigung durch Chris, rechtliche Prüfung vor dem Release); inhaltliche Richtigkeit der Fakten und Kurztexte (Chris).
## 8. Aufgaben
@ -422,41 +441,43 @@ Die Tests müssen auch mit eingespieltem Startbestand grün sein: Sie legen eige
| # | 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 und Indizes, `publishers`, `game_publishers`, Trigger `games_mark_manual_fields` und `game_links_mark_manual` (Variante nach F4), View `catalog_games`, Spaltenrechte auf `games`, RLS und Grants für alle neuen Tabellen | Freigabe Spec; F4 |
| B1 | backend-dev | Migration `<ts>_catalog_foundation.sql` nach 3.2: `catalog_sources` (+ Zeile `bgg`), neue Spalten in `games` mit Checks und Indizes, `publishers`, `game_publishers`, Trigger `games_mark_manual_fields` (Insert markiert alle gesetzten Felder, F4) und `game_links_mark_manual`, View `catalog_games`, Spaltenrechte auf `games`, RLS und Grants für alle neuen Tabellen | – |
| B2 | backend-dev | pgTAP `06_catalog_foundation.test.sql` nach 7.1 (Krit. 1, 2, 4, 6, 9, 10, Sicherheit Krit. 2); bestehende Tests mit Startbestand lauffähig halten; `00_schema.test.sql` prüft fest „genau 17 Tabellen“ → auf 20 anpassen (`catalog_sources`, `publishers`, `game_publishers`), Zahlen in `supabase/README.md` („17 Tabellen, 2 Views“) nachziehen (3 Views mit `catalog_games`) | B1 |
| B3 | backend-dev | Startbestand-Werkzeug nach 4.3 und F6: `supabase/catalog/vocabulary.sql` (Gerüst; Inhalt nach F8), Format/Skript `build_startbestand.ts` mit Prüfregeln und Deno-Tests, `startbestand.sql` (zunächst leer bzw. nur Kopf), `checks.sql`, `config.toml` `[db.seed] sql_paths`, `seed.sql` bereinigen (keine kollidierenden Begriffe) | B1; F6, F8 |
| B4 | backend-dev | Doku: `docs/katalog-pflege.md` Abschnitte Zugang, Startbestand, Spiel anlegen/bearbeiten, Admin-Rolle (4.4); `docs/datenmodell-r1.md` (neue Objekte, doppelten Abschnitt zusammenführen); `supabase/README.md` | B1, B3 |
| U1 | ui-designer | `GameCover` in `core/widgets/` nach 5 (Platzhalter nach F9, Größen, Ladezustand, Semantics, ARB-Schlüssel de/en) | F9 |
| T1 | test-engineer | Review der pgTAP-Abdeckung gegen 7.1; Deno-Tests des Skripts ergänzen; CI-Schritt „erzeugte SQL ist aktuell“ mit devops abstimmen | B2, B3 |
| B3a | backend-dev | Vokabular-Vorschlag (ca. 12–15 Genres, 20–30 Mechaniken, je `name_de`/`name_en`) als `supabase/catalog/vocabulary.sql` mit festen UUIDs; Vorlage zur Freigabe an Chris (im PR als eigener Abschnitt). Gilt erst nach Freigabe (C0). | B1 |
| B3 | backend-dev | Startbestand-Werkzeug nach 4.3: CSV-Vorlage `startbestand.csv` (nur Kopfzeile + Hinweise), Skript `build_startbestand.ts` mit allen Prüfregeln (S2, S4–S7) und Deno-Tests, erzeugtes `startbestand.sql` (zunächst leer bzw. nur Kopf), `checks.sql` mit Regeln S1, S2, S4, S6 (Soll/Ist/Ergebnis), `config.toml` `[db.seed] sql_paths`, `seed.sql` bereinigen (keine kollidierenden Begriffe) | B1, B3a |
| B4 | backend-dev | Doku: `docs/katalog-pflege.md` Abschnitte Zugang, Startbestand (Randbedingungen S1–S8, Ablauf CSV → Skript → Einspielen → Abnahme, Einfrieren nach Go-live), Spiel anlegen/bearbeiten, Admin-Rolle (4.4); `docs/datenmodell-r1.md` (neue Objekte, doppelten Abschnitt zusammenführen); `supabase/README.md` | B1, B3 |
| U1 | ui-designer | `GameCover` in `core/widgets/` nach 5: ein einheitlicher neutraler Platzhalter (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 S2, S4–S7 mit Positiv- und Negativfall); CI-Schritt „erzeugte SQL ist aktuell“ mit devops abstimmen | B2, B3 |
| T2 | test-engineer | Widget-Tests `GameCover` (Platzhalter, Ladefehler, de/en, a11y-Guidelines, `textScaler` 2.0) | U1 |
| D1 | devops | Studio-/Postgres-Zugang auf Staging/Prod nur über SSH-Tunnel/VPN, Basic-Auth gesetzt (Doku in `docs/`, kein Eingriff ohne Freigabe); CI: Deno für das Skript bereitstellen, Schritt „erzeugte SQL aktuell“ | B3 |
| C1 | Chris | Startbestand einpflegen (~200 Spiele, Kurztexte, Vokabular freigeben) im Format nach F6; vor dem Go-live Prüfabfrage abnehmen | B3 |
| D1 | devops | Studio-/Postgres-Zugang auf Staging/Prod nur über SSH-Tunnel/VPN, Basic-Auth gesetzt, Studio-Rolle per `select current_user` bestätigen (Doku in `docs/`, kein Eingriff ohne Freigabe); CI: Deno für das Skript bereitstellen, Schritt „erzeugte SQL aktuell“; Release-Checkliste um `checks.sql` (S1: 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 S3, deutsche Kurztexte nach S4), Skript ausführen, committen; vor dem Go-live auf Produktion einspielen und `checks.sql` abnehmen | B3, C0 |
### 8.2 PR 2b – Admin-Pflege (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`, `admin_merge_games`, Rechte (`revoke execute`) | 2a gemergt; F1 |
| B6 | backend-dev | pgTAP `07_catalog_merge.test.sql` inkl. Fremdschlüssel-Wächter | B5 |
| B5 | backend-dev | Migration `<ts>_catalog_merge.sql` nach 3.3: `repoint_game_references` mit Kollisionsregel F1, `admin_merge_games`, Rechte (`revoke execute`) | 2a gemergt |
| B6 | backend-dev | pgTAP `07_catalog_merge.test.sql` inkl. Kollisionsfällen (F1: gleiche Liste, unterschiedliche Liste, leere/gefüllte Felder) und Fremdschlüssel-Wächter | B5 |
| B7 | backend-dev | Runbook: Abschnitte Ausblenden und 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: Tabelle, Checks, Indizes, RLS (nur Lesen der eigenen), `admin_resolve_report`, `admin_merge_games` erweitert | 2b gemergt; F3 |
| B8 | backend-dev | Migration `<ts>_game_reports.sql` nach 3.4: Tabelle, Checks, Indizes, RLS (nur Lesen der eigenen), `admin_resolve_report` (Umhängen bei `approved` und `merged`, nur Verweis bei `rejected` mit Ziel; F3), `admin_merge_games` erweitert | 2b gemergt |
| B9 | backend-dev | pgTAP `08_game_reports.test.sql`; `00_schema.test.sql` auf 21 Tabellen, README-Zahlen nachziehen | B8 |
| B10 | backend-dev | Runbook: Abschnitt Meldungen bearbeiten | B8, B9 |
### 8.4 Reihenfolge und Parallelität
```
R1.1 Freigabe → B1 → B2 | B3 → B4, T1, D1 parallel: U1 → T2 danach: C1 (Chris, laufend bis Go-live)
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
```
2c muss vor der Umsetzung von #13 gemergt sein (E12); die Spec von #13 kann parallel entstehen und baut auf 3.4 auf. Merge nur durch Chris.
B3 kann mit einem vorläufigen Fixture-Vokabular für die Tests beginnen; die Datei `vocabulary.sql` wird erst nach C0 endgültig. 2c muss vor der Umsetzung von #13 gemergt sein (E12); die Spec von #13 kann parallel entstehen und baut auf 3.4 auf. Merge nur durch Chris.
## 9. Risiken & offene Fragen
@ -465,44 +486,45 @@ R1.2 B5 → B6 → B7 (2b) → B8 → B9 → B10 (2c) → #13 KAT-04 Umsetz
| # | Risiko | Auswirkung | Gegenmaßnahme |
|---|---|---|---|
| R1 | **Studio umgeht RLS** und hat im Self-Hosting keine eigene Benutzerverwaltung | Wer Studio erreicht, kann alles lesen und ändern, auch private Nutzerdaten | Studio und Postgres nur über SSH-Tunnel/VPN (D1), Basic-Auth als zweite Schicht; Runbook: immer in Transaktionen, Funktionen statt Hand-SQL für Zusammenführen und Meldungen |
| R2 | **EU-Datenbankrecht und Nutzungsbedingungen:** Werden die Fakten für ~200 Spiele in großem Umfang aus BGG oder einer anderen Datenbank übernommen, kann das das Schutzrecht des Datenbankherstellers bzw. deren Nutzungsbedingungen verletzen, auch wenn Einzelfakten nicht geschützt sind | Rechtliches Risiko für den Start | Frage F7; Empfehlung: Fakten aus Verlagsseiten, Spielschachtel und eigener Kenntnis, nicht systematisch aus einer fremden Datenbank kopieren; vor dem Release mit der rechtlichen Prüfung (R11 in Spec #1) klären |
| R2 | **EU-Datenbankrecht und Nutzungsbedingungen:** Werden die Fakten für ~200 Spiele in großem Umfang aus BGG oder einer anderen Datenbank übernommen, kann das das Schutzrecht des Datenbankherstellers bzw. deren Nutzungsbedingungen verletzen, auch wenn Einzelfakten nicht geschützt sind | Rechtliches Risiko für den Start | Entschieden (F7): Fakten aus Verlagsseiten, Spielschachtel, eigener Kenntnis, ergänzend Wikidata (CC0); keine systematische Übernahme aus BGG (S3); optionaler Herkunftsnachweis `facts_source`; endgültige Bewertung mit der rechtlichen 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 in `07` (7) |
| R4 | Ausgeblendete Spiele bleiben über die rohe API per ID lesbar (E3) | Ein aus rechtlichen Gründen ausgeblendeter Eintrag wäre weiter abrufbar | Frage F2; bei Bedarf eigener Status „gesperrt“ mit RLS-Filter |
| R5 | Startbestand weicht zwischen CSV/SQL-Datei und Produktion ab, sobald Chris nach dem Go-live im Studio korrigiert | Erneutes Einspielen der Datei überschreibt Studio-Korrekturen (Upsert) | Regel im Runbook (F6): bis zum Go-live ist die Datei maßgeblich, danach nur Studio; die Datei wird danach nicht mehr eingespielt |
| R6 | `manual_fields` nach Option A (F4) markiert den ganzen Startbestand als manuell | Ein späterer Import ergänzt nur leere Felder und korrigiert keine Fakten | Bewusste Wahl in F4; Admin kann Markierungen per SQL entfernen (Runbook) |
| R4 | Ausgeblendete Spiele bleiben über die rohe API per ID lesbar (E3) | Ein aus rechtlichen Gründen ausgeblendeter Eintrag wäre weiter abrufbar | Akzeptiert (F2); im Ernstfall Eintrag zusammenführen oder inhaltlich leeren; Status „gesperrt“ mit RLS-Filter erst bei Bedarf |
| R5 | Startbestand weicht zwischen CSV/SQL-Datei und Produktion ab, sobald Chris nach dem Go-live im Studio korrigiert | Erneutes Einspielen der Datei überschreibt Studio-Korrekturen (Upsert) | Entschieden (F6): Datei ist ab dem Go-live eingefroren und wird nicht mehr eingespielt; Regel im Runbook |
| R6 | `manual_fields` markiert den ganzen Startbestand als manuell (F4) | Ein späterer Import ergänzt nur leere Felder und korrigiert keine Fakten | Bewusst entschieden (F4); Admin kann Markierungen per SQL gezielt entfernen (Runbook) |
| 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 (für Tests) | Kein Insert-Recht für Nutzer in 2c, deshalb kein offener Zwischenzustand |
| R8 | Spaltenrechte auf `games` schränken auch einen künftigen Admin über die API ein | QUE-08 (R4) braucht eigene RPCs | Bewusst; Hinweis in 3.2 |
| R9 | ~200 Spiele einpflegen ist viel Handarbeit für Chris | Startbestand zum R1-Start unvollständig | Pflegeformat mit Prüfungen (F6), Go-live-Abnahme mit Mindestzahl (F8); Prüfabfrage zeigt Fortschritt |
| R9 | ~180–200 Spiele mit deutschen Kurztexten einpflegen ist viel Handarbeit für Chris | Startbestand zum R1-Start unvollständig | CSV mit Prüfungen (F6), KI-Entwürfe für Kurztexte mit Prüfung durch Chris zulässig (F5), Mindestzahl 180 (F8); `checks.sql` zeigt den Fortschritt |
### 9.2 Offene Fragen an Chris
### 9.2 Entscheidungen zu den offenen Fragen (Chris, 2026-11-03)
Fachliche Fragen, die ich nicht selbst entscheiden kann. Jede mit Optionen und Empfehlung. Die Umsetzung von 2a hängt nur an F4, F6, F8, F9 (und F5 für das CSV-Format); F1 blockiert 2b, F3 blockiert 2c; F2 und F7 blockieren keinen PR, wohl aber KAT-01/KAT-02 bzw. das Release.
Alle neun Fragen hat Chris am 2026-11-03 entschieden („Alle wie empfohlen“). Keine Frage blockiert mehr einen PR.
| # | Frage | Optionen | Empfehlung | Wirkung |
| # | Frage | Status | Entscheidung | Wirkung in der Spec |
|---|---|---|---|---|
| F1 | **Zusammenführen bei Doppelbesitz:** Ein Nutzer hat beide Spiele (Quelle und Ziel) in Sammlung oder Listen. Was passiert mit seinen zwei Einträgen? | **A:** Zieleintrag bleibt; leere Felder des Ziels (z. B. Notiz, Kaufdatum) werden aus dem Quell-Eintrag gefüllt; Liste: Sammlung schlägt Wunsch- und Einkaufsliste (wie Klärung 34 „besessen und gewünscht → Sammlung“); Quell-Eintrag wird gelöscht. **B:** Zieleintrag bleibt unverändert, Quell-Eintrag wird gelöscht. **C:** Zusammenführen bricht ab, Admin klärt vorher manuell. | **A** – keine Angaben gehen verloren, Regel ist schon aus Klärung 34 bekannt; der Fall ist bei einem handgepflegten Katalog selten | 3.3 Schritt 2, B5, `07` |
| F2 | **Ausgeblendete Einträge:** Reicht „nicht in Suche und Entdecken“ (Krit. 4), oder sollen sie auch per Direktaufruf (Detailseite, API) verschwinden? | **A:** Nur aus Suche/Entdecken; Detailseite und bestehende Sammlungen/Partien zeigen sie weiter, mit Hinweis „nicht mehr im Katalog“. **B:** Zusätzlich per RLS für alle gesperrt, die das Spiel nicht in Sammlung/Partien haben. **C:** Wie A, und später ein eigener Status „gesperrt“ für rechtliche Fälle. | **A**, bei Bedarf C – einfach, keine kaputten Verweise; ein Eintrag mit Rechtsproblem wird zusammengeführt oder inhaltlich geleert | E3, R4, KAT-02 (#4) |
| F3 | **Meldung „zusammengeführt“ oder „abgelehnt mit Verweis“:** Soll der vorläufige Eintrag des Meldenden automatisch auf den Zieleintrag umgehängt werden (wie bei „freigegeben“)? | **A:** „zusammengeführt“ hängt automatisch um; „abgelehnt mit Verweis“ setzt nur den Verweis, der Nutzer entscheidet in der App (KAT-04). **B:** Beide hängen automatisch um. **C:** Keiner hängt um, nur Verweis und Hinweis. | **A** – „zusammengeführt“ heißt „das ist dasselbe Spiel“, Umhängen ist dann richtig; „abgelehnt mit Verweis“ kann auch „ähnlich, aber anders“ bedeuten (z. B. Edition), das soll der Nutzer bestätigen | 3.4, B8, Spec #13 |
| F4 | **Kennzeichnung des Startbestands:** Gelten alle Felder der ~200 Spiele als „manuell gepflegt“, oder nur der eigene Kurztext? | **A:** Alle beim Einpflegen gesetzten Felder sind manuell; ein späterer Import ergänzt nur leere Felder. **B:** Nur `description` (eigener Kurztext) ist manuell; Fakten darf ein lizenzierter Import korrigieren, spätere Studio-Änderungen werden trotzdem markiert. | **A** – entspricht dem Wortlaut von Krit. 4 und N11 („von Chris gepflegt“), ist die vorsichtige Wahl; einzelne Markierungen lassen sich später gezielt entfernen | E4, 3.2 Trigger, R6 |
| F5 | **Eigene Kurztexte:** In welcher Sprache, wer schreibt sie, sind sie Pflicht zum Go-live? | Sprache: **A** nur Deutsch, **B** nur Englisch, **C** beide (widerspricht „eine Sprache je Spiel“ im Datenmodell, bräuchte eine Übersetzungstabelle). Autor: Chris selbst, oder KI-Entwürfe, die Chris prüft und freigibt. Pflicht: ja für alle Startbestandsspiele, oder optional (fehlende werden weggelassen, KAT-02 Krit. 2). | Deutsch (A), KI-Entwürfe mit Prüfung durch Chris sind zulässig, Pflicht für alle Startbestandsspiele (die Prüfabfrage meldet fehlende) – englische Oberflächen zeigen dann den Sprachhinweis (Klärung 48) | E8, 4.3 CSV, `checks.sql` |
| F6 | **Pflegeformat des Startbestands:** Wie trägst du die ~200 Spiele ein? | **A:** Tabelle (CSV, z. B. aus einer Tabellenkalkulation) im Repo, ein Skript erzeugt daraus die SQL-Datei und prüft alles. **B:** Direkt im Studio-Tabelleneditor (lokal oder Staging), danach Export per `pg_dump --data-only` in die SQL-Datei. **C:** SQL-Datei von Hand nach Vorlage. Zusätzlich: Ist die Datei nach dem Go-live eingefroren (Korrekturen nur noch im Studio)? | **A** und „nach dem Go-live eingefroren“ – Tabellen sind für 200 Zeilen mit mehreren Genres/Mechaniken/Verlagen am bequemsten, Fehler fallen sofort auf; B ist für Zuordnungstabellen mit UUIDs mühsam | E9, 4.3, B3, R5 |
| F7 | **Herkunft der Fakten:** Woher nimmst du Verlag, Jahr, Spielerzahl, Dauer, Alter, Genre und Mechanik? | **A:** Verlagsseiten, Spielschachtel, eigene Kenntnis. **B:** Einzeln von BGG abgelesen. **C:** Wikipedia/Wikidata (Wikidata ist CC0). | **A**, ergänzend C (Wikidata) – vermeidet das Risiko R2 (Datenbankrecht, BGG-Nutzungsbedingungen) am ehesten; endgültig mit der rechtlichen Prüfung vor dem Release klären | R2, Release-Checkliste |
| F8 | **Umfang, Abnahme und Vokabular:** Was heißt „etwa 200“ für die Abnahme, wer legt die Genre- und Mechanik-Liste fest, und gehören Erweiterungen in den Startbestand? | Abnahme: Mindestzahl (z. B. 180) oder fester Bereich (180–220). Vokabular: Agents schlagen eine Liste vor (ca. 12–15 Genres, 20–30 Mechaniken, de/en), du gibst frei; oder du legst sie selbst fest. Erweiterungen: ja (Basisspiel in der Spalte `base_game_key` vormerken, Verknüpfung kommt mit #12), oder erst ab R1.2. | Mindestens 180 sichtbare Katalogspiele mit allen Pflicht-Fakten zum Go-live; Vorschlagsliste durch Agents, Freigabe durch dich; Erweiterungen erlaubt und vorgemerkt | 4.3, B3, `checks.sql`, C1 |
| F9 | **Platzhalter-Cover:** Ein einheitlicher Platzhalter oder je Genre unterschiedlich? | **A:** Ein neutraler Platzhalter für alle (Theme-Farbe, Symbol). **B:** Farbe/Symbol je Haupt-Genre. **C:** Wie A, zusätzlich Anfangsbuchstabe des Titels. | **A** – einfach, ruhig in Listen mit vielen Spielen ohne Cover (in R1 alle); B/C lassen sich später ohne Datenänderung nachrüsten | 5, U1 |
| F1 | Zusammenführen bei Doppelbesitz (Nutzer hat Quell- und Zielspiel) | entschieden 2026-11-03 | **Option A:** Zieleintrag bleibt; leere Felder des Ziels werden aus dem Quell-Eintrag gefüllt; bei unterschiedlicher Liste gilt Sammlung vor Wunsch- vor Einkaufsliste (wie Klärung 34); Quell-Eintrag wird gelöscht. | 3.3 Schritt 2, 7.1 Krit. 4, B5, B6 |
| F2 | Sichtbarkeit ausgeblendeter Einträge | entschieden 2026-11-03 | **Option A:** nur aus Suche und Entdecken entfernt; Detailseite, Sammlungen und Partien zeigen sie weiter mit Hinweis „nicht mehr im Katalog“. Ein Status „gesperrt“ (Option C) kommt erst bei Bedarf. | E3, 3.2 RLS, 5, R4, KAT-02 (#4) |
| F3 | Umhängen bei Meldung „zusammengeführt“ / „abgelehnt mit Verweis“ | entschieden 2026-11-03 | **Option A:** „zusammengeführt“ hängt den vorläufigen Eintrag automatisch um (wie „freigegeben“); „abgelehnt mit Verweis“ speichert nur den Verweis, der Nutzer entscheidet in der App. | 3.4, 4.4, 5, 7.1 Krit. 5, B8, Spec #13 |
| F4 | Kennzeichnung des Startbestands als manuell gepflegt | entschieden 2026-11-03 | **Option A:** alle beim Einpflegen gesetzten Felder gelten als manuell gepflegt; ein späterer Import ergänzt nur leere Felder. | E4, 3.2 Trigger, S8, R6, B1 |
| F5 | Eigene Kurztexte: Sprache, Autor, Pflicht | entschieden 2026-11-03 | **Deutsch**; Chris schreibt selbst oder prüft und gibt KI-Entwürfe frei; **Pflicht** für alle Startbestandsspiele. Englische Oberflächen zeigen den Sprachhinweis (Klärung 48). | E8, § 2 Abnahme, S4, CSV, `checks.sql`, B3 |
| F6 | Pflegeformat des Startbestands | entschieden 2026-11-03 | **Option A:** CSV im Repo, Deno-Skript prüft und erzeugt die SQL-Datei; nach dem Go-live ist die Datei eingefroren, Korrekturen nur noch im Studio. | E9, 4.3, 4.4, R5, B3 |
| F7 | Herkunft der Fakten | entschieden 2026-11-03 | **Option A, ergänzend C:** Verlagsseiten, Spielschachtel, eigene Kenntnis, ergänzend Wikidata (CC0); keine systematische Übernahme aus BGG; endgültige Bewertung mit der rechtlichen Prüfung vor dem Release. **Verbindliche Randbedingung S3.** | § 2 Abnahme, 4.3 S3, `facts_source`, R2, C1 |
| F8 | Umfang, Abnahme, Vokabular, Erweiterungen | entschieden 2026-11-03 | **Mindestens 180** sichtbare Katalogspiele mit allen Pflicht-Fakten zum Go-live (Ziel ~200); Vokabular schlagen Agents vor, **Chris gibt frei**; Erweiterungen erlaubt und über `base_game_key` vorgemerkt. **Verbindliche Randbedingungen S1, S5, S7.** | § 1, § 2 Abnahme, 4.3, B3a, C0, C1, D1 |
| F9 | Platzhalter-Cover | entschieden 2026-11-03 | **Option A:** ein einheitlicher, neutraler Platzhalter für alle Spiele. | 5, U1, T2 |
### 9.3 Ausstehende Prozessschritte
- Spec-Freigabe durch Chris; danach Antworten F1–F9 in diese Spec einarbeiten (Änderungsprotokoll).
- Spec freigegeben am 2026-11-03, F1–F9 eingearbeitet. Nächste Schritte: 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: Insert-Policy, Tageslimit, Ähnlichkeitsprüfung, Hinweis am vorläufigen Eintrag, Herkunft „aus Import“ (N12).
- Spec #13 (KAT-04) baut auf 3.4 auf: Insert-Policy, Tageslimit, Ähnlichkeitsprüfung, Hinweis am vorläufigen Eintrag inkl. Wechsel auf den Zieleintrag bei „abgelehnt mit Verweis“ (F3), Herkunft „aus Import“ (N12).
- Kein ADR nötig: Die Entscheidungen bleiben innerhalb von ADR-0001/0002 und sind ohne großen Aufwand änderbar (Startbestand ist eine Datei, `manual_fields` eine Spalte).
### 9.4 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 neuem `manual_fields`-Schlüssel; Spalte `alt_titles` der Startbestands-CSV auswerten.
- **KAT-02 (#4):** Weiterleitung bei `merged_into_id`; Verhalten bei `hidden_at` nach F2; Quellenangabe aus `source_*` (KAT-02 Krit. 5); Sprachhinweis über `description_locale`.
- **KAT-03 (#12):** Basisspiel-Verweis (Klärung 5) mit `manual_fields`-Schlüssel; `base_game_key` der CSV auswerten; `repoint_game_references` für die neue Spalte prüfen (Fremdschlüssel-Wächter).
- **KAT-02 (#4):** Weiterleitung bei `merged_into_id`; bei `hidden_at` Detailseite weiter anzeigen mit Hinweis „nicht mehr im Katalog“ (F2); Quellenangabe aus `source_*` (KAT-02 Krit. 5); Sprachhinweis über `description_locale` (Startbestand: Deutsch).
- **KAT-03 (#12):** Basisspiel-Verweis (Klärung 5) mit `manual_fields`-Schlüssel; `base_game_key` der CSV auswerten und Erweiterungen des Startbestands verknüpfen (S7); `repoint_game_references` für die neue Spalte prüfen (Fremdschlüssel-Wächter).
- **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 F1 („leere Felder des Ziels aus der Quelle füllen“) aufnehmen.
- **SAM-08 (#25):** Import setzt `set local shuffle.catalog_import = 'on'`, pflegt `source_*` und `source_synced_at`, legt Import-Meldungen mit `origin = 'import'` an (N12, N12a).
- **KTO-05 (#23):** `game_reports` wird per `on delete cascade` mit dem Profil gelöscht; im Export enthalten.
@ -513,3 +535,16 @@ Fachliche Fragen, die ich nicht selbst entscheiden kann. Jede mit Optionen und E
Erste Fassung durch architect, wartet auf Freigabe und Antworten zu F1–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 F1–F9
Chris hat die Spec freigegeben und alle Fragen wie empfohlen entschieden. Eingearbeitet:
- Status auf „freigegeben“; § 9.2 als Entscheidungstabelle.
- E3, E4, E8, E9 mit den Entscheidungen F2, F4, F5, F6.
- § 2: Abnahme von Krit. 7 (mindestens 180 sichtbare Katalogspiele, `checks.sql`) und Krit. 10 (Pflicht-Fakten, deutscher Kurztext, kein Cover, Herkunft nach F7) konkretisiert.
- 4.3: verbindliche Randbedingungen S1–S8 für den Startbestand; CSV-Format mit Pflicht-Kurztext (`de`), optionaler Spalte `facts_source` und Prüfung von `base_game_key`; `checks.sql` mit Soll/Ist je Regel.
- 3.3/3.4: Kollisionsregel F1 und Umhäng-Regel F3 ausformuliert.
- 5: einheitlicher Platzhalter (F9), Hinweis „nicht mehr im Katalog“ (F2).
- 7.1 und 8: Tests und Aufgaben nachgezogen; neue Tasks B3a (Vokabular-Vorschlag) und C0 (Freigabe Vokabular durch Chris); Abhängigkeiten von offenen Fragen entfernt; D1 um die Release-Prüfung S1 ergänzt.
- 9.4: Hinweis für SAM-02/SAM-04 zur Kollisionsregel.