docs(catalog): address re-review of KAT-05 spec and record clarification N15
All checks were successful
All checks were successful
Visibility is immutable, variant copying is derived from visibility, startbestand idempotency and vocabulary handling are defined, and the edition deviation from KAT-04 criterion 4 is recorded as N15. Refs #2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8c57e05458
commit
6dd5854eab
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