docs(catalog): Spec für KAT-05 Katalogdaten (freigegeben) #51
2 changed files with 125 additions and 75 deletions
|
|
@ -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 `<Zieltabelle>_id`. |
|
||||
| E6 | **Anzeige-Erlaubnis je Quelle (Krit. 6): Daten ohne Erlaubnis werden gar nicht gespeichert.** Die View `catalog_games` blendet `description` und `image_path` zusätzlich aus, wenn die Quelle die Anzeige nicht erlaubt (zweite Sicherung). **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/<ts>_catalog_foundation.sql` (R1.1)
|
||||
|
||||
Nur additiv. Bestehende Dateien bleiben unverändert; die Änderung der Rechte auf `games` erfolgt über `revoke`/`grant` in der neuen Datei.
|
||||
|
||||
**Rechte-Regel für alle neuen Objekte (2a–2c):** Supabase vergibt für neue Tabellen, Views und Funktionen standardmäßig alle Rechte an `anon`, `authenticated` und `service_role`. Jede neue Tabelle und View bekommt deshalb zuerst `revoke all … from anon, authenticated` und danach nur die Grants aus der Tabelle unten (wie `catalog.sql` und `supabase/README.md`). Jede neue Funktion bekommt `revoke execute … from public, anon, authenticated, service_role`; `service_role` braucht in R1 keine dieser Funktionen (keine Edge Function), der Import ergänzt später gezielt Rechte für `catalog_importer`.
|
||||
**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/<ts>_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/<ts>_game_reports.sql` (R1.2)
|
||||
|
||||
**Trigger `games_visibility_immutable`** (E14, Entscheidung Chris W-a): `before update on public.games`; ist `new.visibility is distinct from old.visibility` oder `new.owner_id is distinct from old.owner_id`, Fehler `visibility_immutable` (P0001). Gilt für alle Rollen einschließlich `postgres`. Das Konto-Löschen ist nicht betroffen (private Spiele werden per `on delete cascade` gelöscht, nicht geändert).
|
||||
|
||||
**Neue Tabelle `game_reports`** (Klärungen 6, 10, 26, 27, N12, N12a; E12)
|
||||
|
||||
| Spalte | Typ | Hinweis |
|
||||
|
|
@ -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:<key>`, Genre `genre:<vocab_key>`, Mechanik `mechanic:<vocab_key>` (`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 (<Startbestands-IDs>)`, 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('<quelle>', '<ziel>');`, 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', '<ziel>')`), 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('<quelle>', '<ziel>');`, Ergebnis-JSON lesen (Kollisionen nach Spec-F1, Varianten, `bgg_id_conflict`/`source_conflict` mit den alten Werten), Fehlertexte |
|
||||
| Meldungen bearbeiten | 2c | offene Meldungen abfragen (älteste zuerst), Ähnliche im Katalog suchen, Entscheidung: freigeben, zusammenführen (`'merged'`), ablehnen mit oder ohne Verweis (`'rejected'`); Fehlertexte. **Verboten: einen privaten Eintrag öffentlich schalten** (der Trigger lehnt es ab, E14). **Freigeben:** neuen Katalogeintrag anlegen und dabei **nur Fakten aus den Meldungsfeldern** übernehmen (`title`, `publisher_name`, `year`; `link` nur als Recherchehilfe), **nie** `description` oder `image_path` des vorläufigen Eintrags; Kurztext selbst schreiben (SB4); dann `admin_resolve_report(…, 'approved', '<ziel>')`. **Editionen:** werden nicht kopiert; soll die Edition des Nutzers erhalten bleiben, vorher am Katalogeintrag eine Variante anlegen, und zwar ebenfalls nur aus belegbaren Fakten (Name und Jahr der Edition aus Verlags- oder Schachtelangaben), nie durch Übernahme der privaten Variante |
|
||||
| Admin-Rolle in der App | 2a | in R1 nicht vergeben (E10), Vorgehen für R4 |
|
||||
|
||||
Jeder SQL-Baustein im Runbook wird in pgTAP mindestens einmal genau so ausgeführt (`06` für die Abschnitte aus 2a, `07` für 2b, `08` für 2c; siehe 7.1), damit das Runbook nicht veraltet.
|
||||
|
|
@ -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 `<ts>_catalog_foundation.sql` nach 3.2: `catalog_sources` (+ Zeile `bgg`), neue Spalten in `games` mit Checks, `on delete`-Regeln und Indizes, `publishers`, `game_publishers`, Trigger `games_mark_manual_fields` (Import-Ausnahme nur für Rolle `catalog_importer` + `set local`) 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 `<ts>_catalog_foundation.sql` nach 3.2: `catalog_sources` (+ Zeile `bgg`), neue Spalten in `games` mit Checks, `on delete`-Regeln und Indizes, `publishers`, `game_publishers`, Trigger `games_mark_manual_fields` (Import-Ausnahme nur für Rolle `catalog_importer` + `set local`), `game_links_mark_manual` (insert/update/delete) und `games_merged_into_catalog`, View `catalog_games`, Spaltenrechte auf `games` (ohne `id`), RLS, Policies und Grants laut Grant-Tabelle | – |
|
||||
| B2 | backend-dev | pgTAP `06_catalog_foundation.test.sql` nach 7.1 (Krit. 1, 2, 4, 6, 8, 9, 10, Sicherheit Krit. 2) inkl. Runbook-Bausteine aus 2a; bestehende Tests mit Startbestand lauffähig halten; `00_schema.test.sql` auf 20 Tabellen; README-Zahlen (3 Views mit `catalog_games`) | B1 |
|
||||
| B3a | backend-dev | Vokabular-Vorschlag (ca. 12–15 Genres, 20–30 Mechaniken, je `vocab_key`, `name_de`, `name_en`) als `supabase/catalog/vocabulary.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 `<ts>_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 `<ts>_catalog_merge.sql` nach 3.3: `repoint_game_references(p_from, p_to)` (Prüfungen, Variantenfall aus der Sichtbarkeit, Namensabgleich mit fester Auswahlregel, Kollisionsregel Spec-F1 über `variant_map`), `admin_merge_games` (Rollenprüfung, Übertragung `bgg_id`/`source_*`, Konflikte mit alten Werten), Rechte (`revoke execute` inkl. `service_role`) | 2a gemergt |
|
||||
| B6 | backend-dev | pgTAP `07_catalog_merge.test.sql` inkl. Prüfungen von `repoint_game_references`, Kollisionsfällen, Variantenfällen (auch mehrere gleichnamige Zielvarianten), `bgg_id`-Übertragung und -Konflikt, Fremdschlüssel-Wächter | B5 |
|
||||
| B7 | backend-dev | Runbook: Abschnitt Zusammenführen (SQL-Bausteine identisch zu den Tests) | B5, B6 |
|
||||
|
||||
### 8.3 PR 2c – Meldungen (R1.2), Branch `feature/2-katalog-meldungen`
|
||||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| B8 | backend-dev | Migration `<ts>_game_reports.sql` nach 3.4: 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 `<ts>_game_reports.sql` nach 3.4: Trigger `games_visibility_immutable`, Tabelle, Checks, `on delete`-Regeln, Indizes, RLS (nur Lesen der eigenen), `admin_resolve_report` (Umhängen bei `approved`/`merged` ohne Varianten-Kopie; nur Verweis bei `rejected` mit Ziel), `admin_merge_games` per `create or replace` erweitert und **`revoke execute` danach ausdrücklich wiederholt** | 2b gemergt |
|
||||
| B9 | backend-dev | pgTAP `08_game_reports.test.sql` (inkl. `visibility_immutable` auch als `postgres`, Rechte von `admin_merge_games` erneut, Konto-Löschen nach Freigabe); Fremdschlüssel-Wächter um `game_reports.target_game_id` (behandelt) und `game_reports.provisional_game_id` (bewusst ausgelassen) erweitern; `00_schema.test.sql` auf 21 Tabellen; README-Zahlen | B8 |
|
||||
| B10 | backend-dev | Runbook: Abschnitt Meldungen bearbeiten (Verbot „öffentlich schalten“, nur Fakten aus Meldungsfeldern, Editionen nur aus belegbaren Fakten) | B8, B9 |
|
||||
|
||||
### 8.4 Reihenfolge und Parallelität
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
Loading…
Reference in a new issue