docs(db): Meldungen (KAT-04) in Datenmodell, Katalogpflege, README und Umsetzungs-Nachtrag
Refs #13 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0826c97471
commit
3135ed6381
3 changed files with 33 additions and 2 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Dieses Dokument beschreibt, wie der Admin (Chris) den Katalog in R1 pflegt: ausschließlich über Supabase Studio bzw. `psql`, es gibt keine Admin-Oberfläche in der App (Klärungen 25/44). Grundlage ist die Spec `docs/specs/2-katalog-daten.md`.
|
Dieses Dokument beschreibt, wie der Admin (Chris) den Katalog in R1 pflegt: ausschließlich über Supabase Studio bzw. `psql`, es gibt keine Admin-Oberfläche in der App (Klärungen 25/44). Grundlage ist die Spec `docs/specs/2-katalog-daten.md`.
|
||||||
|
|
||||||
Stand: PR 3a (Datenmodell, Startbestand, Ausblenden, Zusammenführen, Meldungen bearbeiten, alternative Titel und Suche).
|
Stand: PR 13a (Datenmodell, Startbestand, Ausblenden, Zusammenführen, Meldungen bearbeiten inkl. Nutzer-Meldungen, alternative Titel und Suche).
|
||||||
|
|
||||||
**Die SQL-Bausteine hier laufen unverändert in den pgTAP-Tests** (Abschnitte 3 und 4 in `supabase/tests/database/06_catalog_foundation.test.sql`, Abschnitt 5 in `07_catalog_merge.test.sql`, Abschnitt 6 in `08_game_reports.test.sql`, Abschnitte 3.1, 5 (Schritt 2b) und 8 in `09_catalog_search.test.sql`; Kommentare mit Abschnittsverweis). Ändert sich ein Baustein, muss der Test mitgezogen werden, damit das Runbook nicht veraltet. Die Bausteine stehen ohne `begin;`/`commit;`, im Studio bzw. in `psql` wird alles in eine Transaktion gepackt (siehe 1).
|
**Die SQL-Bausteine hier laufen unverändert in den pgTAP-Tests** (Abschnitte 3 und 4 in `supabase/tests/database/06_catalog_foundation.test.sql`, Abschnitt 5 in `07_catalog_merge.test.sql`, Abschnitt 6 in `08_game_reports.test.sql`, Abschnitte 3.1, 5 (Schritt 2b) und 8 in `09_catalog_search.test.sql`; Kommentare mit Abschnittsverweis). Ändert sich ein Baustein, muss der Test mitgezogen werden, damit das Runbook nicht veraltet. Die Bausteine stehen ohne `begin;`/`commit;`, im Studio bzw. in `psql` wird alles in eine Transaktion gepackt (siehe 1).
|
||||||
|
|
||||||
|
|
@ -401,6 +401,12 @@ Nutzer melden fehlende Spiele (KAT-04, #13); Meldungen, die ein Import anlegt, g
|
||||||
| **zusammenführen** | `'merged'` + Ziel | Der Eintrag war **schon vorhanden**. Wirkung wie bei `'approved'`. |
|
| **zusammenführen** | `'merged'` + Ziel | Der Eintrag war **schon vorhanden**. Wirkung wie bei `'approved'`. |
|
||||||
| **ablehnen** | `'rejected'`, optional mit Ziel | Mit Ziel wird nur der Verweis gespeichert (die App kann dem Nutzer den Zieleintrag anbieten); der vorläufige Eintrag bleibt unverändert. Ohne Ziel ändert sich nichts außer Status und Zeitpunkt. |
|
| **ablehnen** | `'rejected'`, optional mit Ziel | Mit Ziel wird nur der Verweis gespeichert (die App kann dem Nutzer den Zieleintrag anbieten); der vorläufige Eintrag bleibt unverändert. Ohne Ziel ändert sich nichts außer Status und Zeitpunkt. |
|
||||||
|
|
||||||
|
**Nutzer-Meldungen (KAT-04, ab PR 13a):** Jede Meldung, die ein Nutzer in der App abschickt, hat einen **vorläufigen Eintrag** (`provisional_game_id`), sofern der Nutzer ihn nicht inzwischen gelöscht hat (dann ist die Spalte leer; `approved`/`merged` ändern dann nur Status und Ziel). Die Meldung entsteht über die RPC `report_missing_game` (höchstens 10 Meldungen je Nutzer und Berliner Kalendertag; Import-Meldungen zählen nicht). Daraus folgt für die Bearbeitung:
|
||||||
|
|
||||||
|
- **Bei „abgelehnt mit Verweis“ (`'rejected'` + Ziel) hängt der Admin nichts um.** Der Nutzer entscheidet selbst, ob er auf den Zieleintrag wechselt (Funktion `adopt_report_target`, die dieselbe Umhängelogik `repoint_game_references` nutzt). Status und Ziel nur über `admin_resolve_report` setzen, nie von Hand: Die App leitet den Hinweis an den Nutzer aus Status, Ziel, `merged_into_id` und `result_seen_at` ab.
|
||||||
|
- **Vor `approved` die Edition berücksichtigen** (Regel „Editionen werden nicht kopiert“ unten): Die App zeigt dem Nutzer nach einer Freigabe den Hinweis „Edition neu wählen“, wenn `editions_cleared > 0`.
|
||||||
|
- Der Nutzer kann nur `result_seen_at` ändern (Hinweis „Verstanden“); das Ergebnis der Bearbeitung kann er nicht beeinflussen.
|
||||||
|
|
||||||
**Regeln**
|
**Regeln**
|
||||||
|
|
||||||
- **Nie einen privaten Eintrag öffentlich schalten.** `visibility` und `owner_id` eines Spiels lassen sich nicht ändern (der Trigger `games_visibility_immutable` lehnt es für jede Rolle ab, auch für `postgres`, Fehler `visibility_immutable`). Freigeben heißt immer: **neuen** Katalogeintrag anlegen.
|
- **Nie einen privaten Eintrag öffentlich schalten.** `visibility` und `owner_id` eines Spiels lassen sich nicht ändern (der Trigger `games_visibility_immutable` lehnt es für jede Rolle ab, auch für `postgres`, Fehler `visibility_immutable`). Freigeben heißt immer: **neuen** Katalogeintrag anlegen.
|
||||||
|
|
@ -533,7 +539,7 @@ where r.id = '7c1d2c3d-0000-4000-8000-000000000001';
|
||||||
|
|
||||||
**Falsch gesetzte `visibility` oder `owner_id`** lassen sich nicht mit `update` korrigieren (Trigger `games_visibility_immutable`, Fehler `visibility_immutable`). Korrigieren geht nur durch Löschen und Neuanlegen des Spiels oder, bewusst und nur als Tabelleneigentümer in einer Transaktion, mit `alter table public.games disable trigger games_visibility_immutable;` … `enable trigger …`. Für Clients: Der Fehler `P0001` kommt über PostgREST als HTTP 400 an (Hinweis für die Fehlerabbildung in #3 und #13).
|
**Falsch gesetzte `visibility` oder `owner_id`** lassen sich nicht mit `update` korrigieren (Trigger `games_visibility_immutable`, Fehler `visibility_immutable`). Korrigieren geht nur durch Löschen und Neuanlegen des Spiels oder, bewusst und nur als Tabelleneigentümer in einer Transaktion, mit `alter table public.games disable trigger games_visibility_immutable;` … `enable trigger …`. Für Clients: Der Fehler `P0001` kommt über PostgREST als HTTP 400 an (Hinweis für die Fehlerabbildung in #3 und #13).
|
||||||
|
|
||||||
Ein Aufruf über die App-Schnittstelle (als `anon` oder `authenticated`, auch mit Admin-Claims) scheitert mit `42501`; die Funktion ist nur für die Datenbankrolle ausführbar (E10). Hinweis: Die Rolle `service_role` hat (Supabase-Standard) volle Tabellenrechte und umgeht RLS; die Zugriffsgrenzen in Krit. 9 gelten für die API-Rollen `anon` und `authenticated`, den Funktionen ist `service_role` ausdrücklich entzogen. Meldungen anlegen, ändern oder löschen können Nutzer in dieser Etappe nicht (das Anlegen kommt mit KAT-04); sie lesen nur ihre eigenen. Das Löschen eines Zielspiels, auf das eine Meldung verweist, schlägt mit `23503` fehl (gewollt: ausblenden statt löschen).
|
Ein Aufruf über die App-Schnittstelle (als `anon` oder `authenticated`, auch mit Admin-Claims) scheitert mit `42501`; die Funktion ist nur für die Datenbankrolle ausführbar (E10). Hinweis: Die Rolle `service_role` hat (Supabase-Standard) volle Tabellenrechte und umgeht RLS; die Zugriffsgrenzen in Krit. 9 gelten für die API-Rollen `anon` und `authenticated`, den Funktionen ist `service_role` ausdrücklich entzogen. Nutzer können ab KAT-04 (PR 13a) eigene Meldungen nur über die RPC `report_missing_game` bzw. einen Insert mit eng begrenzten Spaltenrechten anlegen und an eigenen, bearbeiteten Meldungen nur `result_seen_at` ändern; Status, Ziel und Löschen bleiben ihnen verwehrt, sie lesen nur ihre eigenen. Das Löschen eines Zielspiels, auf das eine Meldung verweist, schlägt mit `23503` fehl (gewollt: ausblenden statt löschen).
|
||||||
|
|
||||||
## 7. Admin-Rolle in der App
|
## 7. Admin-Rolle in der App
|
||||||
|
|
||||||
|
|
|
||||||
24
docs/specs/13-umsetzung.md
Normal file
24
docs/specs/13-umsetzung.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Spec #13 – KAT-04 Fehlendes Spiel melden: Umsetzungs-Nachtrag PR 13a (Backend)
|
||||||
|
|
||||||
|
Ergänzt `docs/specs/13-fehlendes-spiel-melden.md` (die Spec selbst wird hier nicht geändert). Stand: 2026-11-04, Branch `feature/13a-fehlendes-spiel-backend` (die Spec nennt `feature/13a-meldungen-backend`), Migration `20261104100000_game_reports_user.sql`, Tests `supabase/tests/database/10_game_reports_user.test.sql`.
|
||||||
|
|
||||||
|
## 1. Abweichungen und Präzisierungen zum Vertrag
|
||||||
|
|
||||||
|
- **Spec-F6 ist entschieden und umgesetzt (2c):** `game_reports.editions_cleared` und die Zuweisung in `admin_resolve_report` stehen schon in der Migration `game_reports` (PR 2c). 13a legt die Spalte nicht an und ändert `admin_resolve_report` nicht; `adopt_report_target` setzt `editions_cleared` genauso.
|
||||||
|
- **Tageslimit gilt auch für `postgres`:** Der Trigger `game_reports_daily_limit` unterscheidet nicht nach Rolle (Spec 3.2). Fixtures, die als `postgres` mehr als 10 Nutzer-Meldungen „von heute“ für einen Nutzer anlegen, scheitern deshalb. Folge für die Tests: `08_game_reports.test.sql` schaltet den Trigger am Anfang ab (`alter table … disable trigger`, mit dem Rollback am Ende zurückgenommen); in `10` bekommen Fixtures für gestern/älter feste Zeitstempel. Außerdem ist `created_at` nach dem Anlegen unveränderlich (Trigger `set_updated_at`), alte Zeitstempel müssen also beim Einfügen gesetzt werden.
|
||||||
|
- **Limit-Zählung über `report_day(created_at) = report_day(now())`** (nicht die optionale Bereichsform aus Spec 3.2); bei höchstens 10 Zeilen je Nutzer und Tag spielt der Index `(owner_id, created_at)` keine Rolle.
|
||||||
|
- **Trimmen:** `report_missing_game` trimmt Leerzeichen, Tab, CR und LF (nicht nur Leerzeichen). Titel, Verlag und Link werden vor allen Prüfungen getrimmt, Längen gelten für den getrimmten Wert.
|
||||||
|
- **`find_similar_catalog_games`:** Der Titel wird vor der Normalisierung getrimmt und die Länge (1 bis 200) am getrimmten Wert geprüft. Ist der normalisierte Titel leer (kann nach `normalize_game_title` nur bei leerem Rest vorkommen), gibt es keine Treffer. Je Spiel wird der beste Kandidatentitel gewählt: gleicher Schlüssel vor Ähnlichkeit, dann Haupttitel vor alternativen Titeln, dann Schreibweise. Verlage sind nach `publishers.name_search`, dann `name` sortiert (wie in `search_catalog_games`). Bei gleicher Ähnlichkeit sortiert das Ergebnis nach `title`, dann `game_id`. Die Ähnlichkeit bei `same_title` ist immer `1.0`.
|
||||||
|
- **`report_provisional_in_use`:** Der Handler um den Insert in `game_reports` wertet den Namen des verletzten Index (`game_reports_provisional_game_id_key`) aus; andere Unique-Verletzungen laufen unverändert weiter. Über die RPC ist der Fall nicht erreichbar (jeder Aufruf legt ein frisches Spiel an); der Test lenkt ihn mit einem temporären Trigger herbei.
|
||||||
|
- **`adopt_report_target`:** Alle Prüfungen laufen nach dem Sperren von Meldung und Spielen, in der Reihenfolge der Spec-Tabelle. Eine Meldung, die nicht dem Aufrufer gehört, wird schon beim Sperren nicht gefunden (`report_not_found`, `p_report = null` ebenso). `current_user` wird nirgends ausgewertet.
|
||||||
|
- **Eigentümer von `adopt_report_target`:** Die Migration ändert den Eigentümer nicht ausdrücklich (`alter function … owner to`); sie läuft als `postgres` (lokal und per Supabase CLI). Der Test prüft `proowner = postgres`.
|
||||||
|
- **Testplan:** `00_schema` unverändert (22 Tabellen), `07`/`08`-Fremdschlüssel-Wächter unberührt (kein neuer Fremdschlüssel), `08` nur um das Abschalten des Limit-Triggers ergänzt (Erwartungen und Beschreibungen unverändert gültig: die Spaltenrechte aus 08 waren schon auf `result_seen_at` und die Insert-Spalten ausgelegt).
|
||||||
|
|
||||||
|
## 2. Mutationsprüfung
|
||||||
|
|
||||||
|
Die Mutanten sind veränderte Kopien der Migration, jeweils in einer Transaktion zusammen mit dem Testlauf angewendet und mit dem Rollback des Tests zurückgenommen (am echten Schema bleibt nichts). Die Skripte liegen nicht im Repo. Ergebnis siehe Bericht an Chris; die überlebenden Mutanten sind entweder Doppelabsicherungen (zum Beispiel `origin = 'user'` in der Insert-Policy, die schon durch die Spaltenrechte gesichert ist) oder nur mit Parallelität testbar (Advisory-Lock).
|
||||||
|
|
||||||
|
## 3. Offene Punkte
|
||||||
|
|
||||||
|
- Zeitpunkt des Hinweises: Klickt der Nutzer den Hinweis „abgelehnt mit Verweis“ weg (`result_seen_at` gesetzt) und wechselt danach auf den Zieleintrag, bleibt `result_seen_at` gesetzt (Spec 4.4: unverändert). Der Hinweis „Dein Eintrag zeigt jetzt auf …“ erscheint dann am Zieleintrag nicht mehr. Siehe Bericht, Frage an Chris.
|
||||||
|
- Paralleltest des Limits (zwei `psql`-Sitzungen unter READ COMMITTED) ist nicht automatisiert (Spec 7.1) und gehört zu T1.
|
||||||
|
|
@ -45,6 +45,7 @@ supabase test db # alle pgTAP-Tests ausführen
|
||||||
|
|
||||||
## Katalogsuche (KAT-01)
|
## Katalogsuche (KAT-01)
|
||||||
- `public.search_normalize(text)` (Normalisierung für Suche und Titelvergleich) und die RPC `public.search_catalog_games(…)` sind **für API-Rollen ausführbar** (`search_normalize`: `anon`, `authenticated`, `service_role`, weil generierte Spalten mit den Rechten des Schreibenden rechnen; `search_catalog_games`: `anon`, `authenticated`). Beides ist die Ausnahme von der Regel „Funktionen sind für niemanden aufrufbar“; die Rechte prüft `tests/database/09_catalog_search.test.sql`.
|
- `public.search_normalize(text)` (Normalisierung für Suche und Titelvergleich) und die RPC `public.search_catalog_games(…)` sind **für API-Rollen ausführbar** (`search_normalize`: `anon`, `authenticated`, `service_role`, weil generierte Spalten mit den Rechten des Schreibenden rechnen; `search_catalog_games`: `anon`, `authenticated`). Beides ist die Ausnahme von der Regel „Funktionen sind für niemanden aufrufbar“; die Rechte prüft `tests/database/09_catalog_search.test.sql`.
|
||||||
|
- **Fehlendes Spiel melden (KAT-04, `tests/database/10_game_reports_user.test.sql`):** `public.find_similar_catalog_games(titel, jahr)`, `public.report_missing_game(...)` und `public.adopt_report_target(meldung)` sind nur für `authenticated` ausführbar (`anon` und `service_role` nicht); ebenso die Hilfsfunktionen `strip_leading_article`, `normalize_game_title` und `report_day`. `adopt_report_target` ist die einzige `security definer`-Funktion dieser Gruppe (Eigentümer `postgres`, `search_path` leer, Aufrufer nur über `auth.uid()`, ruft keine `admin_*`-Funktion auf). `authenticated` darf in `game_reports` nur `owner_id, title, publisher_name, year, link, provisional_game_id` einfügen und nur `result_seen_at` ändern. Das Tageslimit (10 Nutzer-Meldungen je Berliner Tag) setzt ein Trigger durch, der unter READ COMMITTED (Standard bei PostgREST) wirkt; er gilt für jede Rolle, auch `postgres`. Vertrag und Fehlercodes: `docs/datenmodell-r1.md`, Abschnitt „Meldungen (KAT-04)“.
|
||||||
- Alternative Titel (`game_alt_titles`) sind für API-Rollen nur lesbar; Pflege über Studio (`docs/katalog-pflege.md`, Abschnitt 3.1) und die CSV-Spalte `alt_titles` (`de:Titel;en:Title`, `;` im Titel nicht erlaubt).
|
- Alternative Titel (`game_alt_titles`) sind für API-Rollen nur lesbar; Pflege über Studio (`docs/katalog-pflege.md`, Abschnitt 3.1) und die CSV-Spalte `alt_titles` (`de:Titel;en:Title`, `;` im Titel nicht erlaubt).
|
||||||
- Nach einem Image-Upgrade der Datenbank die Normalisierung prüfen (`docs/katalog-pflege.md`, Abschnitt 8).
|
- Nach einem Image-Upgrade der Datenbank die Normalisierung prüfen (`docs/katalog-pflege.md`, Abschnitt 8).
|
||||||
- Messskript für das Zeitbudget der Suche (nur lokal, nie gegen Staging/Produktion, bewusst nicht unter `tests/`): `perf/catalog_search_perf.sql`, Aufruf im Kopfkommentar.
|
- Messskript für das Zeitbudget der Suche (nur lokal, nie gegen Staging/Produktion, bewusst nicht unter `tests/`): `perf/catalog_search_perf.sql`, Aufruf im Kopfkommentar.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue