shinkan-jinkendo/docs/compliance-implementation.md
Lars 04663e090a
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 1m3s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 55s
docs: update compliance docs and HANDOVER to reflect P-13 full state (0.8.94)
- compliance-implementation.md: expand P-13 section with all extensions
  (0.8.88-0.8.94: audit log, email notifications, club-admin rights,
  workflow bar, archive split, badge, CI fix); update test summary to
  159 passed; add operativ check items 9-13 for P-13 extensions;
  bump app version header to 0.8.94
- compliance-package-register.md: expand P-13 Letzter Stand with full
  extension list through 0.8.94; update Fortschritt version
- compliance-roadmap.md: update app version to 0.8.94; P-13 row in
  Schnellreferenz marked done (was showing as "next recommended");
  Etappe B P-13 hint updated with full feature summary
- HANDOVER.md: update app version header to 0.8.94; §5b P-13 section
  rewritten with complete feature list including all 0.8.88-0.8.94
  additions; §6 next session updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 06:28:35 +02:00

663 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Compliance-Implementierung Umsetzungsbericht
**Erstellt:** 2026-05-09
**Zuletzt aktualisiert:** 2026-05-11
**Audit-Basis:** `docs/compliance-audit.md`
**App-Version nach Umsetzung:** 0.8.94
---
## Freigegebene Pakete und Umsetzungsstatus
### P-01 Rechtstexte ⚠️ (technischer Teil umgesetzt; juristische Inhalte offen)
**Status:** Technisch teilweise umgesetzt (2026-05-10, Version 0.8.69)
**Betroffene Dateien:**
- `frontend/src/pages/LegalPage.jsx` (neu) — Platzhalter-Komponente für alle vier Seiten
- `frontend/src/App.jsx` — 4 neue öffentliche Routen
- `frontend/src/pages/LoginPage.jsx` — Rechtstext-Links im Card-Footer
- `frontend/src/components/DesktopSidebar.jsx` — Rechtstext-Links im Sidebar-Footer
**Technische Änderung:**
Vier öffentliche Routen angelegt, kein Auth erforderlich, kein Redirect für nicht eingeloggte Nutzer:
- `/impressum``<LegalPage type="impressum" />`
- `/datenschutz``<LegalPage type="datenschutz" />`
- `/nutzungsbedingungen``<LegalPage type="nutzungsbedingungen" />`
- `/medienrichtlinie``<LegalPage type="medienrichtlinie" />`
Jede Seite enthält:
- Deutlich sichtbaren Platzhalterhinweis: „MUSTER / PLATZHALTER Inhalt wird vor Produktivbetrieb juristisch geprüft und durch den Betreiber ergänzt."
- Strukturierte Pflichtfelder je Rechtstext (Betreiber, Rechtsgrundlagen, Betroffenenrechte etc.)
- Querlinks zu den anderen drei Rechtstextseiten
Links zu allen vier Seiten sind in der Login-/Registrierungsseite (Card-Footer) und im Desktop-Sidebar-Footer sichtbar ohne Anmeldung erreichbar.
**Nicht umgesetzt (außerhalb Freigabe):**
Juristische Texte, inhaltliche Überprüfung, Admin-konfigurierbare Inhalte, Einwilligungs-Checkboxen.
**KRIT-01 Blocking-Status:**
Der Blocker KRIT-01 bleibt **offen** bis juristisch geprüfte Texte durch den Betreiber eingepflegt sind. Die technische Voraussetzung (Routen und Seitenstruktur) ist erfüllt.
**Tests:** 5 Playwright-Tests, alle grün:
- Impressum ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Datenschutz ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Nutzungsbedingungen ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Medienrichtlinie ohne Auth erreichbar + Platzhalterhinweis sichtbar + Reload OK
- Login-Seite enthält alle vier Rechtstext-Links
---
### P-01b Mobile/PWA-Erreichbarkeit der Rechtstexte ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.70) — Nacharbeit zu P-01
**Betroffene Dateien:**
- `frontend/src/pages/SettingsLegalPage.jsx` (neu) — Hub-Seite `/settings/legal`
- `frontend/src/App.jsx` — Route `/settings/legal` im ProtectedLayout
- `frontend/src/pages/AccountSettingsPage.jsx` — Link zu `/settings/legal`
**Technische Änderung:**
Neue Seite `/settings/legal` im eingeloggten Einstellungsbereich: Hub-Seite mit Links zu allen vier Rechtstextseiten, erreichbar über Einstellungen → Rechtliches. Entspricht dem bestehenden Pattern von `/settings/system`. Auf der AccountSettingsPage ist ein Link „Rechtliches" unterhalb der System-Info-Verlinkung ergänzt.
**Tests:** 3 Playwright-Tests, alle grün (17/17 Gesamt):
- Einstellungen enthält Link zu `/settings/legal`
- `/settings/legal` enthält Überschrift + alle vier Rechtstext-Links
- Jeder Link aus `/settings/legal` führt zur korrekten öffentlichen Route
---
### P-01c Admin-konfigurierbare Rechtstexte ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.71) — Nacharbeit zu P-01
**Betroffene Dateien:**
- `backend/migrations/047_legal_documents.sql` (neu) — Tabellen `legal_documents` + `legal_document_audit`
- `backend/routers/legal_documents.py` (neu) — Öffentliche + Superadmin-Endpoints
- `backend/main.py` — Router-Registrierung
- `frontend/src/pages/LegalPage.jsx` — API-Fetch mit Fallback auf statischen Platzhalter
- `frontend/src/pages/AdminLegalDocumentsPage.jsx` (neu) — Superadmin-UI
- `frontend/src/App.jsx` — Route `/admin/legal-documents`
- `frontend/src/components/AdminPageNav.jsx` — Link „Rechtstexte" im Admin-Nav
- `frontend/src/utils/api.js` — 8 neue API-Funktionen
**Technische Änderung:**
**Datenbank (Migration 047):**
Tabelle `legal_documents`: versionierte Rechtstexte mit Workflow `draft → published → archived`. Felder: `document_type` (impressum | privacy_policy | terms_of_use | media_policy), `version` (INT, auto-inkrementiert), `title`, `content_sections` (JSONB: `[{heading, content}]`), `status`, `change_note`, Timestamps, FK auf Ersteller + Publisher. Partial-Unique-Index: nur ein `published`-Datensatz pro `document_type` gleichzeitig. Tabelle `legal_document_audit`: unveränderlicher Änderungslog je Dokument.
**Backend-Endpoints:**
| Endpoint | Auth | Beschreibung |
|----------|------|--------------|
| `GET /api/legal-documents/{type}/published` | Kein | Liefert veröffentlichtes Dokument oder `null` |
| `GET /api/admin/legal-documents` | Superadmin | Alle Versionen aller Typen |
| `POST /api/admin/legal-documents` | Superadmin | Neuen Entwurf anlegen |
| `GET /api/admin/legal-documents/{id}` | Superadmin | Einzeldokument mit `content_sections` |
| `PUT /api/admin/legal-documents/{id}` | Superadmin | Entwurf bearbeiten (nur `status=draft`) |
| `POST /api/admin/legal-documents/{id}/publish` | Superadmin | Veröffentlichen; bisherige Version → `archived` |
| `POST /api/admin/legal-documents/{id}/archive` | Superadmin | Archivieren |
| `GET /api/admin/legal-documents/{id}/audit` | Superadmin | Änderungslog |
**Frontend:**
`LegalPage.jsx` ruft beim Laden `GET /api/legal-documents/{type}/published` ab. Gibt die API `null` zurück (kein veröffentlichtes Dokument vorhanden), zeigt die Seite weiterhin den bisherigen Platzhalter mit MUSTER-Banner. Ist ein Dokument veröffentlicht, wird dessen Inhalt ohne Platzhalter-Banner angezeigt. `AdminLegalDocumentsPage.jsx` unter `/admin/legal-documents` (nur Superadmin) ermöglicht Erstellen, Bearbeiten, Veröffentlichen und Archivieren von Entwürfen mit Tabs pro Dokumententyp und Änderungslog.
**Kein neues npm-Paket notwendig** — JSONB-Struktur mit `{heading, content}` statt Markdown; keine XSS-Gefahr.
**Tests:** 3 Playwright-Tests:
- Rechtstextseiten laden ohne Fehler (API-fetch mit Fallback)
- `/admin/legal-documents` erreichbar für Superadmin mit korrekter Überschrift
- Admin-Nav enthält Link zu Rechtstexten
---
### P-01c Erweiterung — Als-Entwurf-kopieren ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.72) — Ergänzung zu P-01c
**Betroffene Dateien:**
- `backend/routers/legal_documents.py` — Neuer Endpoint `POST /api/admin/legal-documents/{id}/copy-as-draft`
- `frontend/src/utils/api.js``copyLegalDocumentAsDraft(id)`
- `frontend/src/pages/AdminLegalDocumentsPage.jsx` — „Als Entwurf kopieren"-Button in der Dokumentliste
**Technische Änderung:**
Neuer Superadmin-Endpoint kopiert `title` und `content_sections` eines bestehenden Dokuments in einen neuen Entwurf. Die Versionsnummer wird dabei automatisch inkrementiert (letztes Dokument dieses Typs + 1). Status ist immer `draft`. Ermöglicht inkrementelle Überarbeitung ohne vollständige Neueingabe.
**Motivation:** Bei jeder fälligen Textanpassung mussten alle Abschnitte neu erfasst werden. Die Kopierfunktion ermöglicht, den letzten Stand zu übernehmen und nur die geänderten Abschnitte zu bearbeiten.
---
### P-01c Erweiterung — Echter PDF-Download + Abschnitts-Sortierung ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.74) — Ergänzung zu P-01c
**Betroffene Dateien:**
- `frontend/src/pages/AdminLegalDocumentsPage.jsx``generateLegalPdf()` via jsPDF, `SectionEditor` mit Sortierung
- `frontend/src/pages/LegalPage.jsx``generateLegalPdf()` via jsPDF, Button „PDF herunterladen"
- `frontend/package.json` — neues npm-Paket `jspdf`
**Technische Änderung — PDF:**
Ersetzt die bisherige `window.open()` + `window.print()`-Lösung (Browser-Druckdialog) durch `jsPDF`. Die Funktion `generateLegalPdf(doc)` erzeugt ein A4-PDF client-seitig mit:
- Titel (bold, 20 pt), Metazeile (Version + Gültigkeitsdatum), Trennlinie
- Abschnitte mit Heading (bold, 11 pt) und Fließtext (10 pt, `splitTextToSize` für automatischen Zeilenumbruch)
- Automatischer Seitenumbruch bei `y > 277 mm`
- Footer auf jeder Seite: „Shinkan Jinkendo | Exportiert am DD.MM.YYYY Seite X von Y"
- Direkter Dateidownload via `pdf.save('{document_type}_v{version}.pdf')`
Gilt sowohl für die Admin-Seite (Download aus der Dokumentliste, ruft `getLegalDocument(id)` für Volldokument ab) als auch für `LegalPage.jsx` (öffentlich, nur bei veröffentlichten Dokumenten).
**Technische Änderung — Abschnitts-Sortierung und Einfügen:**
`SectionEditor` in `AdminLegalDocumentsPage.jsx` erhält:
- ▲/▼-Buttons pro Abschnitt (deaktiviert an den Rändern) — Reihenfolge per Array-Swap
- `InsertButton` zwischen jedem Abschnitt (inkl. vor dem ersten) — fügt leeren Abschnitt an beliebiger Stelle per `splice` ein
- Kein Drag-and-Drop-Framework — reine React-State-Manipulation
---
### P-03 Papierkorb-Retention-Job aktivieren ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `docker-compose.yml` neuer Service `retention-cron`
**Technische Änderung:**
Neuer Docker-Service `retention-cron` nutzt dasselbe Backend-Image und führt `scripts/media_retention_job.py` täglich um 03:00 Uhr aus. Der Service startet beim ersten Hochfahren sofort und schläft bis zum nächsten 3 AM (Python-basierter Loop ohne externe Cron-Abhängigkeit). Zugriff auf DB und Media-Volume identisch zur Backend-Konfiguration.
**Tests:** Keine automatisierten Tests möglich (Runtime-Verhalten); operativ über Container-Logs (`docker logs shinkan-retention-cron`) überprüfbar.
---
### P-03b Retention-Zeiten mit Löschkonzept abgleichen ✅
**Status:** Umgesetzt (2026-05-10)
**Betroffene Dateien:**
- `backend/media_lifecycle.py:24` Default `HIDDEN_TO_PURGE_DAYS`
- `docker-compose.yml` explizite Env-Variable im `retention-cron`-Service
**Befund des Re-Audits:**
Das fachliche Löschkonzept (Audit §6.4) sieht 30 + 30 Tage vor:
- Stufe 1 → Stufe 2 (Soft → Hidden): 30 Tage ✓ (war bereits korrekt)
- Stufe 2 → Purge (Hidden → gelöscht): **weitere 30 Tage** → Code-Default war 90 Tage ❌
**Technische Änderung:**
```python
# media_lifecycle.py Zeile 24 (vorher "90", jetzt "30"):
HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "30")))
```
`docker-compose.yml` enthält jetzt explizit `MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS: "${...:-30}"` mit Kommentar, der das 30+30-Konzept dokumentiert. Der Wert kann per Env-Variable in einzelnen Deployments überschrieben werden.
---
### P-04 Copyright-Pflicht bei Archiv-Promotion vereinheitlichen ✅
**Status:** Umgesetzt; Tests nachgehärtet (2026-05-10)
**Betroffene Dateien:**
- `backend/routers/media_assets.py` `patch_media_asset()` und `bulk_media_patch()`
- `backend/tests/test_media_assets_copyright_promotion.py`
**Technische Änderung:**
Beide Endpoints prüfen, ob `copyright_notice` vorhanden ist, wenn `visibility` auf `club` oder `official` gewechselt wird. Priorität: Wert aus dem Request-Body > bestehender Wert in der DB. Ist beides leer, wird HTTP 400 zurückgegeben.
Fehlermeldung: `"Für Vereins- oder offizielle Medien ist eine Urheberrechtsangabe (copyright_notice) Pflicht."` (Umlaut korrekt, 2026-05-10 korrigiert)
**Tests:** 7 Tests, alle grün:
- Promotion zu `club` ohne Copyright → **400** (exakt)
- Promotion zu `official` ohne Copyright → **400** (exakt)
- Promotion zu `club` mit Copyright im Body → **200** + Payload-Prüfung (gehärtet)
- Promotion zu `club`, Copyright bereits auf Asset → **200** + Payload-Prüfung (gehärtet)
- Kein Visibility-Wechsel → keine Copyright-Prüfung → **200** (exakt)
- Bulk: ohne Copyright → in `failed`, `updated_count == 0` (exakt)
- Bulk: mit Copyright → in `updated`, `updated_count == 1` (exakt)
---
### P-05 Passwort-Mindestlänge angleichen ⚠️ (Teil 1 von 2 Re-Audit-Auflage offen)
**Status:** Teilweise umgesetzt
**Betroffene Dateien (initialer Fix 2026-05-09):**
- `backend/routers/auth.py:101` `PUT /api/auth/pin`: `< 4``< 8`
- `frontend/src/pages/LoginPage.jsx:175` `minLength="6"``minLength="8"`
- `frontend/src/pages/AccountSettingsPage.jsx:403` `minLength={4}``minLength={8}`
**Verbleibende Lücke identifiziert im Re-Audit 2026-05-09:** `POST /api/auth/reset-password` hatte kein Mindestlängen-Limit → siehe P-05b.
---
### P-05b reset-password Mindestlänge 8 Zeichen ✅
**Status:** Umgesetzt (2026-05-10)
**Betroffene Dateien:**
- `backend/models.py:29` `PasswordResetConfirm.new_password`
- `backend/tests/test_auth_password_reset_minlength.py` 7 neue Tests
**Technische Änderung:**
```python
# models.py (vorher):
class PasswordResetConfirm(BaseModel):
token: str
new_password: str
# models.py (nachher):
class PasswordResetConfirm(BaseModel):
token: str
new_password: str = Field(min_length=8, max_length=128)
```
FastAPI lehnt Requests mit `new_password < 8 Zeichen` nun mit HTTP **422** (Pydantic Validation Error) ab, bevor der Endpoint-Handler ausgeführt wird. Kein DB-Zugriff erfolgt für unvalide Requests.
**Tests:** 7 Tests, alle grün:
- Leer-String → 422
- 1-Zeichen-Passwort → 422
- 7-Zeichen-Passwort (`"1234567"`) → 422
- Exakt 8 Zeichen (`"12345678"`) → **200**
- Langes Passwort → **200**
- Fehlendes `new_password`-Feld → 422
- Gültiges Passwort, ungültiger Token → 400
**P-05 vollständig geschlossen?** Ja — alle passwortverarbeitenden Backend-Endpoints erzwingen jetzt Mindestlänge 8:
| Endpoint | Mindestlänge | Mechanismus |
|---|---|---|
| `POST /api/auth/register` | 8 | `if len(password) < 8` im Handler |
| `PUT /api/auth/pin` | 8 | `if len(new_pin) < 8` im Handler |
| `POST /api/auth/reset-password` | 8 | Pydantic `Field(min_length=8)` |
| Management-Reset (profiles.py) | 8 | Pydantic `Field(min_length=8)` |
| Frontend LoginPage | 8 | `minLength="8"` |
| Frontend AccountSettingsPage | 8 | `minLength={8}` |
---
### P-07 ALLOW_PUBLIC_MEDIA_STATIC Release-Test ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `backend/tests/test_security_release.py` 2 neue Tests
**Tests:** Beide grün.
---
### P-11 Legal-Hold Lifecycle-Status ✅
**Status:** Vollständig umgesetzt (2026-05-11, Version 0.8.84 + Nachfixe 0.8.850.8.86)
#### P-11 Kernum­setzung (Version 0.8.84)
**Betroffene Dateien:**
- `backend/migrations/051_legal_hold.sql` neue Spalten `legal_hold_active`, `legal_hold_reason_code`, `legal_hold_reason_note`, `legal_hold_set_by_profile_id`, `legal_hold_set_at`, `legal_hold_released_by_profile_id`, `legal_hold_released_at`, `legal_hold_release_note` in `media_assets`; Audit-Log-CHECK um `legal_hold_set`/`legal_hold_released` erweitert
- `backend/media_legal_hold.py` Neu: zentrale Services `set_legal_hold`, `release_legal_hold`, `assert_not_under_legal_hold`, `is_media_available_for_normal_use`, `assert_superadmin_for_legal_hold`
- `backend/media_lifecycle.py` `run_retention_pass()` filtert `AND (legal_hold_active = FALSE OR legal_hold_active IS NULL)` in beiden Retention-Queries
- `backend/routers/media_assets.py` `admin_legal_hold_router` mit drei Endpoints; `_list_active_visibility_clause()` berücksichtigt `include_legal_hold`-Parameter; `download_media_asset_file()` prüft Legal-Hold für Nicht-Superadmins
- `backend/routers/exercises.py` `attach_exercise_media_from_asset()` ruft `assert_not_under_legal_hold()` vor Verknüpfung auf
- `backend/main.py` `app.include_router(media_assets.admin_legal_hold_router)`
- `frontend/src/utils/api.js` `setMediaAssetLegalHold`, `releaseMediaAssetLegalHold`, `listMediaAssetsWithLegalHold`
- `frontend/src/pages/MediaLibraryPage.jsx` Legal-Hold-Badge auf Kacheln; Superadmin-Aktionen „Sofort sperren"/„Sperre aufheben" im Edit-Modal; Bestätigungs-Dialog mit Pflichtfeldern; Journal-Renderpfad für `legal_hold_set`/`legal_hold_released`
- `frontend/src/app.css` CSS-Klassen für Legal-Hold-Badge, -Dialog, -Button
**Neue API-Endpoints (Superadmin):**
- `POST /api/admin/media-assets/{asset_id}/legal-hold` — Sofortsperre setzen (reason_code + reason_note Pflicht)
- `POST /api/admin/media-assets/{asset_id}/legal-hold/release` — Sofortsperre aufheben (release_note Pflicht)
- `GET /api/admin/media-assets/legal-hold` — Liste aller aktuell gesperrten Assets
**Tests:** 15 Backend-Unit-Tests in `backend/tests/test_p11_legal_hold.py` — alle grün.
#### P-11 Nachfixe UI-Bugs und Sichtbarkeitskorrektur (Version 0.8.850.8.86)
Nach Erstimplementierung wurden beim Testen weitere Mängel festgestellt und behoben:
**Version 0.8.85 UI-Bugfixes:**
- Fix: `submitLegalHold` rief `loadMedia()` statt `loadItems()` auf → "loadMedia is not a function" beim Sperren
- Fix: Listabfrage in `list_media_assets` enthielt `legal_hold_active`, `reason_code`, `reason_note`, `set_at` nicht → Badge und „Sperre aufheben"-Button im Modal waren nie sichtbar
- Fix: Journal-Renderpfad verwendete Keys `nw.legal_hold_reason_code/note` statt `nw.reason_code/note` (Audit-Log-Format) → Begründung und Kommentar nicht angezeigt
- Fix: Archiv-Picker (ExerciseFormPage, ExerciseInlineFileMediaModal) filterte Legal-Hold-Assets bereits client-seitig heraus
**Version 0.8.86 Vollständige Absicherung der Auslieferung:**
Betroffene Dateien:
- `backend/routers/exercises.py` `download_exercise_media_file()` gibt HTTP 451 zurück wenn `asset_legal_hold_active=TRUE` (Datei wird nicht ausgeliefert); `enrich_exercise_detail()` SELECT erweitert um `ma.legal_hold_active AS asset_legal_hold_active`
- `backend/routers/media_assets.py` `list_media_assets` übergibt `include_legal_hold=is_sup` statt `include_legal_hold=(is_plat or is_sup)` — Legal-Hold-Assets nur noch für Superadmin sichtbar, nicht für alle Plattform-Admins
- `frontend/src/components/ExerciseMediaEmbed.jsx` Zeigt „Medium nicht verfügbar (gesperrt)" statt Datei wenn `asset_legal_hold_active`
- `frontend/src/components/ExerciseMediaThumbTile.jsx` Zeigt rot-markierte „Gesperrt"-Kachel statt Dateivorschau; kein Preview-Trigger
- `frontend/src/pages/ExerciseFormPage.jsx` Vorschau-Modal zeigt Hinweis statt Datei wenn `asset_legal_hold_active`
#### Sicherheitsarchitektur (vollständig)
- Legal-Hold ist orthogonal zum normalen Papierkorb-Lifecycle (P-03) — kein 30-Tage-Warten
- `rights_status='blocked'` wird als Schnell-Spiegel gesetzt und bei Aufhebung basierend auf vorhandenen Deklarationen wiederhergestellt (`declared` wenn Deklaration vorhanden, sonst `legacy_unreviewed`)
- Nur Superadmin darf setzen/aufheben; nur Superadmin sieht Legal-Hold-Assets in der Medienliste; normale Nutzer sehen gesperrte Assets nicht
- Retention-Job überspringt Legal-Hold-Assets (verhindert versehentliche Löschung unter laufender Sperrmaßnahme)
- `assert_not_under_legal_hold()` blockiert das Verknüpfen von Legal-Hold-Assets mit Übungen
- Dateiauslieferung (`download_exercise_media_file`) gibt HTTP 451 zurück — keine Umgehung via direkten File-Endpoint
- Frontend-Komponenten zeigen Placeholder statt Datei, auch wenn das Asset bereits in einer Übung verknüpft ist
---
### P-12 sessionStorage bei Logout bereinigen ✅
**Status:** Umgesetzt (2026-05-10, Version 0.8.68)
**Betroffene Dateien:**
- `frontend/src/context/AuthContext.jsx` `logout()`
- `tests/dev-smoke-test.spec.js` neuer Playwright-Test
**Befund (war offen):**
`logout()` löschte nur `localStorage`-Einträge. `TrainingCoachPage` schrieb sessionStorage-Schlüssel mit Präfix `sj_coach_` (`sj_coach_step_{unitId}`, `sj_coach_deltas_{unitId}`, `sj_coach_debrief_{unitId}`), die nach Logout im Tab erhalten blieben. Bei Nutzerwechsel im selben Tab (geteilter Rechner) konnte der neue Nutzer Trainingsfortschritt des Vorgängers sehen.
**Technische Änderung:**
Gezielte Präfix-Löschung aller `sj_coach_*`-Schlüssel beim Logout (kein `sessionStorage.clear()`). Fremde sessionStorage-Schlüssel (Browser-Extensions o. ä.) bleiben erhalten.
```javascript
// AuthContext.jsx logout() — Ergänzung:
for (const key of Object.keys(sessionStorage)) {
if (key.startsWith('sj_coach_')) {
sessionStorage.removeItem(key)
}
}
```
**Begründung gezielte statt vollständiger Löschung:**
Alle Shinkan-spezifischen sessionStorage-Schlüssel sind eindeutig über den Präfix `sj_coach_` identifizierbar (definiert in `TrainingCoachPage.jsx` Zeilen 1525). Ein `sessionStorage.clear()` würde auch Schlüssel fremder Quellen im selben Tab löschen; die Präfix-Löschung ist spezifischer und sicherer.
**Tests:** Playwright E2E-Test `tests/dev-smoke-test.spec.js` (Test „P-12: sessionStorage wird bei Logout bereinigt"):
- Setzt drei `sj_coach_*`-Schlüssel und einen fremden Schlüssel
- Klickt „Abmelden"
- Prüft: `sj_coach_*` → null (entfernt), fremder Schlüssel → erhalten, `authToken` → null (localStorage weiterhin korrekt bereinigt)
**Hinweis Tests:** Das Projekt verfügt über kein Frontend-Unit-Test-Framework (kein Vitest/Jest in package.json). Der Test ist als Playwright E2E-Test implementiert, der einen laufenden Dev-Server voraussetzt. Automatisierte Ausführung der Playwright-Tests erfordert `npx playwright test` mit gesetzter `PLAYWRIGHT_BASE_URL`.
---
### P-23 LoginPage: minLength + Versionsstring ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `frontend/src/pages/LoginPage.jsx`
**Technische Änderung:**
- `minLength="6"``minLength="8"`
- Hardcodierter Versionsstring `v0.1.0 • Development` entfernt
---
### P-24 CORS allow_methods und allow_headers einschränken ✅
**Status:** Umgesetzt
**Betroffene Dateien:**
- `backend/main.py:85-86`
**Technische Änderung:**
```python
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
```
---
## Test-Zusammenfassung (Stand 0.8.94)
```
tests/test_auth_password_reset_minlength.py 7 passed (neu, P-05b)
tests/test_media_assets_copyright_promotion.py 7 passed (gehärtet, P-04)
tests/test_media_rights_declaration.py 25 passed (neu, P-06aP-06d)
tests/test_security_release.py 9 passed (inkl. 2 P-07-Tests)
tests/test_p11_legal_hold.py 15 passed (neu, P-11)
tests/test_p13_content_reports.py 15 passed (neu, P-13; CI-Fix 0.8.94)
Weitere bestehende Tests: 81 passed, 6 skipped
Gesamt (Backend): 159 passed, 6 skipped, 1 failed
Fehlgeschlagener Test: test_list_media_assets_invalid_lifecycle_400
→ Pre-existing: benötigt laufenden PostgreSQL-Container (Hostname "postgres")
→ Bestand bereits vor allen Compliance-Änderungen (verifiziert per git stash)
Playwright E2E (dev-smoke-test.spec.js): 22+ passed (inkl. P-06-Tests)
→ P-01: 4× Route ohne Auth + Platzhalterhinweis + Reload, 1× Login-Links
→ P-01b: 3× /settings/legal (Link in Einstellungen, Überschrift, Rechtstext-Links)
→ P-01c: 3× Admin-Rechtstexte (Seitenladung, Route /admin/legal-documents, Admin-Nav)
→ P-12: sessionStorage-Bereinigung (grün)
→ P-06: 5× RightsDeclarationDialog (Anzeige, Pflichtfeld-Validierung, Upload-Flow,
Promotions-Dialog, Legacy-Altbestand-Indikator)
Anmerkung jsPDF (0.8.74):
→ PDF-Download via pdf.save() ist ein Browser-Download, kein Server-Request.
→ Kein Backend-Test möglich; funktional im Browser verifizierbar.
→ Playwright-E2E-Test erfordert laufenden Dev-Server (npx playwright test).
Anmerkung P-06+ Journal/Korrektur (0.8.820.8.83):
→ Journal-Endpoint und Korrektur-Endpoint durch manuellen API-Test (curl) auf Dev-System verifiziert.
→ Bugfix club_admin-Prüfung (has_club_role) verifiziert: 500 → 200 nach Fix.
→ Keine dedizierte Playwright-Testsuite für Journal-Modal und Korrektur-Formular (UI-Verifikation ausstehend).
Anmerkung P-11 Frontend-Absicherung (0.8.850.8.86):
→ UI-Fixes (loadItems, Badge-Sichtbarkeit, Journal-Keys) manuell auf Dev-System verifiziert.
→ 15 Backend-Unit-Tests decken Services und Retention-Schutz ab.
→ Keine Playwright-Tests für Legal-Hold-Aktionen im Modal — manuelle UI-Verifikation.
Anmerkung P-13 Erweiterungen (0.8.880.8.94):
→ E-Mail-Benachrichtigungen, Audit-Log, Club-Admin-Rechte, Workflow manuell auf Dev-System verifiziert.
→ CI-Fix (0.8.94): 3 pytest-Tests angepasst (frühzeitige 403, vollständige Mock-Zeile, DB-Mock für COUNT).
→ Alle 15 pytest-Tests grün nach CI-Fix.
→ Keine Playwright-Tests für InboxPage Meldungs-Workflow — manuelle UI-Verifikation.
```
---
### P-06 Upload-Einwilligungsdialog ⚠️ (technisch vollständig umgesetzt inkl. P-06+; juristische Validierung offen)
**Status:** Technisch vollständig umgesetzt (inkl. P-06+ Volljournal + Korrektur, Version 0.8.83) — KRIT-04 bleibt offen.
**Deklarationsversion:** `p06-v1-conservative`
---
#### P-06aP-06d — Kernum­setzung (Version 0.8.75)
**Betroffene Dateien:**
- `backend/migrations/048_media_rights_declarations.sql` (neu) — Append-only Deklarations-Log + 3 Schnellfelder in `media_assets` (`rights_status`, `rights_declared_for_visibility`, `rights_declared_at`)
- `backend/migrations/049_media_rights_consent_context.sql` (neu) — Kontext-Freitextfelder in `media_asset_rights_declarations` (`person_consent_context`, `parental_consent_context`, `music_rights_context`, `third_party_rights_context`)
- `backend/media_rights.py` (neu) — Zentrales Policy-Modul: `validate_rights_declaration`, `check_rights_coverage`, `assert_rights_for_promotion`, `assert_rights_for_exercise_link`, `write_rights_declaration`, `update_rights_quick_fields`
- `backend/routers/media_assets.py` — P-06-Enforcement in Bulk-Upload, PATCH, Bulk-PATCH; 3 neue Endpoints
- `backend/routers/exercises.py` — P-06 bei `upload_exercise_media` (neue Assets) und `attach_exercise_media_from_asset`
- `frontend/src/components/RightsDeclarationDialog.jsx` (neu) — Einwilligungsdialog (9 Pflichtfelder + Kontext-Freitexte)
- `frontend/src/pages/MediaLibraryPage.jsx` — Dialog-Integration vor Bulk-Upload; Altbestand-Indikator
- `frontend/src/pages/ExerciseInlineFileMediaModal.jsx` + `ExerciseInlineEmbedModal.jsx` — RightsDeclarationDialog vor Upload
- `frontend/src/utils/api.js``bulkUploadMediaAssets` erweitert um P-06-Felder
- `backend/tests/test_media_rights_declaration.py` (neu) — 25 Unit/HTTP-Tests
**Neue Endpoints (P-06b):**
| Endpoint | Beschreibung |
|----------|-------------|
| `POST /api/media-assets/{id}/rights-declarations` | Explizite Re-/Nachdeklaration |
| `GET /api/admin/media-rights/legacy-summary` | Zusammenfassung Altbestand nach Sichtbarkeit (Plattform-Admin) |
| `GET /api/admin/media-rights/legacy-assets` | Paginierte Liste Altbestand club/official (Plattform-Admin) |
**Abweichung von Spec §3 (konservative Erstannahme):**
Person-Fragen sind auch bei Sichtbarkeit `private` Pflicht (§10.1 in `docs/p06-upload-rights-spec.md`).
**Altbestand (Legacy):**
Alle vor Migration 048 hochgeladenen Medien erhalten `rights_status = 'legacy_unreviewed'`.
Promotion blockiert bis Nachdeklaration. In Bibliotheks-UI als „Altbestand ⚠" markiert.
---
#### P-06+ — Volljournal + Korrektur (Version 0.8.820.8.83)
**Motivation:** Vollständige Nachvollziehbarkeit aller Medien-Ereignisse, nicht nur der Deklarationen. Plus: Möglichkeit, fehlerhafte Deklarationen mit Begründung nachträglich zu korrigieren (append-only — neueste gilt).
**Betroffene Dateien:**
- `backend/migrations/050_media_audit_log.sql` (neu) — Neue Tabelle `media_asset_audit_log` + `correction_note TEXT` in Deklarations-Tabelle + erweiterter CHECK (action_type += 'correction')
- `backend/media_rights.py` — Neue Funktionen: `write_audit_log_entry()`, `write_rights_correction_declaration()`
- `backend/routers/media_assets.py` — Neues Pydantic-Model `RightsCorrectionBody`; PATCH-Endpoint schreibt Audit-Log-Einträge; Lifecycle-Aktionen schreiben `lifecycle_change`-Einträge; 2 neue Endpoints; Import `has_club_role` ergänzt
- `frontend/src/pages/MediaLibraryPage.jsx` — Journal-Modal komplett überarbeitet: unified `events[]`-Ansicht; Korrektur-Formular inline; Helper-Funktionen `actionTypeLabel`, `eventTypeLabel`, `visLabel`
- `frontend/src/utils/api.js` — Neue Funktion `addMediaAssetDeclarationCorrection(assetId, body)`
- `frontend/src/app.css` — CSS für Audit-Einträge (`--audit`), Korrektur-Einträge (`--correction`), Korrektur-Formular
**Neue Endpoints (P-06+):**
| Endpoint | Auth | Beschreibung |
|----------|------|-------------|
| `GET /api/admin/media-rights/assets/{id}/journal` | Superadmin / Uploader / Vereins-Admin | Volljournal: `events[]` aus Deklarationen + Audit chronologisch gemischt. Gibt `can_correct` zurück. |
| `POST /api/admin/media-rights/assets/{id}/correction` | Superadmin / Uploader / Vereins-Admin | Korrektur-Deklaration (append-only, neueste gilt). Felder = P-06-Dialog + `correction_note`. |
**Automatische Audit-Log-Einträge:**
| event_type | Auslöser |
|-----------|----------|
| `visibility_change` | PATCH wenn `visibility` oder `club_id` sich ändert |
| `copyright_change` | PATCH wenn `copyright_notice` sich ändert |
| `metadata_change` | PATCH wenn `original_filename` etc. sich ändert |
| `lifecycle_change` | Lifecycle-Aktionen: trash_soft, trash_hidden, recover, reactivate (nicht bei Hard-Delete/Purge) |
**Bugfixes in P-06+:**
| Bug | Fix |
|-----|-----|
| Journal + Korrektur gaben 500 (falsches Schema club_admin) | `has_club_role(cur, profile_id, club_id, "club_admin")` statt `AND role = 'admin'` in `club_members` |
| Frontend-Build-Fehler: doppelte `lcLabel`-Deklaration | Duplikat in Zeile 264 von `MediaLibraryPage.jsx` entfernt |
**KRIT-04 Status:**
Offen. Juristische Validierung der Feldtexte (§10.3 p06-v1-conservative, T1T10), KUG/DSGVO-Anforderungen (§7.1§7.12 der Spec) und Korrekturfähigkeit (Spec §11.9) steht aus.
Referenz: `docs/p06-upload-rights-spec.md` §10.5, §11.9.
---
### P-13 Content-Melde-Backend ✅
**Status:** Vollständig umgesetzt (2026-05-11, Version 0.8.870.8.94)
**Finding:** KRIT-03
**Architekturentscheidung:** Anstelle einer separaten Moderations-Queue wurde die bestehende Admin-Inbox (`InboxPage.jsx`) um einen zweiten Abschnitt erweitert. Keine generische `inbox_items`-Tabelle, keine separate `/api/admin/reports`-Queue.
#### P-13 Kernum­setzung (Version 0.8.87)
**Betroffene Dateien:**
- `backend/migrations/052_content_reports.sql` (neu) — Tabelle `content_reports` mit Status-Workflow, Priorisierung, 3 Indizes
- `backend/routers/content_reports.py` (neu) — alle Endpoints, Reason-Code-Mapping, Priorisierung
- `backend/tests/test_p13_content_reports.py` (neu) — 15 Unit-Tests
- `backend/main.py` — Router-Registrierung
- `frontend/src/utils/api.js` — 5 neue API-Funktionen (submitContentReport, getInboxContentReports, getContentReport, patchContentReport, setLegalHoldFromReport)
- `frontend/src/context/OrgInboxContext.jsx` — contentReports-State, contentReportCount, canAccessContentReports, isSuperadmin
- `frontend/src/pages/InboxPage.jsx` — zweiter Abschnitt „Inhaltsmeldungen", ReportDetailModal
- `frontend/src/components/ReportContentModal.jsx` (neu) — Meldungsformular
- `frontend/src/components/MediaPreviewModal.jsx` (neu) — geteilter Vorschau-Dialog
**API-Endpoints (Kernum­setzung):**
| Endpoint | Auth | Beschreibung |
|----------|------|--------------|
| `POST /api/content-reports` | Optional | Meldung einreichen (official-Medien ohne Login; eingeloggt: alle sichtbaren Medien) |
| `GET /api/me/inbox/content-reports` | Plattform-Admin | Liste aller Meldungen, JOIN auf Zieltabellen |
| `GET /api/content-reports/{id}` | Plattform-Admin | Einzel-Detail |
| `PATCH /api/content-reports/{id}` | Plattform-Admin | Status/Notiz/Zuweisung; resolution_note Pflicht für Abschluss-Status |
| `POST /api/content-reports/{id}/legal-hold` | Superadmin | Legal Hold via P-11 `set_legal_hold()`; setzt Status auf `resolved_legal_hold` |
**Reason-Code-Mapping (P-13 → P-11):**
| Meldegrund | Legal-Hold reason_code |
|------------|------------------------|
| copyright | copyright_complaint |
| image_rights | rights_dispute |
| privacy | privacy_complaint |
| minors | youth_protection |
| illegal_content | illegal_content |
| youth_protection | youth_protection |
| offensive_content | illegal_content |
| other | other |
---
#### P-13 Erweiterungen — Audit-Log, E-Mail, Workflow, Club-Admin (Version 0.8.880.8.94)
**Betroffene Dateien:**
- `backend/migrations/053_content_report_audit_event.sql` (neu) — `content_report_filed` Event-Typ dem `media_asset_audit_log` CHECK-Constraint hinzugefügt (alle 7 gültigen Event-Typen)
- `backend/routers/content_reports.py` — stark erweitert: E-Mail-Benachrichtigungen, Audit-Log, deutsche Labels, Club-Admin-Berechtigungen, Workflow-Verbesserungen, frühzeitige 403-Prüfung
- `backend/routers/media_assets.py``open_report_count` Batch-Query für Admin-Nutzer in `list_media_assets`
- `frontend/src/pages/InboxPage.jsx` — vollständig überarbeitetes `ReportDetailModal` mit Workflow-Bar, Archiv-Trennung
- `frontend/src/pages/MediaLibraryPage.jsx` — Badge auf Medienkarten, Journal-Eintrag für `content_report_filed`
- `frontend/src/components/ReportContentModal.jsx``onSuccess` Callback, readOnly-Felder für eingeloggte Nutzer
- `frontend/src/context/OrgInboxContext.jsx``contentReportsError`, `isClubAdmin`, `isPlatformAdmin` exponiert
**Backend-Erweiterungen:**
- **E-Mail-Benachrichtigungen** (best-effort, nach Commit): Bestätigung an Melder + Benachrichtigung aller Plattform-Admins nach Eingang einer Meldung; `original_filename` oder `exercises.title` als Medienbezeichnung im E-Mail-Betreff
- **Audit-Log-Integration**: Bei `target_type='media_asset'` Eintrag `content_report_filed` in `media_asset_audit_log`; bei Statuswechseln und reinen Notizänderungen via PATCH ebenfalls; deutsche Labels via `REASON_LABELS_DE` / `STATUS_LABELS_DE`
- **Club-Admin-Berechtigungen**: `GET /me/inbox/content-reports` liefert Club-Admins nur Meldungen zu Medien ihrer Vereine (COUNT-Check, 403 wenn keine Club-Admin-Rolle); `GET /content-reports/{id}` und `PATCH` via `_assert_can_manage_report()` auch für Club-Admins (nur eigene Vereinsmedien); `POST /content-reports/{id}/legal-hold` für Club-Admins auf nicht-offizielle Vereinsmedien via `_assert_can_set_legal_hold_from_report()`; Superadmin-Prüfung frühzeitig vor DB-Zugriff (plain Admin → sofort 403)
- **Workflow-Verbesserungen in PATCH**: Wiedereröffnen einer Meldung (`status='submitted'`) setzt `reviewed_by_profile_id` und `reviewed_at` auf NULL zurück
- **`open_report_count`** in `list_media_assets`: Für Admin-Nutzer wird je Medium die Anzahl offener Meldungen (`submitted`/`under_review`) als Batch-Query zurückgegeben
**Frontend-Erweiterungen:**
- **`WorkflowBar`** in InboxPage: 3-Schritte-Fortschrittsbalken (Eingegangen → In Bearbeitung → Abgeschlossen) mit visueller Hervorhebung des aktuellen Schritts
- **`ReportDetailModal`** überarbeitet: zeigt WorkflowBar, Prüferinformationen (`reviewed_by_name`, `reviewed_at`), `resolution_note` (readOnly bei abgeschlossenen Meldungen), eigener „Kommentar speichern"-Button, „Meldung wieder öffnen"-Button für abgeschlossene Meldungen
- **Archiv-Trennung**: Offene Meldungen (`submitted`/`under_review`) im Hauptbereich; abgeschlossene/abgewiesene Meldungen in einer kollabierbaren Archiv-Sektion (Standard: zugeklappt)
- **Legal-Hold-Button** in ReportDetailModal: sichtbar für Superadmin sowie Club-Admins (bei nicht-offiziellen Medien)
- **Badge auf Medienkarten** in MediaLibraryPage: rotes Badge mit `open_report_count` (nur sichtbar wenn > 0); wird nach erfolgreicher Meldung via `onSuccess`-Callback sofort aktualisiert
- **Journal-Eintrag** in MediaLibraryPage: `content_report_filed`-Ereignisse mit Meldungs-ID, Grund, Priorität, Status, Begründung
- **`OrgInboxContext`**: `contentReportsError` (Fehlerstring oder null), `isClubAdmin` und `isPlatformAdmin` exponiert; `contentReportCount` zählt nur offene Meldungen (submitted/under_review)
- **`ReportContentModal`**: `onSuccess`-Callback nach erfolgreichem Submit; Name und E-Mail readOnly für eingeloggte Nutzer (grauer Hintergrund)
- **Melde-Einstieg**: `MediaPreviewModal` (geteilt) + Melden-Button in MediaLibraryPage, ExerciseFormPage und ExerciseAttachmentMediaStrip
**CI-Fix (Version 0.8.94):**
3 pytest-Tests in `test_p13_content_reports.py` repariert: frühzeitige 403-Prüfung in `set_legal_hold_from_report` für plain Admin (vor DB-Zugriff); Test 10 mit korrektem DB-Mock für COUNT-Query; Test 12 mit vollständiger Mock-Zeile (`target_type`, `target_id`, `resolution_note`). Alle 15 Tests grün.
**Nicht in P-13-Scope:** P-14 (Moderations-UI als eigene Seite), P-15 (Uploader-Benachrichtigung per E-Mail bei Sperrung), P-16 (Beschwerdeverfahren).
---
## Nicht umgesetzte Pakete
> Paket-IDs und -Titel gemäß kanonischem Register `docs/compliance-package-register.md`.
> Abweichende Beschreibungen in der Ursprungsversion dieses Abschnitts wurden am 2026-05-10 korrigiert (P-06, P-08, P-09, P-10, P-11, P-18 — Details im Konsistenzbericht des Registers).
| Paket | Kanonischer Titel | Status | Begründung |
|-------|------------------|--------|------------|
| P-01 | Rechtstexte | offen | Scope ausgeschlossen (juristischer Inhalt) |
| P-02 | Self-Service-Kontolöschung + Datenexport | offen | Scope ausgeschlossen |
| P-06 | Upload-Einwilligungsdialog (Recht am eigenen Bild) | **teilweise umgesetzt** | Technisch umgesetzt (2026-05-11, v0.8.75) unter vorläufigen Erstannahmen `p06-v1-conservative` — siehe §P-06 unten. KRIT-04 bleibt offen bis juristische Validierung. |
| P-08 | HSTS / externe Proxy-Sicherheit dokumentieren | offen | Scope ausgeschlossen (außerhalb Repo — Reverse-Proxy) |
| P-09 | Admin-Audit-Log | offen | Scope ausgeschlossen |
| P-10 | Mindestalter-Abfrage | offen | Scope ausgeschlossen |
| P-11 | Legal-Hold Lifecycle-Status | ✅ umgesetzt | Version 0.8.840.8.86 — siehe §P-11 oben |
| P-12 | sessionStorage bei Logout bereinigen | ✅ umgesetzt | Version 0.8.68 — siehe §P-12 oben |
| P-13 | Content-Melde-Backend | ✅ umgesetzt | Version 0.8.87 — siehe §P-13 unten |
| P-14 | Moderations-UI | offen | Scope ausgeschlossen |
| P-15 | Uploader-Benachrichtigung bei Sperrung | offen | Scope ausgeschlossen |
| P-16 | Beschwerdeverfahren | offen | Scope ausgeschlossen |
| P-17 | MFA für Superadmins (TOTP) | offen | Scope ausgeschlossen |
| P-18 | HttpOnly-Cookie als Auth-Alternative | offen | Scope ausgeschlossen |
| P-19 | Anti-Virus-Scan (ClamAV) | offen | Scope ausgeschlossen |
| P-20 | VVT erstellen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
| P-21 | AV-Verträge abschließen | offen | Scope ausgeschlossen (Betreiber-Aufgabe) |
| P-22 | HTML-Sanitizer für Rich-Text-Felder | offen | Scope ausgeschlossen |
---
## Re-Audit-Empfehlung
Operativ prüfen (Stand 0.8.94):
1. **P-03/P-03b**: `docker logs shinkan-retention-cron` — Job läuft täglich 03:00 Uhr; Retention-Zeiten: 30 → 30 Tage
2. **P-04**: Manuell: PATCH privates Medium auf `official` ohne `copyright_notice` → muss 400 liefern
3. **P-05b**: Manuell: Reset-Link mit 7-Zeichen-Passwort → muss mit Fehler abgewiesen werden
4. **P-24**: Browser DevTools Preflight → `Access-Control-Allow-Headers: content-type, x-auth-token, x-active-club-id`
5. **P-06**: Manuell: Upload ohne `rights_holder_confirmed` → muss 400 liefern; Journal-Endpoint für vorhandene Assets → muss 200 + `events[]` liefern; Korrektur-Endpoint → muss neue Deklaration mit `action_type='correction'` schreiben
6. **P-06 Audit-Log**: PATCH Sichtbarkeit eines Assets → `media_asset_audit_log` muss Eintrag `visibility_change` enthalten
7. **P-11**: Superadmin → Medium sperren → in Übung öffnen → Kachel zeigt „Gesperrt"; direkter Dateiaufruf `/exercises/{id}/media/{mid}/file` → muss HTTP 451 liefern; Plattform-Admin (kein Superadmin) → gesperrtes Medium darf in Medienliste nicht erscheinen
8. **P-13 Grundfunktion**: Anonym → `POST /api/content-reports` für official-Medium ohne Auth → muss 200 liefern; `good_faith_confirmed=false` → muss 400 liefern; Plattform-Admin → `GET /api/me/inbox/content-reports` → Liste; Superadmin → `POST /api/content-reports/{id}/legal-hold` → setzt Legal Hold + Report-Status `resolved_legal_hold`
9. **P-13 E-Mail**: Meldung einreichen → Melder erhält Bestätigungs-E-Mail; Plattform-Admins erhalten Benachrichtigungs-E-Mail (sofern SMTP konfiguriert)
10. **P-13 Club-Admin**: Club-Admin → `GET /api/me/inbox/content-reports` → liefert nur Meldungen zu Medien des eigenen Vereins; Club-Admin → `PATCH /api/content-reports/{id}` für Meldung zu eigenem Vereinsmedium → muss 200 liefern
11. **P-13 Audit-Log**: Meldung einreichen für media_asset → `media_asset_audit_log` muss Eintrag `content_report_filed` enthalten; Statuswechsel via PATCH → muss weiteren Audit-Eintrag enthalten
12. **P-13 Badge**: Medium mit offener Meldung in Medienbibliothek → rotes Badge mit Zähler sichtbar
13. **P-13 Workflow**: Abgeschlossene Meldungen in InboxPage im Archiv (kollabierbar); Wiedereröffnen einer Meldung → `reviewed_by_profile_id` und `reviewed_at` werden zurückgesetzt
Nächster vollständiger Re-Audit empfohlen nach juristischer Klärung P-06/KRIT-04 (Textfreigabe T1T10) und nach Einpflegen der Rechtstexte P-01 durch Betreiber.