# 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.85–0.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.85–0.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 15–25). 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-06a–P-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.82–0.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.85–0.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.88–0.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-06a–P-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.82–0.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, T1–T10), 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.87–0.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.88–0.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.84–0.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 T1–T10) und nach Einpflegen der Rechtstexte P-01 durch Betreiber.
