docs(catalog): Review-Befunde zur Spec KAT-04 eingearbeitet
Tageslimit-Rechte (report_day), Normalisierung als Wrapper um search_normalize aus Spec #3, Sperrreihenfolge und Rechte von adopt_report_target, Indizes aus 2c, Testdatei 10, Edition-Hinweis mit editions_cleared als Empfehlung. Refs #13 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ece8e3eef2
commit
f7a7c1e54c
1 changed files with 218 additions and 227 deletions
|
|
@ -3,12 +3,12 @@
|
|||
| | |
|
||||
|---|---|
|
||||
| Issue | #13 (KAT-04) |
|
||||
| Status | Entwurf, wartet auf Freigabe durch Chris und Antworten zu Spec-F1 bis Spec-F9 (§ 9.2) |
|
||||
| Status | Entwurf, Review-Befunde eingearbeitet; wartet auf Freigabe durch Chris und Antworten zu Spec-F1 bis Spec-F9 (§ 9.2) |
|
||||
| Autor | architect |
|
||||
| Stand | 2026-11-03 |
|
||||
| Etappe | R1.2 Sammlung |
|
||||
| Abhängigkeiten | **#2 PR 2b und 2c müssen gemergt sein**, bevor 13a beginnt (Spec #2 E12): `game_reports`, `admin_resolve_report`, `repoint_game_references`, Trigger `games_visibility_immutable`. #1 (Anmeldung), #3 KAT-01 (Einstieg aus der leeren Suche, Krit. 6). Fachlich zusätzlich #6 SAM-01 und #4 KAT-02 (Orte für „in die Sammlung“ und den Hinweis „am Eintrag“), in Forgejo bisher **nicht** als Abhängigkeit gesetzt (Spec-F8). |
|
||||
| Quellen | Issue #13 inkl. Kommentare (u. a. Hinweis aus PR #51 vom 2026-11-03), `requirements/20-r1-akzeptanzkriterien.md` § KAT-04 und § Querschnitt, `requirements/r1-klaerungen.md` (6, 10, 12, 15, 20, 23, 26, 27, 28, 36, 44/25, 45, N12, N12a, N15), `requirements/backlog.md`, `docs/specs/2-katalog-daten.md` (E10–E14, 3.2–3.4, 4.4, 9.5), `docs/datenmodell-r1.md`, `supabase/migrations/20261102100200_catalog.sql`, `…100300_collection.sql`, `…100400_plays.sql`, `…20261103142204_catalog_foundation.sql`, CLAUDE.md |
|
||||
| Abhängigkeiten | **Vor 13a gemergt:** #2 PR 2b und 2c (Spec #2 E12: `game_reports`, `admin_resolve_report`, `repoint_game_references`, Trigger `games_visibility_immutable`) und **#3 KAT-01** (`search_normalize`, `game_alt_titles`, Einstieg aus der leeren Suche, Krit. 6). Außerdem #1. Fachlich zusätzlich #6 SAM-01 und #4 KAT-02 (Orte für „in die Sammlung“ und den Hinweis „am Eintrag“), in Forgejo bisher **nicht** als Abhängigkeit gesetzt (Spec-F8). |
|
||||
| Quellen | Issue #13 inkl. Kommentare (u. a. Hinweis aus PR #51 vom 2026-11-03), `requirements/20-r1-akzeptanzkriterien.md` § KAT-04 und § Querschnitt, `requirements/r1-klaerungen.md` (6, 10, 12, 15, 20, 23, 26, 27, 28, 36, 44/25, 45, N12, N12a, N15), `requirements/backlog.md`, `docs/specs/2-katalog-daten.md` (E10–E14, 3.2–3.4, 4.4, 9.5), Spec #3 (`search_normalize`, `game_alt_titles`), `docs/datenmodell-r1.md`, `supabase/migrations/…catalog.sql`, `…collection.sql`, `…plays.sql`, `…catalog_foundation.sql`, CLAUDE.md |
|
||||
|
||||
**Namenskonvention:** „Spec-F1“ bis „Spec-F9“ sind die Fragen an Chris aus § 9.2. „E1“ usw. sind Kernentscheidungen dieser Spec, „#2 E12“ verweist auf Spec #2. „B1“, „F1“, „U1“, „T1“ sind Aufgaben (§ 8).
|
||||
|
||||
|
|
@ -16,24 +16,24 @@
|
|||
|
||||
Ein Nutzer, der ein Spiel im Katalog nicht findet, meldet es mit wenigen Angaben und kann es **sofort** als vorläufigen privaten Eintrag nutzen (Entscheidung O5). Der Admin (Chris) prüft jede Meldung manuell im Studio (Klärungen 27, 44; Werkzeug aus #2 PR 2c). Das Ergebnis sieht der Nutzer als Hinweis in der App, nie als E-Mail (Klärungen 26, 45).
|
||||
|
||||
Diese Spec baut auf der Meldungstabelle `game_reports` und der Funktion `admin_resolve_report` aus Spec #2 § 3.4 (PR 2c) auf. Sie ergänzt nur, was der **Nutzer** braucht: Anlegen mit Tageslimit, Ähnlichkeitsprüfung, vorläufigen Eintrag, Ergebnishinweise und das Wechseln auf den Zieleintrag.
|
||||
Diese Spec baut auf `game_reports` und `admin_resolve_report` aus Spec #2 § 3.4 (PR 2c) und auf der Normalisierung aus Spec #3 auf. Sie ergänzt nur, was der **Nutzer** braucht: Anlegen mit Tageslimit, Ähnlichkeitsprüfung, vorläufigen Eintrag, Ergebnishinweise und das Wechseln auf den Zieleintrag.
|
||||
|
||||
**Nicht im Scope** (aus dem Issue): öffentliche Anzeige gemeldeter Spiele, Bild-Upload, E-Mail/Push, Admin-Oberfläche in der App, automatische Import-Meldungen (SAM-08 #25). Außerdem nicht in dieser Spec: Bearbeiten weiterer Felder des vorläufigen Eintrags (Spielerzahl, Dauer usw.), Zurückziehen einer Meldung, Auswahl vorläufiger Spiele in Partien (gehört zu PAR-01 #15 Krit. 3).
|
||||
**Nicht im Scope** (aus dem Issue): öffentliche Anzeige gemeldeter Spiele, Bild-Upload, E-Mail/Push, Admin-Oberfläche in der App, automatische Import-Meldungen (SAM-08 #25). Außerdem nicht: Bearbeiten weiterer Felder des vorläufigen Eintrags, Zurückziehen einer Meldung, Auswahl vorläufiger Spiele in Partien (PAR-01 #15 Krit. 3).
|
||||
|
||||
### Kernentscheidungen (Kurzfassung)
|
||||
|
||||
| # | Entscheidung | Begründung |
|
||||
|---|---|---|
|
||||
| E1 | **Jede Nutzer-Meldung legt genau einen vorläufigen privaten Eintrag an** (1:1, eindeutiger Index auf `game_reports.provisional_game_id`). Er bekommt nur `title` und `year` aus der Meldung. | Der Hinweis zum Ergebnis braucht einen festen Ort, und Krit. 3 verlangt die sofortige Nutzung. Das entspricht dem Backlog („eine Meldung je vorläufigem Eintrag“, N12) und dem Import (SAM-08 Krit. 4). Mit nur Titel und Jahr gibt es außer der Edition nichts, was beim Ersetzen verloren gehen kann (Krit. 4, N15). |
|
||||
| E2 | **Anlegen über eine RPC `report_missing_game` (security invoker)**, die in einer Transaktion Ähnlichkeit prüft, den vorläufigen Eintrag, die Meldung und optional den Listeneintrag anlegt. | Getrennte Client-Aufrufe ließen verwaiste vorläufige Spiele zurück, wenn das Tageslimit die Meldung ablehnt. `security invoker` heißt: RLS, Spaltenrechte und Trigger gelten weiter, die RPC ist nur Bündelung, keine Rechte-Erweiterung. |
|
||||
| E3 | **Schutz liegt in der Datenbank, nicht in der RPC:** Spaltenrechte auf `game_reports` (kein `id`, `created_at`, `origin`, `status`, `target_game_id`, `resolved_at`, `bgg_id`), Insert-Policy und ein Limit-Trigger mit Sperre je Nutzer. | Ein direkter REST-Insert umgeht nur den UX-Schritt der Ähnlichkeitsprüfung, nicht Limit oder Rechte. Weil `created_at` nicht schreibbar ist, lässt sich das Tageslimit nicht durch ein falsches Datum umgehen. |
|
||||
| E4 | **Ähnlichkeitsprüfung serverseitig** über `find_similar_catalog_games` mit `pg_trgm` und einer unveränderlichen (`immutable`) Normalisierungsfunktion `normalize_game_title`. Nur sichtbare Katalogspiele (`catalog_games`). | Klärung 15 ist eine Datenbankregel (Trigramm). Dieselbe Regel prüft die RPC beim Absenden noch einmal; ohne ausdrückliche Bestätigung (`p_confirmed_distinct`) wird nicht gespeichert (Krit. 2). |
|
||||
| E5 | **„Abgelehnt mit Verweis“: Der Nutzer entscheidet selbst**, ob er auf den Zieleintrag wechselt (Spec #2 Spec-F3, entschieden). Dafür gibt es die RPC `adopt_report_target` (security definer) mit strenger Prüfung von Eigentümer und Status; sie nutzt `repoint_game_references` aus 2b. | Krit. 5 verlangt, dass der vorläufige Eintrag „auf den richtigen Eintrag verweist“. Der Verweis steht in `target_game_id`; das Umhängen der eigenen Daten bleibt eine Entscheidung des Nutzers. Das ist der **erste Weg, auf dem ein Nutzer `repoint_game_references` indirekt auslöst**; die Prüfungen sind deshalb ausführlich (4.4). |
|
||||
| E6 | **Ergebnishinweise werden aus vorhandenen Daten abgeleitet** (`status`, `target_game_id`, `merged_into_id` des vorläufigen Eintrags) und lassen sich wegklicken (`game_reports.result_seen_at`, Spec-F5). Die Markierung „abgelehnt“ (Krit. 6) bleibt dauerhaft. | Kein Benachrichtigungssystem in R1 (QUE-01 erst R2). Eine Spalte reicht. |
|
||||
| E7 | **Edition (N15) ohne Änderung an 2c:** Die App zeigt „Edition neu wählen“, wenn der vorläufige Eintrag mindestens eine eigene Variante hatte und der Eintrag des Nutzers am Zielspiel keine Edition mehr hat (Spec-F6). | Die privaten Varianten bleiben am ausgeblendeten vorläufigen Eintrag erhalten und sind für den Eigentümer weiter lesbar (Spec #2 § 3.3). Alternative mit einer Spalte `edition_cleared` steht in Spec-F6. |
|
||||
| E8 | **Verlag bleibt Freitext** in `game_reports.publisher_name`; der vorläufige Eintrag bekommt keinen Verlag. | `publishers` ist nur für den Admin schreibbar (Spec #2 3.2). Der Admin legt den Verlag beim Freigeben selbst an (Runbook). |
|
||||
| E9 | **Keine neuen Pakete, keine Edge Function.** `pg_trgm` ist Teil des Supabase-Postgres-Images und self-hosted verfügbar (ADR-0002). | CLAUDE.md, ADR-0002. |
|
||||
| E10 | **Zwei PRs:** 13a Backend (`Refs #13`), 13b Flutter (`Closes #13`). 13b kann gegen ein gemocktes Repository parallel beginnen, sobald der RPC-Vertrag (4.2–4.5) freigegeben ist. | Der Vertrag steht vollständig in dieser Spec. |
|
||||
| E1 | **Jede Nutzer-Meldung legt genau einen vorläufigen privaten Eintrag an** (1:1; der eindeutige Teilindex auf `provisional_game_id` kommt aus 2c). Er bekommt nur `title` und `year`. | Der Hinweis braucht einen festen Ort, Krit. 3 verlangt die sofortige Nutzung; entspricht Backlog (N12) und Import (SAM-08 Krit. 4). Mit nur Titel und Jahr kann außer der Edition nichts verloren gehen (Krit. 4, N15). |
|
||||
| E2 | **Anlegen über die RPC `report_missing_game` (security invoker)**: Limit-Vorprüfung, Ähnlichkeitsprüfung, vorläufiger Eintrag, Meldung, optional Listeneintrag in einer Transaktion. | Getrennte Client-Aufrufe ließen verwaiste vorläufige Spiele zurück. `security invoker`: RLS, Spaltenrechte und Trigger gelten weiter. |
|
||||
| E3 | **Schutz liegt in der Datenbank:** Spaltenrechte auf `game_reports`, Insert-Policy, Limit-Trigger mit Sperre je Nutzer. | Ein direkter REST-Insert umgeht nur den UX-Schritt der Ähnlichkeitsprüfung, nicht Limit oder Rechte. `created_at` ist nicht schreibbar, das Limit lässt sich nicht durch ein falsches Datum umgehen. |
|
||||
| E4 | **Ähnlichkeitsprüfung serverseitig** über `find_similar_catalog_games` mit `pg_trgm`. Normalisierung: `search_normalize` aus **Spec #3** (Eigentümer #3), dazu in 13a nur der dünne Wrapper `normalize_game_title` (führende Artikel). Haupttitel und alternative Titel (`game_alt_titles`), nur sichtbare Katalogspiele. | Klärung 15 ist eine Datenbankregel. Eine einzige Normalisierung für Suche und Meldung. Die RPC prüft beim Absenden noch einmal; ohne Bestätigung wird nicht gespeichert (Krit. 2). |
|
||||
| E5 | **„Abgelehnt mit Verweis“: Der Nutzer entscheidet selbst** über den Wechsel (Spec #2 Spec-F3) per `adopt_report_target` (security definer, Eigentümer `postgres`), streng geprüft; nutzt `repoint_game_references` aus 2b. | Krit. 5. Erster Weg, auf dem ein Nutzer `repoint_game_references` indirekt auslöst; deshalb ausführliche Prüfungen, feste Sperrreihenfolge und Rechte-Tests (4.4). |
|
||||
| E6 | **Ergebnishinweise werden aus vorhandenen Daten abgeleitet** und lassen sich wegklicken (`result_seen_at`, Spec-F5). Die Markierung „abgelehnt“ (Krit. 6) bleibt dauerhaft. Fehlt der vorläufige Eintrag (vom Nutzer gelöscht), folgt das Ergebnis nur aus `status`. | Kein Benachrichtigungssystem in R1 (QUE-01 erst R2). |
|
||||
| E7 | **Edition (N15):** Empfehlung (Spec-F6) ist ein Zähler `game_reports.editions_cleared` (Nachtrag in 2b/2c, solange beide noch nicht gemergt sind); Rückfall ist die Ableitung aus den Daten (nur Sammlung, `list = 'owned'`). | Der Zähler ist genau und jetzt günstig; die Ableitung braucht keine Änderung an 2b/2c, ist aber ungenau. |
|
||||
| E8 | **Verlag bleibt Freitext** in `game_reports.publisher_name`. | `publishers` ist nur für den Admin schreibbar (Spec #2 3.2). |
|
||||
| E9 | **Keine neuen Pakete, keine Edge Function.** `pg_trgm` gehört zum Supabase-Postgres-Image (ADR-0002). | CLAUDE.md, ADR-0002. |
|
||||
| E10 | **Zwei PRs:** 13a Backend (`Refs #13`), 13b Flutter (`Closes #13`). 13b beginnt gegen ein gemocktes Repository, sobald der RPC-Vertrag (4.2–4.5) freigegeben ist. | Der Vertrag steht vollständig in dieser Spec. |
|
||||
|
||||
## 2. Akzeptanzkriterien
|
||||
|
||||
|
|
@ -48,58 +48,51 @@ Diese Spec baut auf der Meldungstabelle `game_reports` und der Funktion `admin_r
|
|||
7. Gegeben ich habe heute schon 10 Meldungen abgeschickt, wenn ich eine weitere melden will, dann wird sie nicht gespeichert und ich sehe einen Hinweis auf das Tageslimit (Klärung 27). Meldungen, die der BGG-Import automatisch anlegt (SAM-08 Kriterium 4), zählen nicht auf dieses Limit (N12).
|
||||
8. Gegeben ich habe eine Meldung abgeschickt, dann bleibt sie im Status „offen“, bis der Admin sie manuell bearbeitet; eine automatische Freigabe gibt es nicht (Klärungen 6, 27, 44).
|
||||
|
||||
**Abweichung bei Krit. 4 (Klärung N15, entschieden von Chris am 2026-11-03):** Beim Ersetzen bleibt die **Edition** nur erhalten, wenn der Katalogeintrag eine gleichnamige Variante hat; sonst wird sie an Sammlungseintrag und Partien geleert, und die App fordert auf, sie neu zu wählen (E7). Alle anderen eigenen Angaben (Liste, Notiz, Kaufdatum, Angaben aus SAM-02/SAM-04, Partien) bleiben erhalten; das leistet `repoint_game_references` aus 2b.
|
||||
**Abweichung bei Krit. 4 (Klärung N15, entschieden von Chris am 2026-11-03):** Beim Ersetzen bleibt die **Edition** nur erhalten, wenn der Katalogeintrag eine gleichnamige Variante hat; sonst wird sie an Sammlungseintrag und Partien geleert, und die App fordert auf, sie neu zu wählen (E7). Alle anderen eigenen Angaben (Liste, Notiz, Kaufdatum, Angaben aus SAM-02/SAM-04, Partien) bleiben erhalten (`repoint_game_references`, 2b).
|
||||
|
||||
**Lesart von Krit. 2 (Klärung 15):** Die Bedingung „Erscheinungsjahr ± 1“ gehört nur zur Trigramm-Regel. Gleicher normalisierter Titel gilt unabhängig vom Jahr als „sehr ähnlich“. Fehlt das Jahr auf einer Seite, siehe Spec-F2.
|
||||
**Lesart von Krit. 2 (Klärung 15):** „Erscheinungsjahr ± 1“ gehört nur zur Trigramm-Regel; gleicher normalisierter Titel gilt unabhängig vom Jahr. Fehlt ein Jahr, siehe Spec-F2.
|
||||
|
||||
**Lesart von Krit. 1:** Nur der Titel ist Pflicht; Verlag und Jahr sind optional (Spec-F1).
|
||||
|
||||
**Relevante Querschnittskriterien**
|
||||
|
||||
- **Sicherheit der Daten Krit. 1 und 3:** Meldungen und vorläufige Einträge sieht nur der Meldende (und der Admin im Studio). pgTAP prüft fremden Zugriff auf `game_reports`, auf vorläufige Spiele und auf die neuen RPCs.
|
||||
- **Sicherheit der Daten Krit. 1 und 3:** Meldungen und vorläufige Einträge sieht nur der Meldende (und der Admin im Studio). pgTAP prüft fremden Zugriff auf `game_reports`, vorläufige Spiele und die neuen RPCs.
|
||||
- **Sicherheit der Daten Krit. 2:** `anon` kann nichts melden und keine Meldungen lesen.
|
||||
- **Datenschutz (QUE-04):** Meldungen und vorläufige Einträge werden mit dem Konto hart gelöscht (`on delete cascade`, Klärung 10) und gehören in den Export (KTO-05 #23, Klärung 20). Freigegebene Katalogeinträge übernehmen nur Fakten aus den Meldungsfeldern (Runbook aus 2c). Der Link wird in der App nicht geöffnet und nicht vorgeladen (keine Verbindung zu Fremdseiten).
|
||||
- **Sprachen (QUE-05) Krit. 2, 4:** alle Texte, Fehler- und Statushinweise aus ARB (de/en).
|
||||
- **Barrierefreiheit (QUE-06) Krit. 1–4:** Formular mit Feldbeschriftungen, Statushinweise mit Text (nicht nur Farbe), Touch-Ziele ≥ 48 dp, `textScaler` 2.0, Tastaturbedienung im Web inkl. Bestätigungsdialog.
|
||||
- **Zustände, Fehler und Netz Krit. 1–3:** Liste „Meine Meldungen“ mit Lade-, Leer-, Fehler- und Datenzustand; offline Hinweis statt technischem Fehler; bei Limit- oder Netzfehler bleiben die Eingaben im Formular erhalten (Issue: „besonders relevant“).
|
||||
- **Datenschutz (QUE-04):** Meldungen und vorläufige Einträge werden mit dem Konto hart gelöscht (Klärung 10) und gehören in den Export (KTO-05 #23, Klärung 20). Freigegebene Katalogeinträge übernehmen nur Fakten aus den Meldungsfeldern (Runbook aus 2c). Der Link wird in der App nicht geöffnet und nicht vorgeladen.
|
||||
- **Sprachen (QUE-05) Krit. 2, 4:** alle Texte aus ARB (de/en).
|
||||
- **Barrierefreiheit (QUE-06) Krit. 1–4:** Feldbeschriftungen, Status mit Text (nicht nur Farbe), Touch-Ziele ≥ 48 dp, `textScaler` 2.0, Tastaturbedienung im Web inkl. Bestätigungsschritt.
|
||||
- **Zustände, Fehler und Netz Krit. 1–3:** „Meine Meldungen“ mit Lade-, Leer-, Fehler- und Datenzustand; offline Hinweis statt Fehler; bei Limit- oder Netzfehler bleiben die Eingaben erhalten.
|
||||
- **Plattformen Krit. 1, 2:** identisch auf iOS, Android und Web; breites Layout im Web.
|
||||
|
||||
## 3. Datenmodell
|
||||
|
||||
### 3.1 Ausgangslage (nach #2 PR 2c)
|
||||
### 3.1 Ausgangslage (nach #2 PR 2c und #3)
|
||||
|
||||
Spec #2 § 3.4 legt an (Spaltennamen werden hier so vorausgesetzt; weicht 2c davon ab, passt B1 die Namen an):
|
||||
Aus Spec #2 § 3.4 (Spaltennamen werden so vorausgesetzt; weicht 2c ab, passt B1 die Namen an):
|
||||
|
||||
- `game_reports` mit `id`, `owner_id` (FK → `profiles`, `on delete cascade`), `origin` (`user`/`import`, Standard `user`), `title` (1–200), `publisher_name` (≤ 200), `year` (1800–2100), `link` (≤ 500, `^https?://`), `bgg_id`, `provisional_game_id` (FK → `games`, `on delete set null`), `status` (`open`/`approved`/`rejected`/`merged`, Standard `open`), `target_game_id` (FK → `games`, `on delete restrict`), `resolved_at`, `created_at`, `updated_at`; Checks zu Status, Ziel und `resolved_at`; Index `(owner_id, created_at)`; RLS: Lesen nur eigene, **kein** Insert/Update/Delete für `authenticated`.
|
||||
- `admin_resolve_report(p_report, p_status, p_target)`: bei `approved`/`merged` hängt sie den vorläufigen Eintrag per `repoint_game_references` auf das Ziel um und setzt am vorläufigen Eintrag `merged_into_id` und `hidden_at`; bei `rejected` speichert sie nur den Verweis (oder nichts).
|
||||
- `game_reports` mit `id`, `owner_id` (FK → `profiles`, `on delete cascade`), `origin` (`user`/`import`, Standard `user`), `title` (1–200), `publisher_name` (≤ 200), `year` (1800–2100), `link` (≤ 500, `^https?://`), `bgg_id`, `provisional_game_id` (FK → `games`, `on delete set null`), `status` (`open`/`approved`/`rejected`/`merged`), `target_game_id` (FK → `games`, `on delete restrict`), `resolved_at`, `created_at`, `updated_at`; Checks zu Status, Ziel und `resolved_at`.
|
||||
- **Indizes aus 2c:** `(owner_id, created_at)` (Tageslimit), `game_reports_target_game_id_idx` (Hinweis am Zieleintrag), eindeutiger Teilindex auf `provisional_game_id` (E1). **13a legt keine eigenen Indizes an.**
|
||||
- RLS: Lesen nur eigene, kein Insert/Update/Delete für `authenticated`.
|
||||
- `admin_resolve_report(p_report, p_status, p_target)`: bei `approved`/`merged` Umhängen per `repoint_game_references`, am vorläufigen Eintrag `merged_into_id` und `hidden_at`; bei `rejected` nur der Verweis (oder nichts).
|
||||
- Trigger `games_visibility_immutable`: ein vorläufiger Eintrag kann nie öffentlich werden (#2 E14).
|
||||
|
||||
Aus Spec #3: `search_normalize(text)` und die Tabelle `game_alt_titles` (alternative Titel je Spiel mit Sprache). Der Fremdschlüssel `game_alt_titles.game_id` und seine Einordnung im Fremdschlüssel-Wächter gehören zu #3.
|
||||
|
||||
### 3.2 Änderungen in 13a (Migration `supabase/migrations/<ts>_game_reports_user.sql`)
|
||||
|
||||
Nur additiv; der Zeitstempel liegt nach der Migration aus 2c. Rechte-Regel aus Spec #2 § 3.2 gilt: neue Funktionen zuerst `revoke execute … from public, anon, authenticated, service_role`, danach nur die Grants aus 4.1.
|
||||
|
||||
**Erweiterungen**
|
||||
Nur additiv; der Zeitstempel liegt nach den Migrationen aus 2c und #3. Rechte-Regel aus Spec #2 § 3.2: jede neue Funktion zuerst `revoke execute … from public, anon, authenticated, service_role`, danach nur die Grants aus 4.1.
|
||||
|
||||
```
|
||||
create extension if not exists pg_trgm with schema extensions;
|
||||
```
|
||||
|
||||
Falls #3 die Erweiterung schon anlegt, ist die Zeile ein No-op. Kein `unaccent` (Spec-F3).
|
||||
|
||||
**Neue Spalte in `game_reports`**
|
||||
|
||||
| Spalte | Typ | Hinweis |
|
||||
|---|---|---|
|
||||
| `result_seen_at` | timestamptz | nullable; der Nutzer hat den Ergebnishinweis weggeklickt (E6, Spec-F5). Check `result_seen_at is null or status <> 'open'`. |
|
||||
|
||||
**Neue Indizes**
|
||||
|
||||
| Index | Zweck |
|
||||
|---|---|
|
||||
| `unique (provisional_game_id) where provisional_game_id is not null` | 1:1 Meldung ↔ vorläufiger Eintrag (E1). Falls 2c ihn schon anlegt: entfällt. |
|
||||
| `(target_game_id) where target_game_id is not null` | Hinweis am Eintrag: „gibt es eine Meldung von mir mit diesem Ziel?“ (5.4) |
|
||||
|
||||
**Kein Trigramm-Index:** Ein GIN-Index mit `gin_trgm_ops` würde nur über den Operator `%` genutzt, nicht über `similarity(...) >= 0.8` (3.3). Bei ~200–2000 Katalogspielen ist ein sequenzieller Durchlauf schnell genug (einfach vor clever). Braucht KAT-01 einen Trigramm-Index für die Suche, legt #3 ihn an.
|
||||
Bei Spec-F6 = Zähler (Empfehlung) kommt `editions_cleared integer not null default 0` schon in 2c (Nachtrag Spec #2), nicht in 13a.
|
||||
|
||||
**Rechte auf `game_reports`** (E3)
|
||||
|
||||
|
|
@ -109,71 +102,68 @@ grant insert (owner_id, title, publisher_name, year, link, provisional_game_id)
|
|||
grant update (result_seen_at) on public.game_reports to authenticated;
|
||||
```
|
||||
|
||||
**Bewusste Änderung gegenüber Spec #2 § 3.4:** Dort haben Nutzer kein Update-Recht auf `game_reports`, und `08_game_reports.test.sql` prüft, dass nur `postgres` Meldungen ändert. 13a erlaubt Nutzern genau eine Spalte (`result_seen_at`) an **eigenen**, bereits bearbeiteten Meldungen. Das bleibt mit KAT-05 Krit. 9 vereinbar (verboten ist das Ändern **fremder** Meldungen); B2 passt den Test aus `08` an.
|
||||
**Bewusste Änderung gegenüber Spec #2 § 3.4:** Dort haben Nutzer kein Insert- und kein Update-Recht, und `08_game_reports.test.sql` prüft, dass nur `postgres` Meldungen ändert. 13a erlaubt das Anlegen eigener Meldungen und das Ändern genau einer Spalte (`result_seen_at`) an **eigenen**, bereits bearbeiteten Meldungen. Das bleibt mit KAT-05 Krit. 9 vereinbar (verboten ist das Ändern **fremder** Meldungen); B2 passt `08` an.
|
||||
|
||||
`select` bleibt wie in 2c. **Nicht** schreibbar für Nutzer: `id`, `origin`, `status`, `bgg_id`, `target_game_id`, `resolved_at`, `created_at`, `updated_at` (Fehler 42501). Damit setzt die Datenbank `origin = 'user'`, `status = 'open'` (Krit. 8) und `created_at = now()` (Tageslimit) selbst. `delete` bekommt der Nutzer nicht.
|
||||
`select` bleibt wie in 2c. **Nicht** schreibbar für Nutzer: `id`, `origin`, `status`, `bgg_id`, `target_game_id`, `resolved_at`, `created_at`, `updated_at` (42501). Damit setzt die Datenbank `origin = 'user'`, `status = 'open'` (Krit. 8) und `created_at = now()` (Tageslimit) selbst. `delete` bekommt der Nutzer nicht.
|
||||
|
||||
**Merkpunkt für den Client (aus #2 PR 2a):** Clients schreiben `games` und `game_reports` nie mit `id`, `created_at` oder `updated_at`, und ein `toJson()` eines Modells geht nie ungefiltert an `insert`/`update`. In #13 schreibt die App ohnehin nur über die RPCs (4.2, 4.4) und das eine Update von `result_seen_at` (4.5) mit einer ausdrücklichen Spaltenliste.
|
||||
**Merkpunkt für den Client (aus #2 PR 2a):** Clients schreiben `games` und `game_reports` nie mit `id`, `created_at` oder `updated_at`, und ein `toJson()` eines Modells geht nie ungefiltert an `insert`/`update`. Die App schreibt in #13 nur über die RPCs (4.2, 4.4) und das Update von `result_seen_at` (4.5) mit ausdrücklicher Spaltenliste.
|
||||
|
||||
**RLS in Klartext (neu in 13a)**
|
||||
|
||||
- **Anlegen (`game_reports_insert_own`, `for insert to authenticated`):** `owner_id = auth.uid()` und, falls `provisional_game_id` gesetzt ist, zeigt er auf ein **eigenes privates**, nicht ausgeblendetes und nicht zusammengeführtes Spiel (`exists (select 1 from games g where g.id = provisional_game_id and g.visibility = 'private' and g.owner_id = auth.uid() and g.hidden_at is null and g.merged_into_id is null)`). `origin`, `status` usw. sind über die Spaltenrechte festgelegt; die Policy prüft zusätzlich `origin = 'user' and status = 'open' and target_game_id is null` (zweite Sicherung).
|
||||
- **Anlegen (`game_reports_insert_own`, `for insert to authenticated`):** `owner_id = auth.uid()`, `origin = 'user'`, `status = 'open'`, `target_game_id is null` und **`provisional_game_id is not null`**, und dieser zeigt auf ein eigenes privates, nicht ausgeblendetes und nicht zusammengeführtes Spiel (`exists (select 1 from games g where g.id = provisional_game_id and g.visibility = 'private' and g.owner_id = auth.uid() and g.hidden_at is null and g.merged_into_id is null)`). Ein zweiter Insert mit demselben vorläufigen Eintrag scheitert am Teilindex aus 2c (23505); `report_missing_game` bildet das auf `report_provisional_in_use` ab, die App auf „unbekannter Fehler“.
|
||||
- **Ändern (`game_reports_update_seen`, `for update to authenticated`):** `using (owner_id = auth.uid() and status <> 'open')`, `with check` gleich. Über die Spaltenrechte ist nur `result_seen_at` änderbar.
|
||||
- **Lesen:** unverändert aus 2c (nur eigene).
|
||||
- **Löschen:** kein Recht (Meldungen verschwinden nur mit dem Konto).
|
||||
- **Vorläufige Spiele:** unverändert; `games_select` zeigt private Spiele nur dem Ersteller, auch wenn sie ausgeblendet oder zusammengeführt sind. So bleiben die privaten Varianten für den Hinweis zur Edition lesbar (E7).
|
||||
- **Lesen:** unverändert aus 2c (nur eigene). **Löschen:** kein Recht.
|
||||
- **Vorläufige Spiele:** unverändert; der Ersteller sieht sie auch ausgeblendet oder zusammengeführt (wichtig für die Ableitung in E7). Er kann sie über `games_delete_private` löschen, solange nichts darauf verweist; dann wird `provisional_game_id` per `on delete set null` leer (E6, 5.4).
|
||||
|
||||
**Trigger `game_reports_daily_limit`** (Krit. 7)
|
||||
|
||||
- `before insert on public.game_reports for each row`, nur wenn `new.origin = 'user'`.
|
||||
- Sperre gegen gleichzeitige Inserts: `perform pg_advisory_xact_lock(hashtextextended('game_reports_limit:' || new.owner_id::text, 0))`. Ohne Sperre könnten zwei parallele Meldungen beide die Zählung „9“ sehen.
|
||||
- Zählt `origin = 'user'` und `owner_id = new.owner_id` mit `created_at` am **selben Kalendertag in Europe/Berlin** (Spec-F4): `public.report_day(created_at) = public.report_day(now())`. Bei ≥ 10 Fehler `report_daily_limit` (P0001).
|
||||
- Hilfsfunktion `public.report_day(p_at timestamptz) returns date` = `(p_at at time zone 'Europe/Berlin')::date`, `language sql`, `immutable`, `search_path = ''`, `execute` nur für den Eigentümer. Sie ist eigens herausgezogen, damit pgTAP die Tagesgrenze mit festen Zeitstempeln prüfen kann (`now()` lässt sich im Test nicht steuern).
|
||||
- `create trigger … before insert on public.game_reports for each row when (new.origin = 'user') execute function public.game_reports_daily_limit()`.
|
||||
- Sperre gegen gleichzeitige Inserts: `perform pg_advisory_xact_lock(hashtextextended('game_reports_limit:' || new.owner_id::text, 0))`. **Setzt die Isolationsstufe READ COMMITTED voraus** (Standard bei PostgREST und Supabase): Die Zählung nach der Sperre sieht dann die inzwischen bestätigten Meldungen der anderen Transaktion. Unter REPEATABLE READ oder SERIALIZABLE wäre die Sperre wirkungslos, weil der Snapshot vor der Sperre feststeht.
|
||||
- Zählt `origin = 'user'` und `owner_id = new.owner_id` mit `public.report_day(created_at) = public.report_day(now())`. Bei ≥ 10 Fehler `report_daily_limit` (P0001). Die Zahl 10 ist eine Konstante in der Funktion; Änderung = neue Migration.
|
||||
- Hilfsfunktion `public.report_day(p_at timestamptz) returns date` = `(p_at at time zone 'Europe/Berlin')::date`, `language sql`, `immutable`, `search_path = ''`. **`grant execute … to authenticated`** (rechnet nur): Der Trigger läuft als `security invoker` mit der Rolle des Einfügenden; ohne dieses Recht scheitert jede Meldung mit 42501. Herausgezogen, damit pgTAP die Tagesgrenze mit festen Zeitstempeln prüfen kann.
|
||||
- Trigger-Funktion `security invoker`, `search_path = ''`: Der Nutzer sieht per RLS alle eigenen Meldungen; Studio (`postgres`) umgeht RLS ohnehin.
|
||||
- Import-Meldungen (`origin = 'import'`, SAM-08) werden weder gezählt noch geprüft (N12).
|
||||
- Funktion `security invoker` mit `search_path = ''`. Der Nutzer sieht per RLS alle eigenen Meldungen, mehr braucht die Zählung nicht; Studio (`postgres`) umgeht RLS ohnehin. `execute` wie oben entzogen (Trigger-Funktionen brauchen kein `execute`-Recht des Aufrufers).
|
||||
- Die Zahl 10 steht als Konstante in der Funktion (kein Konfigurationswert); eine Änderung ist eine neue Migration.
|
||||
|
||||
**Keine neue Tabelle, kein neuer Fremdschlüssel auf `games`.** Der Fremdschlüssel-Wächter aus #2 (7) und `00_schema.test.sql` (21 Tabellen) bleiben unverändert.
|
||||
**Keine neue Tabelle, kein neuer Fremdschlüssel auf `games`.** Die Tabellenzahl in `00_schema.test.sql` bleibt gegenüber `main` unverändert; der Fremdschlüssel-Wächter aus #2 bleibt durch 13a unberührt.
|
||||
|
||||
### 3.3 Normalisierung und „sehr ähnlich“ (Krit. 2, Klärung 15)
|
||||
|
||||
**Funktion `public.normalize_game_title(p_title text) returns text`**, `language sql`, `immutable`, `strict`, `parallel safe`, `search_path = ''`. Schritte in dieser Reihenfolge:
|
||||
**Zuständigkeit und Rechte (gleicher Satz in Spec #3):** Spec #3 besitzt `search_normalize` (u. a. `unaccent`, `lower`, Entfernen von Sonderzeichen) und gibt sie per `grant execute … to anon, authenticated` frei, weil generierte Spalten mit den Rechten der einfügenden Rolle berechnet werden; ein Entzug würde jeden Insert in `games` brechen. `unaccent` mit festem Wörterbuch als `immutable` zu deklarieren ist eine verbreitete Konvention, keine Garantie von PostgreSQL. #13 ändert `search_normalize` nicht und legt keine zweite Normalisierung an.
|
||||
|
||||
1. `lower()`; `ß` → `ss`, `æ` → `ae`, `œ` → `oe`.
|
||||
2. **Akzente falten** (Spec-F3) mit einer festen, handgeschriebenen Zuordnung über `translate()` (z. B. `äàáâãå` → `a`, `ëèéê` → `e`, `ïìíî` → `i`, `öòóôõø` → `o`, `üùúû` → `u`, `ñ` → `n`, `ç` → `c`, `ÿý` → `y`). `translate()` ist `immutable`; `unaccent` wird nicht verwendet (nur `stable`).
|
||||
3. Alles, was danach kein Zeichen aus `a–z` oder `0–9` ist (Satz- und Sonderzeichen, Bindestriche, Doppelpunkte, Apostrophe, Emojis, übrige Schriftzeichen), wird zu einem Leerzeichen: `regexp_replace(…, '[^a-z0-9]+', ' ', 'g')`. Die explizite Zeichenklasse hängt nicht von der Datenbank-Locale ab.
|
||||
4. **Führende Artikel** entfernen, einmal, nur am Anfang (Spec-F3): Deutsch `der`, `die`, `das`, `den`, `dem`, `des`, `ein`, `eine`, `einen`, `einem`, `einer`, `eines`; Englisch `the`, `a`, `an`.
|
||||
5. Mehrfache Leerzeichen zu einem, `trim`.
|
||||
6. Ist das Ergebnis leer, wird der Wert aus Schritt 3 (ohne Artikelentfernung) verwendet; ist auch der leer (Titel nur aus Sonderzeichen oder nicht-lateinischen Schriftzeichen), `lower(btrim(p_title))`.
|
||||
**In 13a neu** (beide `language sql`, `immutable`, `parallel safe`, `search_path = ''`, `grant execute … to anon, authenticated`):
|
||||
|
||||
Beispiele (werden pgTAP-Fälle): „Die Siedler von Catan“ → `siedler von catan`; „CATAN!“ → `catan`; „7 Wonders: Duel“ → `7 wonders duel`; „The Crew“ → `crew`; „Café International“ → `cafe international`; „Mölkky“ und „Molkky“ → `molkky`; „Das“ → `das`.
|
||||
- `public.strip_leading_article(t text) returns text`: entfernt **einmal ein führendes** Artikelwort (Deutsch `der`, `die`, `das`, `den`, `dem`, `des`, `ein`, `eine`, `einen`, `einem`, `einer`, `eines`; Englisch `the`, `a`, `an`) samt folgendem Leerzeichen; erwartet bereits normalisierten Text (Kleinbuchstaben, ein Leerzeichen als Trenner).
|
||||
- `public.normalize_game_title(t text) returns text` =
|
||||
`coalesce(nullif(public.strip_leading_article(public.search_normalize(t)), ''), nullif(public.search_normalize(t), ''), lower(btrim(t)))`.
|
||||
Der letzte Rückfall greift bei Titeln, die nach `search_normalize` leer sind (z. B. nur nicht-lateinische Schriftzeichen oder nur Sonderzeichen).
|
||||
|
||||
**Regel „sehr ähnlich“** zwischen Meldung (Titel `t`, Jahr `y`) und Katalogspiel `g`:
|
||||
Beispiele (pgTAP; das genaue Ergebnis von `search_normalize` legt #3 fest): „Die Siedler von Catan“ und „siedler von catan“ ergeben denselben Wert; „CATAN!“ = „Catan“; „The Crew“ = „Crew“; „Das“ bleibt nicht leer; „Mölkky“ = „Molkky“ (Faltung durch `unaccent`); ein Titel nur aus japanischen Schriftzeichen → `lower(btrim(t))`.
|
||||
|
||||
- **A (gleicher Titel):** `normalize_game_title(g.title) = normalize_game_title(t)`, unabhängig vom Jahr.
|
||||
- **B (Trigramm):** `extensions.similarity(normalize_game_title(g.title), normalize_game_title(t)) >= 0.8` **und** `abs(g.year - y) <= 1`. Fehlt `y` oder `g.year`, gilt B ohne Jahresbedingung (Spec-F2).
|
||||
- Grundlage sind nur sichtbare Katalogspiele (`catalog_games`), keine ausgeblendeten, zusammengeführten oder privaten Spiele (auch nicht eigene vorläufige, Spec-F7).
|
||||
- **Wichtig für die Umsetzung:** In Funktionen mit `search_path = ''` muss `extensions.similarity(...) >= 0.8` ausdrücklich verwendet werden, **nicht** der Operator `%`; dieser nutzt die Einstellung `pg_trgm.similarity_threshold` (Standard 0,3).
|
||||
- **Alternative Titel (Klärung 3, #3):** Hat #3 bei der Umsetzung von 13a eine Tabelle für alternative Titel, werden diese in A und B mitgeprüft (beste Übereinstimmung je Spiel). Sonst nur der Haupttitel; #3 ergänzt die Prüfung dann in seiner Migration (Hinweis 9.4).
|
||||
**Regel „sehr ähnlich“** zwischen Meldung (Titel `t`, Jahr `y`) und Katalogspiel `g` mit Haupttitel und allen alternativen Titeln aus `game_alt_titles` (Kandidatentitel `c`, **verpflichtend**):
|
||||
|
||||
**Zusammenspiel mit #3:** Die Suche in KAT-01 (Krit. 1: „unabhängig von Groß- und Kleinschreibung und Sonderzeichen“) soll dieselbe Funktion nutzen. Wird #3 vor 13a umgesetzt und legt eine Normalisierungsfunktion an, die die Regeln oben erfüllt, verwendet 13a sie statt einer zweiten; sonst legt 13a `normalize_game_title` an und #3 nutzt sie später mit. Eine zweite, abweichende Normalisierung ist zu vermeiden (zweiter Anwendungsfall vorhanden, deshalb eine gemeinsame Funktion).
|
||||
- **A (gleicher Titel):** `normalize_game_title(c) = normalize_game_title(t)` für mindestens ein `c`, unabhängig vom Jahr.
|
||||
- **B (Trigramm):** `extensions.similarity(normalize_game_title(c), normalize_game_title(t)) >= 0.8` für mindestens ein `c` **und** `abs(g.year - y) <= 1`. Fehlt `y` oder `g.year`, gilt B ohne Jahresbedingung (Spec-F2).
|
||||
- Je Spiel zählt die beste Übereinstimmung über alle Kandidatentitel.
|
||||
- Nur sichtbare Katalogspiele (`catalog_games`): keine ausgeblendeten, zusammengeführten oder privaten (auch nicht eigene vorläufige, Spec-F7).
|
||||
- In Funktionen mit `search_path = ''` ausdrücklich `extensions.similarity(...) >= 0.8` verwenden, **nicht** den Operator `%` (nutzt `pg_trgm.similarity_threshold`, Standard 0,3). Deshalb kein Trigramm-Index in 13a; bei ~200–2000 Katalogspielen reicht ein sequenzieller Durchlauf.
|
||||
|
||||
## 4. Backend
|
||||
|
||||
### 4.1 Überblick
|
||||
### 4.1 Überblick und Rechte
|
||||
|
||||
| Objekt | Art | `execute`/Recht | Zweck |
|
||||
| Objekt | Art | `execute` | Zweck |
|
||||
|---|---|---|---|
|
||||
| `normalize_game_title(text)` | Funktion, sql, immutable | `anon`, `authenticated` (harmlos, rein rechnend; nötig für Abfragen als Aufrufer, später auch für die Suche in #3) | Normalisierung (3.3) |
|
||||
| `find_similar_catalog_games(p_title text, p_year smallint)` | RPC, security invoker | `authenticated` | Ähnliche Katalogspiele anzeigen (Krit. 2) |
|
||||
| `strip_leading_article(text)`, `normalize_game_title(text)` | Funktion, sql, immutable | `anon`, `authenticated` | Normalisierung (3.3) |
|
||||
| `report_day(timestamptz)` | Funktion, sql, immutable | `authenticated` | Berliner Kalendertag (3.2) |
|
||||
| `find_similar_catalog_games(p_title text, p_year integer)` | RPC, security invoker | `authenticated` | Ähnliche Katalogspiele (Krit. 2) |
|
||||
| `report_missing_game(...)` | RPC, security invoker | `authenticated` | Meldung + vorläufiger Eintrag + optional Listeneintrag (Krit. 1, 2, 3, 7, 8) |
|
||||
| `adopt_report_target(p_report uuid)` | RPC, **security definer** (Eigentümer `postgres`) | `authenticated` | Wechsel auf den Zieleintrag bei „abgelehnt mit Verweis“ (Krit. 5) |
|
||||
| `adopt_report_target(p_report uuid)` | RPC, **security definer**, Eigentümer `postgres` | `authenticated` | Wechsel auf den Zieleintrag (Krit. 5) |
|
||||
| `game_reports_daily_limit()` | Trigger-Funktion, security invoker | – | Tageslimit (Krit. 7) |
|
||||
| Policies `game_reports_insert_own`, `game_reports_update_seen`, Spaltenrechte | RLS | – | 3.2 |
|
||||
|
||||
`anon` darf keine der RPCs ausführen (42501). `service_role` bekommt nichts (keine Edge Function; der Import ergänzt später eigene Rechte, Spec #2 9.5).
|
||||
Jede Funktion: zuerst `revoke execute … from public, anon, authenticated, service_role`, dann nur die Grants dieser Tabelle. pgTAP prüft mit `has_function_privilege` für **alle vier Rollen** (`public` über eine Rolle ohne eigene Rechte, `anon`, `authenticated`, `service_role`).
|
||||
|
||||
Fehler werden wie in den bestehenden Migrationen als `raise exception '<code>' using errcode = 'P0001'` gemeldet; die App bildet den Code auf einen Domänenfehler ab (5.3).
|
||||
Fehler: `raise exception '<code>' using errcode = 'P0001'`; die App bildet den Code auf einen Domänenfehler ab (5.3).
|
||||
|
||||
### 4.2 RPC `public.report_missing_game`
|
||||
|
||||
|
|
@ -181,19 +171,19 @@ Fehler werden wie in den bestehenden Migrationen als `raise exception '<code>' u
|
|||
report_missing_game(
|
||||
p_title text,
|
||||
p_publisher_name text default null,
|
||||
p_year smallint default null,
|
||||
p_year integer default null,
|
||||
p_link text default null,
|
||||
p_list text default null, -- 'owned' | 'wishlist' | 'shopping' | null
|
||||
p_confirmed_distinct boolean default false
|
||||
) returns jsonb -- {"report_id": uuid, "provisional_game_id": uuid, "collection_item_id": uuid | null}
|
||||
```
|
||||
|
||||
`language plpgsql`, `security invoker`, `search_path = ''`, `volatile`.
|
||||
`language plpgsql`, `security invoker`, `search_path = ''`. `p_year` ist bewusst `integer` und wird selbst geprüft (ein `smallint`-Parameter würde bei großen Werten schon beim Aufruf mit 22003 scheitern, ohne verständlichen Code).
|
||||
|
||||
Ablauf (eine Transaktion; jeder Fehler rollt alles zurück):
|
||||
|
||||
1. `auth.uid()` ist null → `not_authenticated`.
|
||||
2. Eingaben bereinigen: `btrim` auf Titel, Verlag, Link; leere Zeichenketten werden `null`.
|
||||
2. `btrim` auf Titel, Verlag, Link; leere Zeichenketten werden `null`.
|
||||
3. Prüfungen in dieser Reihenfolge:
|
||||
|
||||
| Prüfung | Fehler |
|
||||
|
|
@ -204,78 +194,73 @@ Ablauf (eine Transaktion; jeder Fehler rollt alles zurück):
|
|||
| Jahr außerhalb 1800–2100 | `report_year_invalid` |
|
||||
| Link > 500 Zeichen oder nicht `^https?://` | `report_link_invalid` |
|
||||
| `p_list` nicht in `owned`, `wishlist`, `shopping`, null | `report_list_invalid` |
|
||||
| **Vorprüfung Tageslimit:** schon 10 eigene Nutzer-Meldungen heute (`report_day`) | `report_daily_limit` |
|
||||
| `p_confirmed_distinct = false` und `find_similar_catalog_games` liefert ≥ 1 Treffer | `report_similar_exists` |
|
||||
|
||||
Die Prüfungen spiegeln die Checks aus 2c, damit die App gezielte Feldfehler zeigen kann; die Checks der Tabelle bleiben die eigentliche Grenze (23514 als Rückfall → „unbekannter Fehler“).
|
||||
4. `insert into public.games (owner_id, title, year, visibility) values (auth.uid(), title, year, 'private') returning id` (nur erlaubte Spalten, keine `id`).
|
||||
5. `insert into public.game_reports (owner_id, title, publisher_name, year, link, provisional_game_id) … returning id`. Hier greift der Limit-Trigger (`report_daily_limit`); weil Schritt 4 in derselben Transaktion liegt, bleibt kein verwaistes Spiel zurück.
|
||||
6. Wenn `p_list` gesetzt: `insert into public.collection_items (owner_id, game_id, list) values (auth.uid(), provisional_id, p_list) returning id`. Keine Edition (der vorläufige Eintrag hat keine Varianten; KAT-03 Krit. 7 für Listen).
|
||||
Die Limit-Vorprüfung steht **vor** der Ähnlichkeitsprüfung, damit der Nutzer nicht erst Kandidaten bestätigt und dann am Limit scheitert. Maßgeblich bleibt der Trigger (Sperre, Schritt 5). Die übrigen Prüfungen spiegeln die Checks aus 2c für gezielte Feldfehler; Tabellen-Checks bleiben die Grenze (23514 → „unbekannter Fehler“).
|
||||
4. `insert into public.games (owner_id, title, year, visibility) values (auth.uid(), title, year, 'private') returning id` (keine `id`).
|
||||
5. `insert into public.game_reports (owner_id, title, publisher_name, year, link, provisional_game_id) … returning id`. Hier greift der Limit-Trigger; 23505 aus dem Teilindex → `report_provisional_in_use`.
|
||||
6. Wenn `p_list` gesetzt: `insert into public.collection_items (owner_id, game_id, list) values (auth.uid(), provisional_id, p_list) returning id`. Keine Edition.
|
||||
7. Rückgabe als `jsonb`.
|
||||
|
||||
Aufrufreihenfolge in der App: erst `find_similar_catalog_games` (4.3) zeigen, dann `report_missing_game` mit `p_confirmed_distinct = true`, wenn der Nutzer bestätigt hat. Ruft ein Client direkt mit `true` auf, ohne die Liste gezeigt zu haben, umgeht er nur den UX-Schritt; Rechte und Limit sind nicht betroffen (E3).
|
||||
Aufrufreihenfolge in der App: erst `find_similar_catalog_games`, dann `report_missing_game` mit `p_confirmed_distinct = true` nach Bestätigung. Ein Client, der direkt `true` sendet, umgeht nur den UX-Schritt (E3).
|
||||
|
||||
### 4.3 RPC `public.find_similar_catalog_games`
|
||||
|
||||
```
|
||||
find_similar_catalog_games(p_title text, p_year smallint default null)
|
||||
find_similar_catalog_games(p_title text, p_year integer default null)
|
||||
returns table (
|
||||
game_id uuid,
|
||||
title text,
|
||||
title text, -- Haupttitel
|
||||
matched_title text, -- Titel, der getroffen hat (Haupt- oder alternativer Titel)
|
||||
year smallint,
|
||||
publishers text[], -- Namen aus game_publishers/publishers, sortiert
|
||||
publishers text[], -- sortiert
|
||||
match_kind text, -- 'same_title' | 'similar_title'
|
||||
similarity real -- 1.0 bei 'same_title'
|
||||
)
|
||||
```
|
||||
|
||||
`language sql`, `stable`, `security invoker`, `search_path = ''`. Liest `public.catalog_games` (plus `game_publishers`/`publishers`). Regel nach 3.3. Sortierung: `same_title` zuerst, dann `similarity` absteigend, dann `title`. Höchstens **5** Treffer. Leerer oder nur aus Leerzeichen bestehender Titel → leere Menge (kein Fehler).
|
||||
|
||||
Wird für `authenticated` freigegeben, nicht für `anon` (Melden setzt Anmeldung voraus).
|
||||
`language sql`, `stable`, `security invoker`, `search_path = ''`. Liest `catalog_games`, `game_alt_titles`, `game_publishers`, `publishers`. Regel nach 3.3. Sortierung: `same_title` zuerst, dann `similarity` absteigend, dann `title`; höchstens **5** Treffer. Leerer Titel oder Titel > 200 Zeichen → leere Menge (kein Fehler; `report_missing_game` meldet die Länge selbst). Nur `authenticated`.
|
||||
|
||||
### 4.4 RPC `public.adopt_report_target`
|
||||
|
||||
```
|
||||
adopt_report_target(p_report uuid) returns jsonb
|
||||
-- Zähler aus repoint_game_references, z. B. {"collection_items": 1, "plays": 0, "variants_mapped": 0, "variants_cleared": 1, …}
|
||||
adopt_report_target(p_report uuid) returns jsonb -- Zähler aus repoint_game_references
|
||||
```
|
||||
|
||||
`language plpgsql`, `security definer`, Eigentümer `postgres`, `search_path = ''`. Nötig, weil Nutzer `merged_into_id`/`hidden_at` nicht schreiben und `repoint_game_references` nicht ausführen dürfen (Spec #2 3.2, 3.3).
|
||||
`language plpgsql`, `security definer`, Eigentümer `postgres`, `set search_path = ''` (als Funktionseinstellung). Nötig, weil Nutzer `merged_into_id`/`hidden_at` nicht schreiben und `repoint_game_references` nicht ausführen dürfen. **Innerhalb der Funktion ist `current_user = postgres`;** deshalb darf sie keine `admin_*`-Funktion aufrufen (deren Rollenprüfung würde sonst bestehen), weder jetzt noch in späteren Änderungen. pgTAP prüft `prosecdef = true`, `proowner = postgres` und `proconfig` mit `search_path=""`.
|
||||
|
||||
Prüfungen (alle mit `for update` auf Meldung und vorläufigem Eintrag, damit ein gleichzeitiges `admin_resolve_report` nicht dazwischenkommt):
|
||||
**Sperrreihenfolge** (wie im 2c-Entwurf von `admin_resolve_report`, gegen Verklemmungen): zuerst die Meldung `select … for update`, danach beide Spiele `select … from games where id in (provisional, target) order by id for update`.
|
||||
|
||||
| Prüfung | Fehler |
|
||||
|---|---|
|
||||
| `auth.uid()` ist null | `not_authenticated` |
|
||||
| Meldung existiert nicht **oder** gehört nicht `auth.uid()` (gleicher Fehler, verrät keine fremden IDs) | `report_not_found` |
|
||||
| Meldung existiert nicht oder gehört nicht `auth.uid()` (gleicher Fehler) | `report_not_found` |
|
||||
| `status <> 'rejected'` oder `target_game_id is null` | `report_not_adoptable` |
|
||||
| `provisional_game_id is null` | `report_no_provisional` |
|
||||
| `provisional_game_id is null` (z. B. vom Nutzer gelöscht) | `report_no_provisional` |
|
||||
| vorläufiger Eintrag ist nicht `private` mit `owner_id = auth.uid()` | `report_not_found` |
|
||||
| vorläufiger Eintrag ist schon zusammengeführt (`merged_into_id is not null`) | `report_already_adopted` |
|
||||
| Ziel ist kein sichtbares Katalogspiel (`catalog_games`) | `report_target_not_listed` |
|
||||
| Es gibt Verweise auf den vorläufigen Eintrag von **anderen** Nutzern (`collection_items`, `favorite_games`, `plays`, `score_sheets` mit `owner_id`/`profile_id` ≠ `auth.uid()`) | `report_foreign_references` |
|
||||
| vorläufiger Eintrag ist schon zusammengeführt | `report_already_adopted` |
|
||||
| Ziel ist kein sichtbares Katalogspiel (`visibility = 'catalog' and hidden_at is null and merged_into_id is null`) | `report_target_not_listed` |
|
||||
| Verweise auf den vorläufigen Eintrag mit fremdem oder fehlendem Eigentümer: `collection_items.owner_id`, `favorite_games.profile_id`, `plays.owner_id`, `score_sheets.owner_id` jeweils `is distinct from auth.uid()` (null zählt als fremd) | `report_foreign_references` |
|
||||
|
||||
Die letzte Prüfung ist eine Schutzschicht: Über die API können andere Nutzer ein fremdes privates Spiel nicht referenzieren (Policies verlangen ein sichtbares Spiel), aber `security definer` umgeht RLS, und ein Studio-Handgriff wäre denkbar.
|
||||
Ablauf: `repoint_game_references(provisional, target)` (privat → Katalog, keine Varianten kopiert, gleichnamige abgebildet, sonst `null`; Spec #2 RB1/N15), danach am vorläufigen Eintrag `merged_into_id = target`, `hidden_at = now()`. Die Meldung bleibt `rejected`. **`result_seen_at` bleibt unverändert (null)**, damit der Hinweis „Dein Eintrag zeigt jetzt auf …“ (inkl. Edition-Hinweis) am Zieleintrag einmal erscheint, bis der Nutzer „Verstanden“ wählt (5.4). Bei Spec-F6 = Zähler setzt die Funktion zusätzlich `editions_cleared`. Rückgabe: Zähler aus `repoint_game_references`.
|
||||
|
||||
Ablauf: `repoint_game_references(provisional, target)` (privat → Katalog, keine Varianten kopiert, gleichnamige abgebildet, sonst `null`; Spec #2 RB1/N15), danach am vorläufigen Eintrag `merged_into_id = target`, `hidden_at = now()`. Die Meldung bleibt `rejected` (das ist das Ergebnis der Prüfung durch den Admin); `result_seen_at` wird auf `now()` gesetzt, weil der Nutzer gehandelt hat. Rückgabe: Zähler aus `repoint_game_references`.
|
||||
|
||||
`favorite_games` sind bei vorläufigen Spielen fachlich ausgeschlossen (Klärung 23), technisch aber derzeit erlaubt (Hinweis an #14, 9.4); `repoint_game_references` behandelt sie ohnehin.
|
||||
**Wirkung auf Partien:** `plays.game_id` zeigt danach auf das Katalogspiel; Mitspieler (`play_players` mit `profile_id`) sehen in der Partie dann das Katalogspiel statt eines für sie unsichtbaren privaten Spiels. Das ist gewollt und gilt ebenso für `admin_resolve_report`.
|
||||
|
||||
### 4.5 Ergebnishinweis wegklicken
|
||||
|
||||
Kein eigener RPC. Die App sendet per PostgREST `PATCH /rest/v1/game_reports?id=eq.<id>` genau die Map `{'result_seen_at': <aktuelle Zeit als ISO-8601 in UTC>}` (PostgREST setzt Werte, keine SQL-Ausdrücke wie `now()`). Dass die Zeit vom Client kommt, ist unkritisch: Der Wert steuert nur die Anzeige des eigenen Hinweises. Policy und Spaltenrecht (3.2) begrenzen das Update auf eigene, bereits bearbeitete Meldungen und genau diese Spalte; bei einer offenen oder fremden Meldung ändert sich keine Zeile.
|
||||
Kein RPC. Die App sendet `PATCH /rest/v1/game_reports?id=eq.<id>` mit genau `{'result_seen_at': <aktuelle Zeit, ISO-8601 UTC>}` (PostgREST setzt Werte, keine Ausdrücke wie `now()`). Die Client-Zeit ist unkritisch (steuert nur die eigene Anzeige). Bei offener oder fremder Meldung ändert sich keine Zeile.
|
||||
|
||||
### 4.6 Lesen für die App
|
||||
|
||||
Kein RPC nötig; PostgREST-Abfragen als angemeldeter Nutzer:
|
||||
|
||||
- **Meine Meldungen:** `game_reports` (eigene, RLS) mit Spalten `id, origin, title, publisher_name, year, link, status, target_game_id, provisional_game_id, result_seen_at, created_at, resolved_at`, sortiert nach `created_at desc`, seitenweise (20). Eingebettet: `provisional:games!provisional_game_id(id, merged_into_id, hidden_at, game_variants(id))` und `target:games!target_game_id(id, title, year)`. Die Fremdschlüsselnamen für das Embedding prüft flutter-dev gegen die 2c-Migration.
|
||||
- **Hinweis zu einem Spiel** (5.4): `game_reports` mit `or=(provisional_game_id.eq.<id>,target_game_id.eq.<id>)`; der Index aus 3.2 deckt beide Fälle.
|
||||
- **Edition (E7):** `collection_items` des Nutzers zum Zielspiel (`variant_id`) und `provisional.game_variants` aus der eingebetteten Abfrage.
|
||||
- **Meine Meldungen:** `game_reports` mit `id, origin, title, publisher_name, year, link, status, target_game_id, provisional_game_id, result_seen_at, created_at, resolved_at` (bei Spec-F6 = Zähler zusätzlich `editions_cleared`), `created_at desc`, je 20. Eingebettet: `provisional:games!provisional_game_id(id, merged_into_id, hidden_at, game_variants(id))`, `target:games!target_game_id(id, title, year)`. Fremdschlüsselnamen prüft flutter-dev gegen die 2c-Migration.
|
||||
- **Hinweis zu einem Spiel** (5.4): `game_reports` mit `or=(provisional_game_id.eq.<id>,target_game_id.eq.<id>)` (Indizes aus 2c) plus eingebettetem `provisional`.
|
||||
- **Edition, nur bei Rückfall Ableitung:** `collection_items` des Nutzers zum Zielspiel mit `list = 'owned'` (`variant_id`) und `provisional.game_variants`.
|
||||
|
||||
### 4.7 Doku
|
||||
|
||||
- `docs/datenmodell-r1.md`: `game_reports.result_seen_at`, Indizes, Spaltenrechte, Policies, Trigger `game_reports_daily_limit`, die Funktionen, Abschnitt „Meldungen (KAT-04)“ mit Ablauf und Fehlercodes.
|
||||
- `docs/katalog-pflege.md`, Abschnitt „Meldungen bearbeiten“ (aus 2c) ergänzen: Nutzer-Meldungen haben immer einen vorläufigen Eintrag; „abgelehnt mit Verweis“ lässt dem Nutzer die Wahl (`adopt_report_target`), der Admin hängt dabei nichts um; vor `approved` die Edition berücksichtigen (Runbook aus 2c); Meldungen nie von Hand auf einen anderen Status setzen (nur über `admin_resolve_report`), weil die App den Hinweis aus Status und `merged_into_id` ableitet.
|
||||
- `docs/datenmodell-r1.md`: `result_seen_at`, Spaltenrechte, Policies, Trigger, Funktionen, Abschnitt „Meldungen (KAT-04)“ mit Ablauf und Fehlercodes; Satz zur Zuständigkeit für `search_normalize` (3.3).
|
||||
- `docs/katalog-pflege.md`, Abschnitt „Meldungen bearbeiten“ (2c) ergänzen: Nutzer-Meldungen haben einen vorläufigen Eintrag (sofern nicht gelöscht); „abgelehnt mit Verweis“ lässt dem Nutzer die Wahl (`adopt_report_target`); Status nie von Hand setzen, nur über `admin_resolve_report`, weil die App den Hinweis aus Status und `merged_into_id` ableitet.
|
||||
- `supabase/README.md`: neue RPCs und Rechte.
|
||||
|
||||
## 5. Frontend
|
||||
|
|
@ -291,123 +276,118 @@ features/game_reports/
|
|||
game_report_status.dart enum GameReportStatus { open, approved, rejected, merged }
|
||||
game_report_origin.dart enum GameReportOrigin { user, import }
|
||||
report_outcome.dart sealed ReportOutcome (5.4) + reine Funktion outcomeOf(...)
|
||||
game_report_draft.dart freezed: Eingaben + Validierung (Titel Pflicht, Längen, Jahr, Link)
|
||||
similar_game.dart freezed: SimilarGame (gameId, title, year, publishers, matchKind)
|
||||
game_report_failure.dart freezed/sealed: dailyLimit, similarExists, validation(field, code),
|
||||
notAdoptable, alreadyAdopted, targetNotListed, offline, unknown
|
||||
game_report_draft.dart freezed: Eingaben + Validierung
|
||||
similar_game.dart freezed: SimilarGame (gameId, title, matchedTitle, year, publishers, matchKind)
|
||||
game_report_failure.dart sealed: dailyLimit, similarExists, validation(field, code), notAdoptable,
|
||||
noProvisional, alreadyAdopted, targetNotListed, offline, unknown
|
||||
game_report_repository.dart abstrakte Schnittstelle
|
||||
data/
|
||||
supabase_game_report_repository.dart RPC-Aufrufe und Abfragen (4.2–4.6), explizite Parameter-Maps
|
||||
game_report_dto.dart Mapping aus Zeilen (nur fromJson; kein toJson für Schreibzugriffe)
|
||||
game_report_dto.dart nur fromJson; kein toJson für Schreibzugriffe
|
||||
game_report_failure_mapper.dart P0001-Codes → GameReportFailure
|
||||
presentation/
|
||||
report_form/ Screen, Controller (Notifier), State
|
||||
similar_games/ Abschnitt/Dialog mit Kandidaten und ausdrücklicher Bestätigung
|
||||
my_reports/ Liste „Meine Meldungen“, Controller
|
||||
report_detail/ Detail einer Meldung inkl. Hinweis und Aktionen
|
||||
widgets/report_outcome_notice.dart wiederverwendbarer Hinweis (auch für #4/#6/#11)
|
||||
report_form/ similar_games/ my_reports/ report_detail/
|
||||
widgets/report_outcome_notice.dart wiederverwendbar (auch für #4/#6/#11)
|
||||
providers/ Repository-, Listen- und Hinweis-Provider (Riverpod, generiert)
|
||||
game_report_routes.dart
|
||||
```
|
||||
|
||||
Die Domäne importiert weder Flutter noch Supabase. Die Validierung der Eingaben liegt in `GameReportDraft` (Unit-Tests), nicht im Widget.
|
||||
Die Domäne importiert weder Flutter noch Supabase; Validierung in `GameReportDraft`, nicht im Widget.
|
||||
|
||||
### 5.2 Routen (go_router)
|
||||
|
||||
| Route | Screen | Hinweis |
|
||||
|---|---|---|
|
||||
| `/reports/new?title=<suchbegriff>` | `ReportFormScreen` | Einstieg aus der leeren Trefferliste von KAT-01 (#3 Krit. 6) mit vorbelegtem Titel; Anmeldung nötig (Redirect auf `/login?from=…` wie in Spec #1) |
|
||||
| `/reports` | `MyReportsScreen` | Liste der eigenen Meldungen |
|
||||
| `/reports/:reportId` | `ReportDetailScreen` | Status, Angaben, Hinweis, Aktionen; Deep Link und Web-URL möglich, fremde/unbekannte ID → „nicht gefunden“ (RLS liefert keine Zeile) |
|
||||
| `/reports/new?title=<suchbegriff>` | `ReportFormScreen` | Einstieg aus der leeren Trefferliste von KAT-01 (#3 Krit. 6); Anmeldung nötig (Redirect `/login?from=…`, Spec #1) |
|
||||
| `/reports` | `MyReportsScreen` | eigene Meldungen |
|
||||
| `/reports/:reportId` | `ReportDetailScreen` | Status, Angaben, Hinweis, Aktionen; fremde/unbekannte ID → „nicht gefunden“ |
|
||||
|
||||
Konstanten in `AppRoutes`. Einstieg zu `/reports`: aus der Suche (#3) und aus jedem Hinweis; ob zusätzlich in den Einstellungen (KTO-11 #28) oder im Dashboard (QUE-09), entscheidet ui-designer mit Chris (kein Kriterium verlangt es).
|
||||
Konstanten in `AppRoutes`. Weitere Einstiege zu `/reports` (Einstellungen, Dashboard) stimmt ui-designer mit Chris ab.
|
||||
|
||||
### 5.3 Melden (Krit. 1, 2, 3, 7, 8)
|
||||
|
||||
**Ablauf im `ReportFormController`** (Riverpod-Notifier, Zustände als freezed-Union):
|
||||
`ReportFormController` (Riverpod-Notifier, Zustände als freezed-Union):
|
||||
|
||||
| Zustand | Bedeutung | UI |
|
||||
|---|---|---|
|
||||
| `editing(draft, fieldErrors)` | Eingabe | Felder Titel (Pflicht), Verlag, Erscheinungsjahr, Link (optional), Auswahl „Gleich aufnehmen in“: Sammlung / Wunschliste / Einkaufsliste / nicht aufnehmen (Standard: Sammlung, Spec-F9) |
|
||||
| `checking` | `find_similar_catalog_games` läuft | Button mit Ladeanzeige, Felder gesperrt |
|
||||
| `similarFound(draft, candidates)` | Treffer nach 3.3 | Kandidatenliste (Titel, Jahr, Verlage, `GameCover`). Je Kandidat „Das ist mein Spiel“ → Detailseite des Katalogspiels (#4), keine Meldung. Darunter ausdrückliche Bestätigung: Checkbox „Mein Spiel ist keines davon“ + Button „Trotzdem melden“ (erst aktiv mit Haken). |
|
||||
| `submitting` | `report_missing_game` läuft | Ladeanzeige |
|
||||
| `submitted(result)` | gespeichert | Bestätigung (Krit. 1): „Danke, deine Meldung ist eingegangen. Wir prüfen sie von Hand; bis dahin kannst du das Spiel als vorläufigen Eintrag nutzen.“ Ggf. „Steht jetzt in deiner Sammlung/Wunschliste/Einkaufsliste“; Aktionen „Zur Meldung“, „Zur Sammlung“ (falls aufgenommen) |
|
||||
| `failure(draft, failure)` | Fehler | Eingaben bleiben erhalten (Zustände Krit. 3). `dailyLimit` → Hinweis „Du hast heute schon 10 Spiele gemeldet. Morgen geht es wieder.“ (Krit. 7); `similarExists` (Rennen: Katalog hat sich geändert) → zurück zu `similarFound`; Feldfehler am Feld; offline → „Melden braucht eine Verbindung“ |
|
||||
| Zustand | UI |
|
||||
|---|---|
|
||||
| `editing(draft, fieldErrors)` | Titel (Pflicht), Verlag, Jahr, Link (optional); „Gleich aufnehmen in“: Sammlung / Wunschliste / Einkaufsliste / nicht aufnehmen (Standard Sammlung, Spec-F9) |
|
||||
| `checking` | `find_similar_catalog_games` läuft; Ladeanzeige, Felder gesperrt |
|
||||
| `similarFound(draft, candidates)` | Kandidaten (Titel, getroffener Titel falls alternativ, Jahr, Verlage, `GameCover`). „Das ist mein Spiel“ → Detailseite des Katalogspiels (#4), keine Meldung. Ausdrückliche Bestätigung: Checkbox „Mein Spiel ist keines davon“ + „Trotzdem melden“ (erst mit Haken aktiv) |
|
||||
| `submitting` | `report_missing_game` läuft |
|
||||
| `submitted(result)` | Bestätigung (Krit. 1): „Danke, deine Meldung ist eingegangen. Wir prüfen sie von Hand; bis dahin kannst du das Spiel als vorläufigen Eintrag nutzen.“ ggf. Liste; Aktionen „Zur Meldung“, „Zur Sammlung“ |
|
||||
| `failure(draft, failure)` | Eingaben bleiben erhalten. `dailyLimit` → „Du hast heute schon 10 Spiele gemeldet. Morgen geht es wieder.“ (Krit. 7); `similarExists` → zurück zu `similarFound`; Feldfehler am Feld; offline → „Melden braucht eine Verbindung“ |
|
||||
|
||||
Gibt es keine Treffer, geht `checking` direkt in `submitting` über (`p_confirmed_distinct = false`). Bei Treffern wird mit `true` gesendet.
|
||||
Ohne Treffer geht `checking` direkt in `submitting` (`p_confirmed_distinct = false`), mit Treffern nach Bestätigung mit `true`. Lokale Vorprüfung derselben Feldregeln wie 4.2; das Limit prüft nur der Server (die Vorprüfung in der RPC meldet es vor dem Kandidatenschritt, wenn der Nutzer ohne Treffer sendet; bei Treffern spätestens beim Absenden).
|
||||
|
||||
Das Formular prüft vor dem Senden lokal dieselben Regeln wie 4.2 (Titel Pflicht, Längen, Jahr, Link-Format), damit Fehler sofort am Feld stehen. Das Tageslimit prüft nur der Server.
|
||||
|
||||
**Krit. 3 nachträglich:** Wurde „nicht aufnehmen“ gewählt, kann der Nutzer den vorläufigen Eintrag später auf der Detailseite der Meldung zu einer Liste hinzufügen. 13b nutzt dafür die Funktion „zur Sammlung/Liste hinzufügen“ aus SAM-01/SAM-04/SAM-05 (#6/#8/#9), falls vorhanden; sonst ein einfacher Insert in `collection_items` über das Repository (Spalten `owner_id`, `game_id`, `list`) mit Hinweis an #6, ihn später zu ersetzen (Spec-F8).
|
||||
**Krit. 3 nachträglich:** Bei „nicht aufnehmen“ kann der Nutzer den vorläufigen Eintrag später auf der Meldungsdetailseite zu einer Liste hinzufügen, über die Funktion aus SAM-01/SAM-04/SAM-05 (#6/#8/#9) bzw. einen Insert in `collection_items` mit Spaltenliste `owner_id`, `game_id`, `list` (Spec-F8).
|
||||
|
||||
### 5.4 Ergebnishinweise (Krit. 4, 5, 6, 8)
|
||||
|
||||
`ReportOutcome` wird rein aus den Daten abgeleitet (`outcomeOf(report, provisional, collectionItemOnTarget)`, Unit-Tests je Fall):
|
||||
`outcomeOf(report, provisional?, ownedItemOnTarget?)` ist eine reine Funktion (Unit-Tests je Fall). **Fehlt der vorläufige Eintrag** (`provisional_game_id` null, vom Nutzer gelöscht), folgt das Ergebnis nur aus `status` und `target_game_id`; Aktion „wechseln“ und Edition-Ableitung entfallen.
|
||||
|
||||
| Fall | Erkennung | Hinweis (Kurzform, Texte über ARB) | Aktionen | Wo sichtbar |
|
||||
| Fall | Erkennung | Hinweis (Kurzform, ARB) | Aktionen | Wo sichtbar |
|
||||
|---|---|---|---|---|
|
||||
| `pending` (Krit. 8) | `status = open` | „Gemeldet am …, wird geprüft“ | – | Meldung; vorläufiger Eintrag (Badge „vorläufig“) |
|
||||
| `approved` (Krit. 4) | `status = approved`, vorläufiger Eintrag `merged_into_id = target` | „Dein gemeldetes Spiel ist jetzt im Katalog. Deine Angaben wurden übernommen.“ | „Zum Spiel“, „Verstanden“ | Meldung; **Eintrag am Zielspiel** (Sammlung/Liste, Detailseite) |
|
||||
| `merged` (Krit. 5) | `status = merged`, vorläufiger Eintrag `merged_into_id = target` | „Dein Spiel gab es schon im Katalog als „{title}“. Dein Eintrag zeigt jetzt dorthin.“ | „Zum Spiel“, „Verstanden“ | wie `approved` |
|
||||
| `rejectedWithTarget` (Krit. 5) | `status = rejected`, `target_game_id` gesetzt, vorläufiger Eintrag nicht zusammengeführt | „Dein Spiel gibt es bereits als „{title}“.“ | „Zu „{title}“ wechseln“ (→ `adopt_report_target`, mit Bestätigungsdialog: „Deine Angaben werden auf den Katalogeintrag übertragen.“), „Vorläufig behalten“ | Meldung; vorläufiger Eintrag |
|
||||
| `rejectedAdopted` | `status = rejected`, `target_game_id` gesetzt, vorläufiger Eintrag `merged_into_id = target` | wie `merged` | „Zum Spiel“ | wie `approved` |
|
||||
| `rejected` (Krit. 6) | `status = rejected`, kein Ziel | Dauerhafte Markierung „Abgelehnt“ + Text „Dieses Spiel wird nicht in den Katalog aufgenommen. Dein Eintrag bleibt privat für dich.“ | „Verstanden“ (blendet nur den Text aus, die Markierung bleibt) | Meldung; vorläufiger Eintrag |
|
||||
| `approved` (Krit. 4) | `status = approved` | „Dein gemeldetes Spiel ist jetzt im Katalog. Deine Angaben wurden übernommen.“ | „Zum Spiel“, „Verstanden“ | Meldung; Eintrag am Zielspiel, **nur wenn** `provisional.merged_into_id = target` |
|
||||
| `merged` (Krit. 5) | `status = merged` | „Dein Spiel gab es schon im Katalog als „{title}“. Dein Eintrag zeigt jetzt dorthin.“ | „Zum Spiel“, „Verstanden“ | wie `approved` |
|
||||
| `rejectedWithTarget` (Krit. 5) | `status = rejected`, Ziel gesetzt, vorläufiger Eintrag vorhanden und nicht zusammengeführt | „Dein Spiel gibt es bereits als „{title}“.“ | „Zu „{title}“ wechseln“ (`adopt_report_target`, Bestätigungsdialog „Deine Angaben werden auf den Katalogeintrag übertragen.“), „Vorläufig behalten“ | Meldung; **nur** am vorläufigen Eintrag, nicht am Ziel |
|
||||
| `rejectedReference` | `status = rejected`, Ziel gesetzt, vorläufiger Eintrag fehlt | „Dein Spiel gibt es bereits als „{title}“.“ | „Zum Spiel“ | nur Meldung |
|
||||
| `rejectedAdopted` | `status = rejected`, Ziel gesetzt, `provisional.merged_into_id = target` | wie `merged` | „Zum Spiel“, „Verstanden“ | Meldung; Eintrag am Zielspiel bis „Verstanden“ (`result_seen_at` bleibt nach dem Wechsel null, 4.4) |
|
||||
| `rejected` (Krit. 6) | `status = rejected`, kein Ziel | dauerhafte Markierung „Abgelehnt“ + „Dieses Spiel wird nicht in den Katalog aufgenommen. Dein Eintrag bleibt privat für dich.“ | „Verstanden“ (blendet nur den Text aus) | Meldung; vorläufiger Eintrag |
|
||||
|
||||
**Zusatz Edition (E7, N15):** Bei `approved`, `merged` und `rejectedAdopted` zusätzlich „Deine Edition konnte nicht übernommen werden. Bitte wähle sie neu.“, wenn der vorläufige Eintrag mindestens eine eigene Variante hat **und** der Sammlungseintrag des Nutzers am Zielspiel `variant_id = null` hat. Aktion „Edition wählen“ führt zur Bearbeitung des Sammlungseintrags (KAT-03 #12 / SAM-02 #7), sobald vorhanden. Bei Partien wird nicht einzeln geprüft (Spec-F6).
|
||||
**Zusatz Edition (E7, N15)** bei `approved`, `merged`, `rejectedAdopted`: „Deine Edition konnte nicht übernommen werden. Bitte wähle sie neu.“
|
||||
|
||||
**„Verstanden“** setzt `result_seen_at` (4.5). Danach erscheint der Hinweis nicht mehr am Eintrag; in „Meine Meldungen“ bleibt der Status immer sichtbar.
|
||||
- Bei Spec-F6 = Zähler (Empfehlung): wenn `editions_cleared > 0`. Der Text nennt Sammlung **und** Partien („in deiner Sammlung oder deinen Partien“).
|
||||
- Rückfall Ableitung: wenn der vorläufige Eintrag mindestens eine eigene Variante hat **und** der Sammlungseintrag am Ziel mit `list = 'owned'` `variant_id = null` hat. Wunsch- und Einkaufsliste haben nach Klärung 28 nie eine Edition und werden nicht geprüft; Partien werden nicht einzeln geprüft (Grenze der Ableitung).
|
||||
- Aktion „Edition wählen“ → Bearbeitung des Sammlungseintrags (KAT-03 #12 / SAM-02 #7), sobald vorhanden.
|
||||
|
||||
**Hinweis „am Eintrag“:** Provider `reportNoticeForGameProvider(gameId)` liefert den passenden `ReportOutcome` (oder keinen) für ein Spiel, an dem der Nutzer einen Eintrag hat: Meldungen mit `provisional_game_id = gameId` (vorläufiger Eintrag) oder `target_game_id = gameId` und `result_seen_at is null` (ersetzter Eintrag). Das Widget `ReportOutcomeNotice` (kompakt für Listenzeilen, ausführlich für Detailseiten) wird eingebaut
|
||||
**„Verstanden“** setzt `result_seen_at` (4.5); danach kein Hinweis mehr am Eintrag, in „Meine Meldungen“ bleibt der Status sichtbar.
|
||||
|
||||
- in 13b: in „Meine Meldungen“ und die Meldungsdetailseite,
|
||||
- in die Detailseite eines Spiels (#4) und die Sammlungs-/Listenansicht (#6/#11), soweit diese bei der Umsetzung von 13b gemergt sind; sonst übernehmen #4/#6/#11 den Einbau (Spec-F8, Hinweis 9.4).
|
||||
**Hinweis „am Eintrag“:** `reportNoticeForGameProvider(gameId)` liefert den passenden Fall: am vorläufigen Eintrag (`provisional_game_id = gameId`) die Fälle `pending`, `rejectedWithTarget`, `rejected`; am Zielspiel (`target_game_id = gameId`, `result_seen_at is null`) nur `approved`, `merged`, `rejectedAdopted`, und nur wenn `provisional.merged_into_id = gameId`. `ReportOutcomeNotice` (kompakt/ausführlich) wird eingebaut in 13b in „Meine Meldungen“ und die Meldungsdetailseite, in #4/#6/#11 soweit gemergt, sonst übernehmen diese Stories den Einbau (Spec-F8, 9.4). Vorläufige Einträge zeigen ein Badge „vorläufig“ (Text) und den `GameCover`-Platzhalter.
|
||||
|
||||
Vorläufige Einträge werden in Listen mit einem Badge „vorläufig“ (Text, nicht nur Farbe) und dem `GameCover`-Platzhalter gezeigt.
|
||||
|
||||
**Aktualität:** Kein Realtime in R1. Die Hinweise werden beim Öffnen der Liste/Detailseite und per Pull-to-Refresh neu geladen; das reicht, weil der Admin Meldungen selten und manuell bearbeitet.
|
||||
**Aktualität:** kein Realtime; Neuladen beim Öffnen und per Pull-to-Refresh.
|
||||
|
||||
### 5.5 Meine Meldungen
|
||||
|
||||
`MyReportsController` (AsyncNotifier mit Paging, 20 je Seite): Zustände Laden, Leer („Du hast noch keine Spiele gemeldet“ + Weg zur Suche), Fehler mit „Erneut versuchen“, Daten. Je Zeile: Titel, Jahr, Datum, Status-Badge (Text), ggf. kompakter Hinweis. Import-Meldungen (`origin = import`, SAM-08) erscheinen mit Kennzeichen „aus BGG-Import“ (Text liefert #25; 13b zeigt `origin` nur an, wenn vorhanden).
|
||||
`MyReportsController` (AsyncNotifier mit Paging, 20 je Seite): Laden, Leer („Du hast noch keine Spiele gemeldet“ + Weg zur Suche), Fehler mit „Erneut versuchen“, Daten. Je Zeile Titel, Jahr, Datum, Status-Badge, ggf. kompakter Hinweis; Import-Meldungen mit Kennzeichen „aus BGG-Import“ (Text liefert #25).
|
||||
|
||||
### 5.6 l10n (Auszug, de/en)
|
||||
|
||||
Neue ARB-Schlüssel mit Präfix `gameReport…`: Formularfelder und Fehler (`gameReportTitleRequired`, `gameReportTitleTooLong`, `gameReportPublisherTooLong`, `gameReportYearInvalid`, `gameReportLinkInvalid`), Ähnlichkeit (`gameReportSimilarHeading`, `gameReportSimilarIsMine`, `gameReportSimilarConfirmDistinct`, `gameReportSubmitAnyway`), Listenwahl, Bestätigung, Tageslimit (`gameReportDailyLimit`), Offline, Status-Badges (`gameReportStatusOpen` … `gameReportStatusRejected`, `gameReportProvisionalBadge`), Hinweise aus 5.4 inkl. Edition, Aktionen, Leer-/Fehlerzustand, Semantics-Labels. Platzhalter `{title}`, Datum über `intl`.
|
||||
Präfix `gameReport…`: Felder und Fehler (`gameReportTitleRequired`, `gameReportTitleTooLong`, `gameReportPublisherTooLong`, `gameReportYearInvalid`, `gameReportLinkInvalid`), Ähnlichkeit (`gameReportSimilarHeading`, `gameReportSimilarIsMine`, `gameReportSimilarConfirmDistinct`, `gameReportSubmitAnyway`), Listenwahl, Bestätigung, `gameReportDailyLimit`, Offline, Status-Badges, `gameReportProvisionalBadge`, Hinweise aus 5.4 inkl. Edition, Aktionen, Leer-/Fehlerzustand, Semantics-Labels; Platzhalter `{title}`, Datum über `intl`.
|
||||
|
||||
## 6. Plattform-Besonderheiten
|
||||
|
||||
- **iOS, Android, Web:** gleiches Verhalten. Link-Feld mit `TextInputType.url`, Jahr mit Ziffern-Tastatur; der Link wird in der App nur als Text angezeigt, nicht geöffnet (Datenschutz, 2).
|
||||
- **Web:** Routen sind teilbare URLs; der Bestätigungsschritt bei ähnlichen Spielen ist per Tastatur bedienbar (Checkbox, Fokus-Reihenfolge); breites Layout: Formular und Kandidaten nebeneinander ab Breite ≥ 840 dp (ui-designer).
|
||||
- **Offline:** Melden braucht eine Verbindung (wie SAM-01 Krit. 3); Hinweis statt Fehler, Eingaben bleiben im Formular. Keine Offline-Warteschlange für Meldungen.
|
||||
- **Push, Kamera, Dateien:** nicht betroffen (kein Bild-Upload, keine Push in R1).
|
||||
- **iOS, Android, Web:** gleiches Verhalten; Link-Feld `TextInputType.url`, Jahr mit Ziffern-Tastatur; der Link wird nur als Text angezeigt, nicht geöffnet.
|
||||
- **Web:** teilbare URLs; Bestätigungsschritt per Tastatur bedienbar; breites Layout (Formular und Kandidaten nebeneinander ab ≥ 840 dp, ui-designer).
|
||||
- **Offline:** Melden braucht eine Verbindung (wie SAM-01 Krit. 3); Hinweis statt Fehler, Eingaben bleiben. Keine Offline-Warteschlange.
|
||||
- **Push, Kamera, Dateien:** nicht betroffen.
|
||||
|
||||
## 7. Teststrategie
|
||||
|
||||
Werkzeuge: pgTAP (`supabase test db`), `flutter_test` + mocktail, Playwright (Web-E2E). Testnamen enthalten „KAT-04 Kriterium n“.
|
||||
|
||||
**Neue pgTAP-Datei `supabase/tests/database/09_game_reports_user.test.sql`** (Nummer nach `08_game_reports.test.sql` aus 2c). Fixtures mit festen Test-UUIDs, zwei Nutzer A und B, unabhängig vom Startbestand.
|
||||
**Neue pgTAP-Datei `supabase/tests/database/10_game_reports_user.test.sql`** (`08` gehört 2c, `09` gehört #3). Fixtures mit festen Test-UUIDs, Nutzer A und B, unabhängig vom Startbestand. Einfügungen „als Nutzer“ laufen wirklich als Rolle `authenticated` mit gesetztem `request.jwt.claims`, nicht als `postgres`.
|
||||
|
||||
### 7.1 Abdeckungsmatrix
|
||||
|
||||
| AK | Unit | Widget | Integration/E2E | RLS/pgTAP | Task |
|
||||
|---|---|---|---|---|---|
|
||||
| Krit. 1 | `GameReportDraft`: Titel Pflicht (auch nur Leerzeichen), Längen, Jahr, Link-Format; Fehlercode-Mapping | Formular: Feldfehler am Feld, Bestätigung nach Erfolg, Listenwahl | Playwright: leere Suche → „Spiel melden“ → Titel vorbelegt → absenden → Bestätigung | `report_missing_game` legt Meldung (`status = open`, `origin = user`) und vorläufiges Spiel (`private`, `owner_id = A`, nur Titel/Jahr) an; Trimmen; alle Validierungsfehler (`report_title_required` … `report_list_invalid`); `anon` → 42501 | B1, B2, F2, T1, T2 |
|
||||
| Krit. 2 | Controller: Treffer → `similarFound`; ohne Haken kein Senden; „Das ist mein Spiel“ sendet nichts; `similarExists` beim Senden → zurück zu Kandidaten | Kandidatenliste, Bestätigungs-Checkbox aktiviert Button, Tastatur im Web, Semantics | Playwright: Meldung zu „Catan“ zeigt vorhandenes „CATAN“ | `normalize_game_title`: Beispiele aus 3.3 (Artikel de/en, Sonderzeichen, Akzente und Umlaute gefaltet, `ß`, nur Artikel, nur nicht-lateinische Zeichen); `find_similar_catalog_games`: gleicher normalisierter Titel bei beliebigem Jahr trifft; Trigramm ≥ 0,8 bei Jahr ± 1 trifft, bei ± 2 nicht; < 0,8 nicht; Jahr fehlt → Regel nach Spec-F2; ausgeblendete, zusammengeführte und private Spiele (auch eigene) werden nicht geliefert; höchstens 5, Sortierung; `report_missing_game` ohne Bestätigung bei Treffer → `report_similar_exists`, mit Bestätigung gespeichert | B1, B2, F2, U1, T1, T2 |
|
||||
| Krit. 3 | – | Bestätigung zeigt Liste; Badge „vorläufig“ | Playwright: gemeldetes Spiel steht in der Wunschliste (sofern #6/#8 vorhanden) | `p_list` legt `collection_items` zum vorläufigen Spiel an; B sieht weder vorläufiges Spiel noch Listeneintrag noch Meldung von A; `anon` sieht nichts; Rollback: schlägt der Listeneintrag fehl, gibt es weder Spiel noch Meldung | B1, B2, F2, F3 |
|
||||
| Krit. 4 | `outcomeOf`: `approved` mit/ohne Edition-Hinweis | `ReportOutcomeNotice` für `approved` (de/en), „Verstanden“ blendet aus | integration_test/Playwright: Meldung per SQL-Hilfsfunktion im Testsetup mit `admin_resolve_report(…, 'approved', …)` bearbeiten → Hinweis am Eintrag | nach `admin_resolve_report` (2c) ist der Sammlungseintrag von A am Zielspiel mit Liste/Notiz/Kaufdatum; vorläufige Variante → `variant_id` null, private Variante bleibt am vorläufigen Eintrag und ist für A lesbar (E7); `result_seen_at` setzbar | B2, F3, T1, T2 |
|
||||
| Krit. 5 | `outcomeOf`: `merged`, `rejectedWithTarget`, `rejectedAdopted` | Hinweise und Aktion „wechseln“ mit Bestätigungsdialog | Playwright: abgelehnt mit Verweis → wechseln → Eintrag zeigt Zielspiel | `adopt_report_target`: Erfolg hängt Sammlung und Partien von A um, setzt `merged_into_id`/`hidden_at`, Status bleibt `rejected`, `result_seen_at` gesetzt; Fehler `report_not_found` (fremde Meldung von B, unbekannte ID), `report_not_adoptable` (`open`, `approved`, `rejected` ohne Ziel), `report_already_adopted` (zweiter Aufruf), `report_target_not_listed` (Ziel inzwischen ausgeblendet), `report_foreign_references` (Fixture als `postgres`); `anon` → 42501 | B1, B2, F3, T1, T2 |
|
||||
| Krit. 6 | `outcomeOf`: `rejected` ohne Ziel | dauerhafte Markierung „Abgelehnt“ bleibt nach „Verstanden“ | – | nach `admin_resolve_report(…, 'rejected')` bleibt das vorläufige Spiel unverändert privat und für A lesbar | B2, F3, T2 |
|
||||
| Krit. 7 | Mapping `report_daily_limit` → `dailyLimit` | Limit-Hinweis, Eingaben bleiben erhalten | – | 10 Meldungen am selben Berliner Kalendertag ok, 11. → `report_daily_limit` und **kein** vorläufiges Spiel übrig (Rollback); Meldungen von gestern (Fixture mit `created_at = now() - interval '1 day'` als `postgres` gesetzt) zählen nicht; Tagesgrenze über `report_day` mit festen Zeitstempeln (`2026-01-15 23:30 UTC` → 16.01., `2026-07-15 21:59 UTC` → 15.07., `22:00 UTC` → 16.07.); `origin = 'import'` als `postgres` eingefügt zählt nicht und wird nicht begrenzt; Meldungen von B zählen nicht für A; direkter Insert mit `created_at`, `origin` oder `status` → 42501 | B1, B2, F2, T1 |
|
||||
| Krit. 8 | – | Status „offen“ in Liste und Detail | – | Nutzer kann `status`, `target_game_id`, `resolved_at` weder per Insert noch per Update setzen (42501); Update von `result_seen_at` bei `status = open` → 0 Zeilen; nach dem Insert ist der Status `open`; kein Trigger/Job ändert ihn | B2, T1 |
|
||||
| Sicherheit Krit. 1, 3 | – | – | – | B liest A's Meldungen und vorläufige Spiele nicht; B kann A's Meldung nicht wegklicken (0 Zeilen) und nicht übernehmen; Insert mit `owner_id = A` als B → Policy-Fehler; `provisional_game_id` auf ein fremdes, ein Katalog- oder ein ausgeblendetes Spiel → Policy-Fehler; Spaltenrechte per `has_column_privilege` | B2 |
|
||||
| Sicherheit Krit. 2 | – | – | – | `anon`: kein `select`, `insert`, `update` auf `game_reports`, keine RPC ausführbar | B2 |
|
||||
| Datenschutz | – | – | – | Konto von A löschen → Meldungen, vorläufige Spiele, Listeneinträge weg; Meldungen und Spiele von B unverändert | B2 |
|
||||
| QUE-05/06, Zustände | – | Alle Screens de/en, `meetsGuideline` (Tap-Ziele, Kontrast, Labels), `textScaler` 2.0 ohne Überlauf, Lade-/Leer-/Fehler-/Datenzustand „Meine Meldungen“, Offline-Hinweis | Playwright: Tastaturbedienung des Bestätigungsschritts | – | F2, F3, T2 |
|
||||
| Krit. 1 | `GameReportDraft`: Titel Pflicht (auch nur Leerzeichen), Längen, Jahr, Link; Fehler-Mapping | Feldfehler, Bestätigung, Listenwahl | Playwright: leere Suche → „Spiel melden“ → Titel vorbelegt → Bestätigung | `report_missing_game` als `authenticated`: Meldung (`open`, `user`) und vorläufiges Spiel (`private`, nur Titel/Jahr); Trimmen; alle Validierungsfehler; Jahr 99999 → `report_year_invalid` (kein 22003); `anon` → 42501 | B1, B2, F2, T1, T2 |
|
||||
| Krit. 2 | Controller: Treffer → `similarFound`; ohne Haken kein Senden; „Das ist mein Spiel“ sendet nichts; `similarExists` → zurück | Kandidatenliste, Checkbox aktiviert Button, Tastatur, Semantics | Playwright: Meldung „Catan“ zeigt „CATAN“ | `strip_leading_article`/`normalize_game_title`: Artikel de/en nur führend, Großbuchstaben, Sonderzeichen, Umlaute über `search_normalize`, nur Artikel, nicht-lateinischer Titel (Rückfall); `find_similar_catalog_games`: gleicher normalisierter Titel bei beliebigem Jahr; Trigramm ≥ 0,8 bei ± 1 trifft, bei ± 2 nicht; < 0,8 nicht; fehlendes Jahr nach Spec-F2; **Treffer über alternativen Titel** (`matched_title`); ausgeblendete, zusammengeführte, private Spiele nicht; Titel > 200 → leere Menge; höchstens 5, Sortierung; `report_similar_exists` ohne, Speichern mit Bestätigung | B1, B2, F2, U1, T1, T2 |
|
||||
| Krit. 3 | – | Bestätigung zeigt Liste; Badge „vorläufig“ | Playwright: gemeldetes Spiel in der Wunschliste (sofern #6/#8 da) | `p_list` legt `collection_items` an; B sieht nichts von A; `anon` nichts; Rollback bei Fehler im Listeneintrag | B1, B2, F2, F3 |
|
||||
| Krit. 4 | `outcomeOf`: `approved` mit/ohne Edition; vorläufiger Eintrag fehlt | Hinweis de/en, „Verstanden“ | Playwright: Testsetup bearbeitet per `admin_resolve_report(…, 'approved', …)` → Hinweis am Eintrag | nach `admin_resolve_report`: Sammlungseintrag von A am Ziel mit Liste/Notiz/Kaufdatum; Edition ohne gleichnamige Variante → null; private Variante für A lesbar; bei Zähler `editions_cleared` korrekt; `result_seen_at` setzbar | B2, F3, T1, T2 |
|
||||
| Krit. 5 | `outcomeOf`: `merged`, `rejectedWithTarget`, `rejectedReference`, `rejectedAdopted` | Hinweise, „wechseln“ mit Dialog, Hinweis nur am richtigen Eintrag | Playwright: abgelehnt mit Verweis → wechseln → Eintrag zeigt Zielspiel | `adopt_report_target`: hängt Sammlung und Partien um, setzt `merged_into_id`/`hidden_at`, Status bleibt `rejected`, `result_seen_at` bleibt null; Fehler `report_not_found` (B, unbekannt), `report_not_adoptable`, `report_no_provisional` (gelöschter Eintrag), `report_already_adopted`, `report_target_not_listed`, `report_foreign_references` (fremder Eigentümer **und** `plays.owner_id = null`, Fixture als `postgres`); `prosecdef`, `proowner = postgres`, `proconfig` `search_path=""`; `has_function_privilege` für `public`, `anon`, `authenticated`, `service_role` | B1, B2, F3, T1, T2 |
|
||||
| Krit. 6 | `outcomeOf`: `rejected` ohne Ziel | Markierung bleibt nach „Verstanden“ | – | nach `admin_resolve_report(…, 'rejected')` bleibt das vorläufige Spiel privat und lesbar | B2, F3, T2 |
|
||||
| Krit. 7 | Mapping `report_daily_limit` | Limit-Hinweis, Eingaben bleiben | – | **als `authenticated`** 10 Meldungen über `report_missing_game` ok, 11. → `report_daily_limit`, kein vorläufiges Spiel übrig; ebenso direkter Insert als `authenticated` (prüft, dass der Trigger `report_day` ausführen darf); Meldungen von gestern (`created_at` als `postgres` gesetzt) zählen nicht; `report_day` mit festen Zeitstempeln (`2026-01-15 23:30 UTC` → 16.01., `2026-07-15 21:59 UTC` → 15.07., `22:00 UTC` → 16.07.); `origin = 'import'` als `postgres` zählt nicht und wird nicht begrenzt; Meldungen von B zählen nicht für A; Insert mit `created_at`, `origin` oder `status` → 42501 | B1, B2, F2, T1 |
|
||||
| Krit. 8 | – | Status „offen“ | – | Nutzer setzt `status`, `target_game_id`, `resolved_at` weder per Insert noch Update (42501); Update von `result_seen_at` bei `open` → 0 Zeilen | B2, T1 |
|
||||
| Sicherheit 1, 3 | – | – | – | B liest A's Meldungen und Spiele nicht, kann sie nicht wegklicken (0 Zeilen) und nicht übernehmen; Insert mit `owner_id = A` als B, ohne `provisional_game_id`, mit fremdem/Katalog-/ausgeblendetem Spiel → Policy-Fehler; zweite Meldung zum selben vorläufigen Eintrag → 23505; Spaltenrechte per `has_column_privilege` | B2 |
|
||||
| Sicherheit 2 | – | – | – | `anon`: kein `select`, `insert`, `update`, keine RPC | B2 |
|
||||
| Datenschutz | – | – | – | Konto A löschen → Meldungen, vorläufige Spiele, Listeneinträge weg; B unverändert | B2 |
|
||||
| QUE-05/06, Zustände | – | de/en, `meetsGuideline`, `textScaler` 2.0, Lade/Leer/Fehler/Daten, Offline | Playwright: Tastatur im Bestätigungsschritt | – | F2, F3, T2 |
|
||||
|
||||
**Nicht automatisiert:** echte Parallelität des Limits (die Sperre wird per Code-Review und einem dokumentierten manuellen Test mit zwei `psql`-Sitzungen geprüft); Bearbeitung echter Meldungen im Studio auf Staging (Chris).
|
||||
**Nicht automatisiert:** echte Parallelität des Limits (Code-Review und dokumentierter manueller Test mit zwei `psql`-Sitzungen unter READ COMMITTED); Bearbeitung echter Meldungen auf Staging (Chris).
|
||||
|
||||
**Bestehende Tests:** `08_game_reports.test.sql` aus 2c erwartet „kein Insert und kein Update für `authenticated`“ bzw. „nur `postgres` ändert Meldungen“; 13a passt beide Erwartungen auf die neuen Spaltenrechte an (Insert der erlaubten Spalten, Update nur `result_seen_at` an eigenen bearbeiteten Meldungen; alles andere weiter 42501) (B2). `00_schema.test.sql` bleibt bei 21 Tabellen.
|
||||
**Bestehende Tests:** `08_game_reports.test.sql` (2c) erwartet „kein Insert und kein Update für `authenticated`“; B2 passt beide Erwartungen an (Insert der erlaubten Spalten, Update nur `result_seen_at` an eigenen bearbeiteten Meldungen, alles andere weiter 42501). Tabellenzahl in `00_schema.test.sql` unverändert gegenüber `main`.
|
||||
|
||||
## 8. Aufgaben
|
||||
|
||||
|
|
@ -415,32 +395,32 @@ Werkzeuge: pgTAP (`supabase test db`), `flutter_test` + mocktail, Playwright (We
|
|||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| B1 | backend-dev | Migration `<ts>_game_reports_user.sql` nach 3.2–4.5: `pg_trgm`, `normalize_game_title` (oder Wiederverwendung aus #3, 3.3), Spalte `result_seen_at` mit Check, Indizes, Spaltenrechte, Policies `game_reports_insert_own` und `game_reports_update_seen`, Trigger `game_reports_daily_limit` mit Hilfsfunktion `report_day` (Berliner Kalendertag, Sperre je Nutzer, nur `origin = 'user'`), RPCs `find_similar_catalog_games`, `report_missing_game`, `adopt_report_target`; Rechte nach 4.1 | #2 PR 2c gemergt |
|
||||
| B2 | backend-dev | pgTAP `09_game_reports_user.test.sql` nach 7.1; `08_game_reports.test.sql` an die neuen Insert- und Update-Rechte anpassen | B1 |
|
||||
| B3 | backend-dev | Doku nach 4.7 (`datenmodell-r1.md`, `katalog-pflege.md` Abschnitt Meldungen, `supabase/README.md`) | B1 |
|
||||
| T1 | test-engineer | Review der pgTAP-Abdeckung gegen 7.1; manueller Paralleltest des Limits (zwei Sitzungen) dokumentieren | B2 |
|
||||
| B1 | backend-dev | Migration nach 3.2–4.5: `pg_trgm`, `strip_leading_article`, `normalize_game_title`, `report_day` (mit `execute` für `authenticated`), Spalte `result_seen_at`, Spaltenrechte, Policies, Trigger `game_reports_daily_limit` (`when (new.origin = 'user')`, Sperre), RPCs `find_similar_catalog_games`, `report_missing_game`, `adopt_report_target` (Sperrreihenfolge, `is distinct from`); Rechte nach 4.1 | #2 PR 2c und #3 gemergt |
|
||||
| B2 | backend-dev | pgTAP `10_game_reports_user.test.sql` nach 7.1; `08` an Insert- und Update-Rechte anpassen | B1 |
|
||||
| B3 | backend-dev | Doku nach 4.7 | B1 |
|
||||
| T1 | test-engineer | Review der pgTAP-Abdeckung; manueller Paralleltest des Limits dokumentieren | B2 |
|
||||
|
||||
### 8.2 PR 13b – Flutter (`Closes #13`), Branch `feature/13b-meldungen-app`
|
||||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| U1 | ui-designer | Entwurf Formular, Kandidatenschritt mit ausdrücklicher Bestätigung, Bestätigung, „Meine Meldungen“, Meldungsdetail, `ReportOutcomeNotice` (kompakt/ausführlich), Badge „vorläufig“, breites Web-Layout; Texte de/en; Einstieg zu `/reports` mit Chris abstimmen | Spec freigegeben |
|
||||
| F1 | flutter-dev | Domäne (5.1): Modelle, `ReportOutcome` + `outcomeOf`, `GameReportDraft`-Validierung, Fehler, Repository-Schnittstelle; Unit-Tests | Spec freigegeben (parallel zu B1) |
|
||||
| F2 | flutter-dev | Data-Schicht (RPC-Aufrufe mit expliziten Parametern, kein `toJson()` an Schreibzugriffe; Fehler-Mapper) und Melden-Ablauf (5.3) inkl. Routen, Einstieg aus der leeren Suche (#3), l10n | F1, U1; gegen echtes Backend: B1 |
|
||||
| F3 | flutter-dev | „Meine Meldungen“, Meldungsdetail, Hinweise (5.4) inkl. „Verstanden“, „wechseln“, Edition-Hinweis; Einbau von `ReportOutcomeNotice` in #4/#6/#11, soweit gemergt (sonst Hinweis-Kommentar in den Issues) | F1, F2 |
|
||||
| T2 | test-engineer | Widget-Tests nach 7.1 (alle Zustände, alle Hinweisfälle, de/en, a11y, `textScaler` 2.0); Playwright-Ablauf Krit. 1–5 gegen lokales Supabase, Bearbeitung der Meldung im Testsetup per SQL als `postgres` | F2, F3, B1 |
|
||||
| U1 | ui-designer | Formular, Kandidatenschritt, Bestätigung, „Meine Meldungen“, Detail, `ReportOutcomeNotice`, Badge, Web-Layout; Texte de/en | Spec freigegeben |
|
||||
| F1 | flutter-dev | Domäne (5.1) inkl. `outcomeOf` mit allen Fällen aus 5.4 (auch „vorläufiger Eintrag fehlt“); Unit-Tests | Spec freigegeben (parallel zu B1) |
|
||||
| F2 | flutter-dev | Data-Schicht (explizite Parameter, kein `toJson()` an Schreibzugriffe, Fehler-Mapper inkl. `report_provisional_in_use`) und Melden-Ablauf (5.3), Routen, Einstieg aus #3, l10n | F1, U1; gegen Backend: B1 |
|
||||
| F3 | flutter-dev | „Meine Meldungen“, Detail, Hinweise (5.4), Einbau in #4/#6/#11 soweit gemergt | F1, F2 |
|
||||
| T2 | test-engineer | Widget-Tests und Playwright nach 7.1 | F2, F3, B1 |
|
||||
|
||||
### 8.3 Reihenfolge und Parallelität
|
||||
|
||||
```
|
||||
Vorbedingung: #2 PR 2b → 2c gemergt; #3 KAT-01 gemergt (Einstieg, ggf. Normalisierung)
|
||||
Vorbedingung: #2 PR 2b → 2c gemergt; #3 KAT-01 gemergt
|
||||
13a: B1 → B2 → B3, T1
|
||||
13b: U1 ─┐
|
||||
F1 ─┴→ F2 (Mock-Repository, danach gegen B1) → F3 → T2
|
||||
13b wird erst nach 13a gemergt (RPC-Vertrag muss in main sein).
|
||||
13b wird nach 13a gemergt.
|
||||
```
|
||||
|
||||
F1 und U1 starten sofort nach Freigabe der Spec, parallel zu B1. 13b schließt #13. Merge nur durch Chris.
|
||||
F1 und U1 starten nach Freigabe der Spec, parallel zu B1. Merge nur durch Chris.
|
||||
|
||||
## 9. Risiken & offene Fragen
|
||||
|
||||
|
|
@ -448,57 +428,68 @@ F1 und U1 starten sofort nach Freigabe der Spec, parallel zu B1. 13b schließt #
|
|||
|
||||
| # | Risiko | Auswirkung | Gegenmaßnahme |
|
||||
|---|---|---|---|
|
||||
| R1 | 2b/2c weichen bei der Umsetzung von den in Spec #2 beschriebenen Namen oder Rückgaben ab | 13a passt nicht | B1 liest die gemergten Migrationen zuerst; Abweichungen im PR nennen |
|
||||
| R2 | `adopt_report_target` ist `security definer` und erreicht `repoint_game_references`, das RLS umgeht | Fehler würden fremde Daten berühren | Strenge Prüfungen (4.4) inkl. Prüfung auf fremde Verweise, `for update`, nur eigene private, nicht zusammengeführte Spiele; pgTAP-Negativfälle; Code-Review mit Fokus Sicherheit |
|
||||
| R3 | Trigramm-Schwelle 0,8 ist bei kurzen Titeln streng oder bei Reihen („Catan“, „Catan: Seefahrer“) großzügig | Ähnliche werden nicht gezeigt bzw. zu viele | Klärung 15 ist verbindlich; Kandidatenschritt blockiert nicht, er verlangt nur eine Bestätigung; Erfahrungen nach R1 sammeln |
|
||||
| R4 | `pg_trgm` erkennt Wortzeichen abhängig von der Datenbank-Locale | Ähnlichkeitswerte weichen zwischen lokal und Produktion ab | Normalisierung liefert nur `a–z`, `0–9` und Leerzeichen (3.3); damit ist die Trigrammbildung locale-unabhängig; pgTAP-Fälle mit Umlauten und Akzenten |
|
||||
| R5 | Edition-Hinweis per Ableitung (E7) zeigt auch dann „Edition neu wählen“, wenn der Nutzer nie eine Edition gewählt hatte, aber eine private Variante existiert | Unnötiger Hinweis | Selten (vorläufige Einträge haben anfangs keine Varianten); wegklickbar; Alternative in Spec-F6 |
|
||||
| R6 | Hinweis „am Eintrag“ braucht Screens aus #4/#6/#11, die evtl. noch fehlen | Krit. 4–6 nur in „Meine Meldungen“ sichtbar | Spec-F8: Abhängigkeiten setzen oder Einbau dort nachziehen |
|
||||
| R7 | Nutzer melden das gleiche Spiel mehrfach (eigene offene Meldungen werden nicht geprüft, Spec-F7) | Mehr Arbeit für Chris, doppelte vorläufige Einträge | Tageslimit; Chris lehnt Duplikate mit Verweis ab |
|
||||
| R8 | Kein Realtime | Ergebnis erscheint erst beim nächsten Laden | Akzeptiert (manuelle Bearbeitung, kein Zeitdruck) |
|
||||
| R1 | 2b/2c/#3 weichen von den beschriebenen Namen oder Rückgaben ab | 13a passt nicht | B1 liest die gemergten Migrationen zuerst; Abweichungen im PR nennen |
|
||||
| R2 | `adopt_report_target` ist `security definer` und erreicht `repoint_game_references` | Fehler würden fremde Daten berühren | Prüfungen 4.4 inkl. fremder Verweise mit `is distinct from`, feste Sperrreihenfolge, kein Aufruf von `admin_*`, Rechte- und Attribut-Tests |
|
||||
| R3 | Schwelle 0,8 ist bei kurzen Titeln streng, bei Reihen großzügig | zu wenige/zu viele Kandidaten | Klärung 15 verbindlich; Kandidatenschritt verlangt nur eine Bestätigung |
|
||||
| R4 | Limit-Sperre wirkt nur unter READ COMMITTED | unter höherer Isolation 11+ Meldungen möglich | PostgREST-Standard; in 3.2 festgehalten; manueller Paralleltest |
|
||||
| R5 | Ableitung der Edition (Rückfall) ist ungenau | unnötige oder fehlende Hinweise, Partien nicht erfasst | Empfehlung Zähler `editions_cleared` (Spec-F6) |
|
||||
| R6 | Hinweis „am Eintrag“ braucht Screens aus #4/#6/#11 | Krit. 4–6 sonst nur in „Meine Meldungen“ | Spec-F8 |
|
||||
| R7 | Mehrfachmeldungen desselben Spiels | mehr Arbeit für Chris | Tageslimit; Ablehnen mit Verweis |
|
||||
| R8 | Kein Realtime | Ergebnis erst beim nächsten Laden | akzeptiert |
|
||||
|
||||
### 9.2 Offene Fragen an Chris (Spec-F1 bis Spec-F9)
|
||||
|
||||
Keine Frage blockiert die Umsetzung: Für jede gibt es eine umsetzbare Empfehlung, die mit der Freigabe der Spec gilt, falls Chris nichts anderes entscheidet.
|
||||
Keine Frage blockiert die Umsetzung: Ohne andere Entscheidung gilt mit der Freigabe die Empfehlung.
|
||||
|
||||
| # | Frage | Empfehlung | Begründung |
|
||||
|---|---|---|---|
|
||||
| Spec-F1 | **Krit. 1:** Sind Verlag und Erscheinungsjahr Pflicht oder optional? Der Text nennt nur den Titel als Pflicht. | **Optional**, nur der Titel ist Pflicht. | Nutzer kennen das Jahr oft nicht; eine Pflicht würde zu geratenen Werten führen. 2c hat beide Spalten nullable. |
|
||||
| Spec-F2 | **Krit. 2:** Wie gilt die Trigramm-Regel, wenn das Jahr in der Meldung oder im Katalogeintrag fehlt? | Trigramm ≥ 0,8 **ohne Jahresbedingung**. | Lieber einmal mehr nachfragen als ein Duplikat; der Schritt verlangt nur eine Bestätigung. |
|
||||
| Spec-F3 | **Krit. 2 „Artikel und Sonderzeichen ignoriert“:** Welche Artikel, wo, und zählen Umlaute/Akzente als Sonderzeichen? | Nur **führende** Artikel auf Deutsch und Englisch (Liste in 3.3); Umlaute und Akzente werden **gefaltet** (`ö` = `o`, `é` = `e`, `ß` = `ss`) über eine feste `translate()`-Zuordnung; alle anderen Zeichen werden ignoriert. | Artikel mitten im Titel sind bedeutungstragend („Herr der Ringe“). Ohne Faltung würden „Mölkky“ und „Molkky“ **nicht** als ähnlich erkannt: Die Trigramm-Ähnlichkeit liegt bei etwa 0,4, weit unter 0,8. `unaccent` scheidet aus, weil es nicht `immutable` ist; `translate()` ist es und braucht keine Erweiterung. Nachteil: „ae“-Schreibweisen („Raeuber“) werden nicht auf „ä“ abgebildet. Alternative: Umlaute als eigene Buchstaben behalten (strenger, dann werden Schreibvarianten mit und ohne Umlaut nicht erkannt). |
|
||||
| Spec-F4 | **Krit. 7 „heute“:** Kalendertag in welcher Zeitzone, oder gleitende 24 Stunden? | **Kalendertag in Europe/Berlin.** | Entspricht „heute“ im Kriterium und ist für Nutzer verständlich („morgen geht es wieder“). Eine Profil-Zeitzone gibt es nicht. |
|
||||
| Spec-F5 | **Krit. 4–6:** Wie lange bleibt der Hinweis am Eintrag sichtbar? | **Bis der Nutzer „Verstanden“ wählt** (`result_seen_at`). Die Markierung „abgelehnt“ (Krit. 6) bleibt dauerhaft; in „Meine Meldungen“ bleibt der Status immer sichtbar. | Ein dauerhafter Hinweis an einem längst ersetzten Eintrag stört; ein automatisches Ausblenden nach Zeit könnte übersehen werden. |
|
||||
| Spec-F6 | **N15, Edition:** Woran erkennt die App, dass die Edition geleert wurde? | **Ableitung** (E7): vorläufiger Eintrag hat eine eigene Variante und der Eintrag am Ziel hat keine Edition. Alternative: Spalte `game_reports.edition_cleared`, gesetzt von `admin_resolve_report`/`adopt_report_target`; dafür müsste `repoint_game_references` (2b) zusätzlich zählen, wie viele **Verweise** geleert wurden, und 2c bzw. 13a die Funktionen anpassen. | Die Ableitung braucht keine Änderung an den bereits freigegebenen Teilen 2b/2c. Die Alternative ist genauer und lohnt sich, wenn 2b/2c ohnehin noch nicht gebaut sind; dann sollte die Änderung in Spec #2 nachgetragen werden, nicht in 13a per `create or replace`. |
|
||||
| Spec-F7 | **Krit. 2:** Sollen auch eigene offene Meldungen bzw. eigene vorläufige Einträge mit ähnlichem Titel angezeigt werden („Du hast dieses Spiel schon gemeldet“)? | **Nein in R1.** | Kriterium nennt nur Katalogeinträge; Duplikate begrenzt das Tageslimit, Chris lehnt sie mit Verweis ab. |
|
||||
| Spec-F8 | **Abhängigkeiten:** Krit. 3 („in Sammlung oder Listen“) und der Hinweis „am Eintrag“ (Krit. 4–6) leben in Screens von SAM-01 (#6), KAT-02 (#4) und SAM-03 (#11). Sollen #4 und #6 als Forgejo-Abhängigkeit von #13 gesetzt werden? | **Ja, #13 hängt zusätzlich von #4 und #6 ab** (beide liegen in der Reihenfolge ohnehin vor #13). #11 nicht: Den Einbau des Hinweises in die durchsuchbare Sammlung übernimmt #11, falls es später kommt. | Sonst müsste #13 eigene Zwischenlösungen für Listeneintrag und Detailseite bauen, die später ersetzt werden. |
|
||||
| Spec-F9 | **Krit. 3:** Welche Liste ist im Formular vorausgewählt? | **Sammlung**, änderbar auf Wunsch-/Einkaufsliste oder „nicht aufnehmen“. | Häufigster Fall: Nutzer will ein Spiel erfassen, das er besitzt (Story: „Katalogloch blockiert beim Erfassen“). |
|
||||
| Spec-F1 | **Krit. 1:** Sind Verlag und Jahr Pflicht? | **Optional**, nur der Titel ist Pflicht. | Nutzer kennen das Jahr oft nicht; 2c hat beide Spalten nullable. |
|
||||
| Spec-F2 | **Krit. 2:** Trigramm-Regel bei fehlendem Jahr? | ≥ 0,8 **ohne Jahresbedingung**. | Lieber einmal mehr nachfragen; der Schritt verlangt nur eine Bestätigung. |
|
||||
| Spec-F3 | **Krit. 2 „Artikel ignoriert“:** welche Artikel und wo? (Sonderzeichen, Groß-/Kleinschreibung und Akzente regelt `search_normalize` aus Spec #3.) | Nur **ein führender** Artikel auf Deutsch oder Englisch (Liste in 3.3). | Artikel mitten im Titel sind bedeutungstragend („Herr der Ringe“); andere Sprachen sind im Katalog selten. |
|
||||
| Spec-F4 | **Krit. 7 „heute“:** Kalendertag oder 24 Stunden? | **Kalendertag Europe/Berlin.** | Entspricht „heute“; verständlich („morgen geht es wieder“). |
|
||||
| Spec-F5 | **Krit. 4–6:** Wie lange bleibt der Hinweis sichtbar? | **Bis „Verstanden“**; „abgelehnt“ bleibt dauerhaft markiert. | Dauerhafte Hinweise stören, zeitgesteuertes Ausblenden wird übersehen. |
|
||||
| Spec-F6 | **N15, Edition:** Woran erkennt die App die geleerte Edition? | **Neu bewertet: Zähler `game_reports.editions_cleared integer not null default 0`** (Nachtrag in Spec #2: 2b liefert in `repoint_game_references` die Zahl der geleerten **Verweise** in Sammlung und Partien, wie es `variants_cleared` bei `admin_merge_games` schon für Verweise tut; 2c legt die Spalte an und `admin_resolve_report` setzt sie; 13a setzt sie in `adopt_report_target`). Rückfall: Ableitung (5.4). | Genau, erfasst auch Partien, und **jetzt günstig**, weil 2b und 2c noch nicht gemergt sind; später wäre ein `create or replace` der Admin-Funktionen nötig. Die Ableitung kommt ohne Änderung an Spec #2 aus, ist aber ungenau (R5). |
|
||||
| Spec-F7 | **Krit. 2:** auch eigene frühere Meldungen als ähnlich zeigen? | **Nein in R1.** | Kriterium nennt nur Katalogeinträge. |
|
||||
| Spec-F8 | **Abhängigkeiten:** Soll #13 zusätzlich von #4 und #6 abhängen? | **Ja** (beide liegen ohnehin vorher); #11 übernimmt den Einbau selbst. | Sonst Zwischenlösungen, die später ersetzt werden. |
|
||||
| Spec-F9 | **Krit. 3:** vorausgewählte Liste? | **Sammlung.** | Häufigster Fall laut Story. |
|
||||
|
||||
### 9.3 Entscheidungen innerhalb der Architektur (zur Kenntnis, kein ADR nötig)
|
||||
### 9.3 Entscheidungen innerhalb der Architektur (kein ADR nötig)
|
||||
|
||||
- RPC als Bündelung mit `security invoker`, Rechte in der Datenbank (E2, E3); nur `adopt_report_target` ist `security definer` (E5).
|
||||
- Kein Realtime, keine Edge Function, keine neuen Pakete (E9, R8).
|
||||
- Kein ADR: Die Entscheidungen bleiben innerhalb von ADR-0001/0002 und sind ohne großen Aufwand änderbar.
|
||||
- RPC als Bündelung mit `security invoker`, Rechte in der Datenbank; nur `adopt_report_target` ist `security definer` (E2, E3, E5).
|
||||
- Normalisierung gehört #3; 13a ergänzt nur den Artikel-Wrapper (E4).
|
||||
- Kein Realtime, keine Edge Function, keine neuen Pakete.
|
||||
|
||||
### 9.4 Hinweise für andere Stories
|
||||
|
||||
- **KAT-01 (#3):** Einstieg „Spiel melden“ aus der leeren Trefferliste mit `/reports/new?title=<suchbegriff>` (Krit. 6); Normalisierung `normalize_game_title` für die Suche mitnutzen (3.3); legt #3 alternative Titel an, auch in `find_similar_catalog_games` berücksichtigen.
|
||||
- **KAT-02 (#4), SAM-01 (#6), SAM-03 (#11):** `ReportOutcomeNotice` und `reportNoticeForGameProvider` am Eintrag einbauen; vorläufige Einträge mit Badge „vorläufig“; eine Detailansicht für ein eigenes privates Spiel (nicht in `catalog_games`) lesen sie direkt aus `games`.
|
||||
- **KAT-03 (#12):** Aktion „Edition wählen“ aus dem Edition-Hinweis anbinden.
|
||||
- **KTO-02 (#14):** Die Policy `favorite_games_own` erlaubt derzeit jedes sichtbare Spiel, also auch eigene vorläufige; Klärung 23 verlangt nur Katalogspiele. #14 muss die Policy (neue Migration) auf `visibility = 'catalog'` einschränken.
|
||||
- **PAR-01 (#15):** vorläufige Spiele als wählbare Spiele anbieten (Krit. 3, Klärung 36), mit Badge.
|
||||
- **KTO-05 (#23):** Export enthält `game_reports` (inkl. Status und Link) und vorläufige Spiele; Löschen per Kaskade ist durch pgTAP in 13a abgedeckt.
|
||||
- **SAM-08 (#25):** Import-Meldungen mit `origin = 'import'` und `bgg_id`, einem vorläufigen Eintrag je Meldung (E1); der Limit-Trigger ignoriert sie (N12); die Hinweise aus 5.4 gelten unverändert; Kennzeichen „aus BGG-Import“ in „Meine Meldungen“ (5.5).
|
||||
- **Spec #2 (2b/2c), falls Spec-F6 = Zähler:** Nachtrag vor dem Bau (Spalte `editions_cleared`, Zähler der geleerten Verweise, Setzen in `admin_resolve_report`).
|
||||
- **KAT-01 (#3):** derselbe Satz zu Zuständigkeit und Rechten von `search_normalize` (3.3); Einstieg `/reports/new?title=<suchbegriff>` (Krit. 6); `game_alt_titles` wird von `find_similar_catalog_games` gelesen.
|
||||
- **KAT-02 (#4), SAM-01 (#6), SAM-03 (#11):** `ReportOutcomeNotice` und `reportNoticeForGameProvider` einbauen; Badge „vorläufig“; eigene private Spiele direkt aus `games` lesen.
|
||||
- **KAT-03 (#12):** Aktion „Edition wählen“ anbinden.
|
||||
- **KTO-02 (#14):** `favorite_games_own` erlaubt derzeit jedes sichtbare Spiel, auch eigene vorläufige; Klärung 23 verlangt Katalogspiele. #14 schränkt die Policy (neue Migration) auf ein **sichtbares Katalogspiel** ein (Bedingung wie `catalog_games`: `visibility = 'catalog' and hidden_at is null and merged_into_id is null`).
|
||||
- **PAR-01 (#15):** vorläufige Spiele wählbar (Klärung 36), mit Badge; nach Freigabe/Wechsel zeigen Partien das Katalogspiel, auch für Mitspieler.
|
||||
- **KTO-05 (#23):** Export enthält `game_reports` und vorläufige Spiele.
|
||||
- **SAM-08 (#25):** Import-Meldungen mit `origin = 'import'`, `bgg_id` und vorläufigem Eintrag; Limit-Trigger greift nicht (N12); Hinweise aus 5.4 gelten.
|
||||
|
||||
### 9.5 Ausstehende Prozessschritte
|
||||
|
||||
- Freigabe der Spec und Antworten zu Spec-F1 bis Spec-F9 durch Chris (Label `status/spec-review`).
|
||||
- Bei Spec-F6 Alternative: Nachtrag in Spec #2 (2b/2c) vor deren Umsetzung.
|
||||
- Bei Spec-F8 Ja: Forgejo-Abhängigkeiten #13 → #4, #6 setzen (Chris oder Orchestrator).
|
||||
- Freigabe der Spec und Antworten zu Spec-F1 bis Spec-F9 (Label `status/spec-review`).
|
||||
- Bei Spec-F6 = Zähler: Nachtrag in Spec #2 vor dem Bau von 2b/2c.
|
||||
- Bei Spec-F8 = Ja: Forgejo-Abhängigkeiten #13 → #4, #6 setzen.
|
||||
- Spec #3 erhält denselben Satz zu Zuständigkeit und Rechten von `search_normalize`.
|
||||
|
||||
## 10. Änderungsprotokoll
|
||||
|
||||
### 2026-11-03 – Entwurf
|
||||
|
||||
Erste Fassung durch architect. Berücksichtigt den Hinweis aus der KAT-05-Spec (Issue-Kommentar vom 2026-11-03, Klärung N15) zur eingeschränkten Erhaltung der Edition bei Krit. 4.
|
||||
Erste Fassung durch architect, inkl. Hinweis aus PR #51 (Klärung N15). Nachtrag am selben Tag: Trigramm-Index gestrichen, Abweichung von Spec #2 § 3.4 vermerkt, `report_day` für testbare Tagesgrenze.
|
||||
|
||||
Nachtrag am selben Tag: Normalisierung faltet Akzente per `translate()` (Spec-F3 neu begründet: ohne Faltung läge „Mölkky/Molkky“ bei etwa 0,4); Trigramm-Index gestrichen (wäre mit `similarity() >= 0.8` nicht nutzbar); Abweichung von Spec #2 § 3.4 (Update-Recht auf `result_seen_at`) ausdrücklich vermerkt und Testanpassung in B2 ergänzt; Hilfsfunktion `report_day` für testbare Tagesgrenze.
|
||||
### 2026-11-03 – Review (code-reviewer)
|
||||
|
||||
- **B1:** `report_day` mit `execute` für `authenticated` (sonst 42501 im Trigger); Trigger mit `when (new.origin = 'user')`; Voraussetzung READ COMMITTED; pgTAP fügt als `authenticated` ein.
|
||||
- **B2/B3:** Normalisierung gehört Spec #3 (`search_normalize`); 13a nur `strip_leading_article` und Wrapper `normalize_game_title`; `translate()`-Faltung gestrichen; Regeln A und B auf `normalize_game_title`; alternative Titel verpflichtend; alle „falls #3“-Bedingungen entfernt (#3 vor 13a); Satz zur Zuständigkeit/Rechten; `unaccent`-Immutability als Konvention.
|
||||
- `adopt_report_target`: Sperrreihenfolge, `is distinct from`, Attribut- und Rechte-Tests, kein `admin_*`-Aufruf; `result_seen_at` bleibt null (Hinweis am Ziel nach Wechsel).
|
||||
- Insert-Policy verlangt `provisional_game_id`; 23505 → `report_provisional_in_use`.
|
||||
- Gelöschter vorläufiger Eintrag in `outcomeOf` (`rejectedReference`, nur Status); Hinweis am Ziel nur bei `merged_into_id = target`.
|
||||
- Indizes aus 2c übernommen; Testdatei `10_…`; Tabellenzahl unverändert gegenüber `main`.
|
||||
- Edition: Ableitung nur `list = 'owned'`; Spec-F6 neu bewertet, Empfehlung Zähler `editions_cleared`.
|
||||
- `p_year integer`, Titel > 200 → leere Menge, Limit-Vorprüfung vor der Ähnlichkeit, Großbuchstaben-Testfall, Partien/Mitspieler, Hinweis #14 präzisiert.
|
||||
|
|
|
|||
Loading…
Reference in a new issue