# Medien-Assets, Archiv & Lebenszyklus (Single Source of Truth)

**Status:** verbindlich für Design, API und DB-Migrationen zum Thema Medien  
**Stand:** 2026-05-08 (Ist-Changelog Medien bis 0.8.64; §11 Inline umgesetzt und erweitert)  
**ersetzt/ergänzt:** operatives Upload-Format/Größen siehe weiterhin `MEDIA_UPLOAD_SPEC.md`  
**normative Governance:** Sichtbarkeit & Mandanten wie `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`; bei Abweichung zwischen Dokumenten hat der Zugriffsplan Vorrang, **außer** dieses Dokument präzisiert explizit nur den Medien-Domain.

---

## 1. Zweck und Abgrenzung

### 1.1 Zweck

- **Ein physisches Medium** („Datei-Asset“) wird **einmal** gespeichert und **mehrfach** mit Übungen verknüpft (Wiederverwendung, keine Dubletten auf der Platte).
- **Sichtbarkeit** von Datei-Assets folgt **derselben Semantik** wie bei Übungen (`private` \| `club` \| `official` – ggf. spätere Enum-Erweiterungen nur gemeinsam mit Migration + ACCESS_LAYER-Doku).
- **Superadmin** kann Medien **global** freigeben (fachlich: Stufe wie „offiziell“ / plattformweit lesbar gemäß `library_content_*`-Regeln).
- **Copyright:** pro Asset sind **Vermerke** vorgesehen und an `official` / Vereinsnutzung anzubinden (Validierungsstufen in Umsetzung festlegen).
- **Externer Speicher:** Speicherung darf **lokal/NAS** (Filesystem) oder **extern** (z. B. S3-kompatibler Dienst) erfolgen – die App arbeitet über eine **Speicher-Abstraktion** (Interface), nicht mit verstreuten `Path`-Annahmen.
- **Papierkorb & Retention:** mehrstufiger Lebenszyklus mit **automatischen** und **manuellen** Übergängen, siehe §5.

### 1.2 Abgrenzung

- **Embeds** (reine externe URL, kein Upload) bleiben pro **Verknüpfung zur Übung** möglich (`embed_url` o. Ä.); sie haben **keinen** physischen Lebenszyklus wie Datei-Assets (kein Papierkorb „Stufe 3 physische Löschung“). Trotzdem: UI kann „Link ungültig“ o. Ä. separat abbilden.
- **Übungs-Governance** (wer eine Übung bearbeiten darf) **bleibt maßgeblich** für **Anlegen/Entfernen/Umsortieren** von Medien **in dieser Übung**; Medien erzwingen **keine** schärfere Edit-Policy als die Übung selbst.

---

## 2. Begriffe

| Begriff | Bedeutung |
|--------|-----------|
| **Media Asset** | Logische Entität für eine **hochgeladene Datei** inkl. Metadaten, Sichtbarkeit, Copyright, Speicherreferenz, Lifecycle-Status. |
| **Übungs-Medium / Attachment** | Verknüpfung **Übung ↔ Asset** oder reines Embed; enthält u. a. `context` (Sektion), `sort_order`, übungsspezifischen Titel/Beschreibung, `is_primary`. |
| **Medienmanager / Archiv** | Verwaltungs-UI/API für Assets (Suche, Metadaten, Lifecycle, ggf. Superadmin-global). |

---

## 3. Zieldatenmodell (Überblick)

> Konkrete Tabellennamen/Migrationen beim Implementierungsstart festziehen; dieses Kapitel ist die **fachliche Norm**.

### 3.1 Tabelle `media_assets` (oder gleichwertiger Name)

**Pflichtidee:**

- Identität, technische Metadaten: `id`, `mime_type`, `byte_size`, `sha256` (vollständig), `original_filename`, `created_at`, `updated_at`.
- Mandant & Governance: `visibility`, `club_id` (nullable je nach Semantik wie bei Übungen), `uploaded_by_profile_id`.
- Copyright: mindestens `copyright_notice` (TEXT); optional `license`, `attribution`, `source_url`.
- Speicher: `storage_backend` (Enum/String: z. B. `local`, `s3`, …), `storage_key` (relativer oder bucket-key), optional `storage_url` nur für interne Zwecke – **keine** öffentlichen Secrets in der DB.
- Lifecycle: `lifecycle_state` (siehe §5), Timestamps für Übergänge und geplante Purge-Termine.

**Deduplizierung:** Innerhalb sinnvoller Grenze (z. B. **pro Verein** bei `club`, global nur mit Superadmin-Policy) über `sha256` + `club_id`/`visibility`; Konflikt bei Upload → bestehendes Asset verknüpfen oder expliziter Nutzerdialog (Umsetzungsdetail).

### 3.2 Verknüpfung (heute `exercise_media`)

- Erweiterung um **`media_asset_id`** (FK, nullable wenn reines Embed).
- Embed-Zeilen: `media_asset_id IS NULL`, `embed_url`/`embed_platform` gesetzt wie heute.
- Übungsspezifische Felder: `context` (`ablauf` \| `detail` \| `trainer_hint`), `sort_order`, `title`, `description`, `is_primary`.

### 3.3 Referenzen & physisches Löschen

- Vor **physischem** Löschen muss die Referenzanzahl aller aktiven/nicht-purged Links **0** sein (oder Quarantäne-Policy); siehe §5.3.

---

## 4. Sichtbarkeit, Lesen und Übungs-Promotion

### 4.1 Leseregel Datei-Asset

- Zentrale Entscheidung analog `library_content_visible_to_profile` / TenantContext: **Profil darf Asset lesen** nur wenn Visibility+`club_id`+`created_by` zum Objekt passen (gleiche Philosophie wie Übungen).
- **GET Download / Stream:** muss **Asset-Governance** prüfen; wenn der Aufruf **im Kontext einer Übung** erfolgt, zusätzlich (oder als Schnittmenge) **Übung lesbar** – verbindlich **ohne Seitenkanal** (kein Download nur mit Übungs-ID, wenn Asset für Nutzer unsichtbar wäre).

### 4.2 Promotion Übung → `official` (bzw. global)

Beim Speichern/Promoten einer Übung auf **`official`** (oder vergleichbare globale Stufe):

1. **Pflicht-Dialog:** Hinweis, dass **alle zugeordneten Datei-Assets**, die noch **nicht** diese Sichtbarkeit haben, **mit angehoben** werden (oder die Aktion schlägt fehl / erfordert Anpassung).
2. **Abbruch:** Nutzer kann abbrechen oder einzelne Assets vor Freigabe anpassen / entkoppeln.
3. **Copyright:** Für `official` sind leere oder unzureichende Copyright-Felder **nicht** akzeptabel (genaue Regel in Umsetzung + Validierung).

Superadmin kann Medien **explizit** global/offiziell machen (Archiv-Pfad), unabhängig von einer einzelnen Übung – konsistent zur Rolle.

### 4.3 Bearbeitung Medien in der Übung

Wer die Übung **bearbeiten** darf (bestehende oder künftig erweiterte Regel), darf für diese Übung:

- Medien **hinzufügen** (Upload neu oder Verknüpfung aus Archiv),
- **Reihenfolge** / **Sektion** / **Titel** ändern,
- **Verknüpfung** entfernen (Asset bleibt im Archiv, sofern nicht anderweitig gelöscht),
- Embeds pflegen.

Das widerspricht **nicht** dem Papierkorb: wer kein Recht hat, ein Asset **global** zu Trash/ Purge zu bewegen, darf trotzdem die **Verknüpfung** in „seiner“ Übung lösen, solange die Übungs-Edit-Policy das erlaubt.

---

## 5. Lebenszyklus (Papierkorb) – Norm

### 5.1 Zustände

| Zustand | Code (Beispiel) | Neue Zuordnung zu Übungen | Anzeige in Übung (Lesemodus) | Anzeige Bearbeiten |
|---------|-----------------|---------------------------|------------------------------|-------------------|
| Aktiv | `active` | ja | normal | normal |
| Stufe 1 – Papierkorb | `trash_soft` | **nein** | ja + **Warnhinweis** („wird abgeschafft“) | ja + Warnhinweis |
| Stufe 2 – Ausblendung | `trash_hidden` | nein | **nein** (Platzhalter/„nicht verfügbar“) | Recovery-Aktion sichtbar |
| Stufe 3 – purgiert | `purged` / Zeile entfernt | nein | nein | nur noch historischer Hinweis nach Policy |

**Default-Retention (automatisch):**

- Stufe 1 → Stufe 2: **ca. 30 Tage** nach Eintritt in Stufe 1 (konfigurierbar).
- Stufe 2 → Stufe 3 **physisches Löschen**: **ca. 90 Tage** (3 Monate) nach Eintritt in Stufe 2 (konfigurierbar).

**Manuell:** Berechtigte Rollen dürfen Übergänge **vorziehen** (Stufe 1 erzwingen, Stufe 2 erzwingen, Purge erzwingen) gemäß §5.2 – ggf. **Tier-Gates** (§6).

### 5.2 Wer darf Lifecycle-Transitionen?

| Aktion | Vereins-Asset (`club`, Owner-Verein) | Privates Asset (`private`, Uploader) | `official` / plattformweit |
|--------|--------------------------------------|--------------------------------------|-----------------------------|
| Stufe 1 (Papierkorb) | **Vereinsadmin** + **Superadmin** | **Uploader** + **Superadmin** | **Superadmin** |
| Stufe 2 vorziehen / erzwingen | Vereinsadmin + Superadmin | Uploader + Superadmin | Superadmin |
| Recovery Stufe 2 → 1 | wie Stufe 1-Eintritt, ggf. Tier | wie links | Superadmin |
| Physisches Löschen (3) | Vereinsadmin + Superadmin; Systemjob nach Frist | Uploader + Superadmin | Superadmin |

**Superadmin / Systemadmin** ist immer **Override** (Klarstellung zur „systemadmin“-Formulierung).

### 5.3 Konsistenz mit bestehenden Übungen

- In **Stufe 1** bleiben Verknüpfungen funktional für Anzeige bestehen; Nutzer sehen **Warnung**.
- In **Stufe 2** wird das Medium in der **öffentlichen Übungsansicht nicht gerendert**; im **Bearbeitungsmodus** gibt es einen **Recovery-Link**, der das Asset zurück auf **Stufe 1** setzt (Policy: nur wer nach §5.2 Stufe 1 setzen darf, oder erweiterte Regel – bei Konflikt ACCESS_LAYER + dieses Dokument anpassen).
- **Physisches Löschen:** nur nach Stufe 2-Frist oder manueller Purge-Aktion; Backend entfernt Datei über Speicher-Backend und markiert Asset endgültig.

### 5.4 Hintergrundjobs

- Geplanter Job (täglich o. Ä.): Transitionen 1→2 und 2→3 gemäß Timestamps.
- Alle Zeiten **konfigurierbar** (Umgebungsvariablen oder DB-Config), Defaults wie §5.1.

---

## 6. Tier & Features

- **Tier** kann erlauben oder verbieten: **manuelles** Vorziehen von Stufe 2 oder Purge, zusätzliche Speicherquoten, externes Backend, etc.
- Tier ändert **nicht** die Übungs-Sichtbarkeit an sich; es **limitiert** nur Medien-spezifische Aktionen (Löschstufen, Archiv-Größe, …). Konkrete Feature-Keys in Umsetzung in `features` / `tier_limits` ergänzen und hier im **Changelog** dieses Dokuments verlinken.

---

## 7. Externe Speicherung (Server / S3)

- Abstraktion **StorageAdapter**: `put`, `get`, `delete`, ggf. `exists`.
- Konfiguration über ENV (z. B. Endpoint, Bucket, Credentials) – nie im Frontend.
- **NAS / anderer Rechner als App-Host:** In der Praxis `MEDIA_ROOT` in Compose/ENV auf den **Mount-Punkt** des NAS (oder NFS/SMB) setzen – die App läuft z. B. auf dem Raspberry, die Bytes liegen auf dem Speichersystem. Kein »Mediaserver« mit eigener Geschäftslogik nötig.
- **Pfadkonvention** auf der Platte z. B. `clubs/{club_id}/…` für Mandantentrennung (Umsetzung schrittweise; neue Uploads können zuerst unter `exercises/{sha256}{ext}` liegen).

### 7.1 Konfiguration: Bootstrap vs. Superadmin (Laufzeit)

| Ebene | Inhalt | Wer / Wo |
|-------|--------|----------|
| **Bootstrap** | Basis-Verzeichnis `MEDIA_ROOT` (Container/Host), ggf. S3-Secrets | Deployment (`.env`, Docker Compose) – Container muss ohne UI starten können |
| **Laufzeit (nicht-geheim)** | Zusätzlicher **relativer Unterpfad** unter `MEDIA_ROOT` (`local_relative_root`), später: aktives Backend `local` \| `s3`, öffentlicher Endpoint/Bucket-Name | **Superadmin** über Tabelle `platform_media_storage` + API; keine Secrets für S3 im Klartext in der DB (Keys nur ENV/Vault) |
| **Effektives Wurzelverzeichnis** | `Path(MEDIA_ROOT) / local_relative_root` nach Normalisierung (kein `..`, kein absoluter Pfad im relativen Segment) | berechnet im Backend bei jedem Zugriff |

**Hinweis Beta:** Primär ein Verein, Videos auf separatem physischen System → typischerweise **ein NAS-Mount** als `MEDIA_ROOT`; Superadmin kann bei Bedarf einen **Unterordner** setzen, ohne neues Image zu bauen.

**Drift:** Jede Änderung an Speicher-Konfiguration oder Asset-Schema → Migration + Eintrag **§10 Changelog** + bei neuen Endpoints **ACCESS_LAYER_ENDPOINT_AUDIT.md**. Änderungen an **Inline-Platzhaltern** (§11) → ebenfalls Changelog + ggf. Frontend-Sanitize-Regeln dokumentieren.

---

## 8. Embeds & Streaming-Plattformen

- Embeds **bleiben** erste Klasse; bestehende Plattform-Erkennung erweiterbar (`embed_platform`).
- Roadmap: weitere Hosts (Player-Komponente, oEmbed, CSP) – **ohne** Änderung am Asset-Lifecycle-Modell.

---

## 9. Pflichten zur Drift-Vermeidung

| Änderung an … | Pflegepflicht |
|---------------|---------------|
| DB-Schema Medien | Neue Migration + **Abschnitt/Changelog in diesem Dokument** (Datum, Kurzbeschreibung) |
| Sichtbarkeit / Enums | Zuerst `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, dann dieses Dokument synchron halten |
| Neue Endpoints Medien/Archiv | `ACCESS_LAYER_ENDPOINT_AUDIT.md` + `get_tenant_context`/Governance |
| **Inline-Referenzen** in Übungstexten (§11) | Renderer/Sanitizer-Regeln + dieses Dokument §11; kein zweites Rechtemodell |
| Implementierungs-Meilenstein | `backend/version.py` (`MODULE_VERSIONS` / Schema-Kommentar) und bei größerem Release `PROJECT_STATUS.md` |

---

## 10. Changelog

| Datum | Änderung |
|-------|----------|
| 2026-05-08 | **0.8.64:** Bearbeiten-UX erweitert: Drag&Drop mit Auto-Scroll in Richtung Rich-Text, Medienliste in Kachel-Grid, Warnhinweis vor Wechsel „Ansehen“ bei ungespeicherten Änderungen + Rückweg „Zurück zur Bearbeitung“, Katalogvorschau ohne separaten Anhangs-Medienstreifen. |
| 2026-05-08 | **0.8.63:** Inline-Medien-Picker als Modal (Mediathek/Upload + separates Embed-Modal), Größenwahl `small|medium|full`, Platzhalter-Caption (`data-shinkan-exercise-media-caption`), Wiederverwendung bereits verknüpfter Assets im Picker, kompakter Medienbereich mit Drag&Drop in Textfelder. |
| 2026-05-08 | **0.8.60 §11:** Inline in Übungstexten (`{{exerciseMedia:id}}` / `data-shinkan-exercise-media`); Server-Normalisierung + Validierung; Client-Sanitize und zentraler Block-Renderer (`ExerciseRichTextBlock`); CREATE ohne bestehende `exercise_media` lehnt Platzhalter ab. |
| 2026-05-07 | **0.8.59:** Dokumentation — Aktiver Verein (Profil/Header/`effective_club_id`) für Plattform-Admin und UI-Dropdown synchron; kein fachliches Archiv-Schema-Change. |
| 2026-05-07 | **0.8.58:** Medien **`official`:** Lifecycle schwerpunktmäßig **Superadmin** (nicht Plattform-Admin); Bearbeitungsdialog Bibliothek für andere Rollen **Lesemodus**; Superadmin-Upload: Vereinskontext folgt aktiv gesetztem Verein / `effective_club_id`. |
| 2026-05-07 | **0.8.47–0.8.57 (Auszug):** Übung **`official`** nur Superadmin; Vereinsübungen mit File-Assets: **Copyright-Pflicht**; Speicherpfade **`library/`** mit Vereinsordner (Name+c{id}), Medienkind-Unterordner, Governance-Umzug bei Sichtbarkeit; Bibliothek-GET mit Filtern/Tags/Nutzungs-Anzeige; Bulk-Lifecycle/PATCH; Lesemodus/Kacheln; Konflikt **409** bei Upload-Dedupe vs. Papierkorb + UI-Reaktivierung. |
| 2026-05-07 | **0.8.42–0.8.43:** Papierkorb-Lifecycle API, Retention-Skript; **`POST …/exercises/{id}/media/from-asset`**; Übungs-UI Archiv verknüpfen / Vorschau. |
| 2026-05-07 | Erstfassung als Single Source of Truth (verbindlich); Abstimmung Stakeholder: Promotion Übung↔Medien, Copyright, Papierkorb 3-stufig, externe Speicher, Embeds getrennt vom Asset-Lifecycle. |
| 2026-05-07 | §11 **Inline-Medien im Fließtext**: Leitplanken (Anker `exercise_media.id`, einheitlicher Render-Pfad, keine zweite Governance); Umsetzung **nach** tragfähigem Archiv — siehe **`docs/HANDOVER.md`** §5. |
| 2026-05-07 | **Medienmanager (Basis):** `GET /api/media-assets?lifecycle=…`, `copyright_notice` in Response; `PATCH` Copyright; `GET …/file` für Papierkorb-Zeilen bei Verwaltungsrecht; UI-Route `/media` (Medienbibliothek). |

---

## 11. Inline-Medien im Fließtext (Umsetzung & Leitplanken)

**Status:** Frontend-Renderer und API-Validierung/Normalisierung umgesetzt (App ≥ 0.8.60); verbindliche Leitplanken unten.

### 11.1 Ziel

- Medien (Player, Bild) sollen **an definierter Stelle** in Feldern wie Ablauf / Ziel / Notizen erscheinen können – zusätzlich oder statt reiner Zuordnung zu den Sektionen `ablauf` / `detail` / `trainer_hint`.
- **Keine zweite Sichtbarkeit:** Inline verweist immer auf dieselbe **Übungs-Medium-Zeile** (`exercise_media.id`) bzw. indirekt auf das gleiche Asset wie die Medienliste; **Lesen/Ausliefern** nur nach **bestehender** Übungs- + Medien-Governance (§4.1).

### 11.2 Platzhalter-Konvention (**festgelegt**)

- **Kanonisches Markup nach Speichern:** `<span data-shinkan-exercise-media="<numerische exercise_media.id>" class="shinkan-inline-media"></span>`.
- **Erweiterung (UI/Lesbarkeit):** optionales Attribut `data-shinkan-exercise-media-size="small|medium|full"` (Default `medium`) und optional `data-shinkan-exercise-media-caption="..."` für sprechende Platzhalter-Chips im Editor.
- **Kurzsyntax (Eingabe/Import):** `{{exerciseMedia:123}}` — Server normalisiert beim Speichern (PUT Übung / Varianten) in das kanonische `span`; **CREATE** ohne bestehende `exercise_media`-Zeilen verwirft Platzhalter mit **400**.

### 11.3 Rendering & Sicherheit

- **Ein zentraler Pfad** „Übungstext für Anzeige aufbereiten“: HTML sanitizen (Allowlist), erlaubte Platzhalter auflösen, **ID gehört zur aktuellen Übung** und Medium ist für den Nutzer **sichtbar** – sonst Platzhalter mit neutralem Hinweis oder ausblenden.
- **XSS/CSP:** keine rohen `iframe`/Skripte aus Nutzer-HTML ohne Kontrolle; eingebettete Player nur über kontrollierte Komponenten.

### 11.4 Koexistenz mit Sektions-Medien

- **Liste + Inline** dürfen dasselbe `exercise_media` referenzieren (ein Player an zwei Stellen).  
- Stand 0.8.64: Katalog-/Vorschaupfade priorisieren Inline-Darstellung; ein zusätzlicher Anhangsblock wird dort nicht mehr parallel erzwungen.
- Import/Wiki: vor großen Content-Migrationen **Syntax festlegen**, damit nicht irreversibel „falsches“ HTML importiert wird.

### 11.5 Wann umsetzen (Reihenfolge)

1. ~~**Erledigt (Basis):** Medien-Archiv, `media_assets`, Upload/Dedupe, Speicherpfad, Papierkorb, Bibliothek `/media`, Verknüpfung `from-asset`, Governance `official`/Copyright.~~  
2. **Umgesetzt (Stand 0.8.64):** Platzhalter/Kurzsyntax + zentraler Frontend-Render-Pfad + Server-Validierung gemäß §11.1–11.4; Editor-Einstieg „Medium einfügen“ inkl. Modal-Picker (Mediathek/Upload), separates Embed-Modal, Größenwahl und Drag&Drop aus der Medienliste.  
3. **Editor:** kein Zwang zum vollen Block-Editor vorab; **Platzhalter im bestehenden RTE** ist der vorgesehene schlanke Einstieg.

### 11.6 Refactor-Vermeidung (jetzt schon)

- Neue Features **nicht** so bauen, dass HTML aus Übungstexten an **vielen** Stellen „roh“ gerendert wird – **ein** wiederverwendbarer Renderer vorbereiten (schrittweise einziehen).  
- Medien immer über **stabile IDs** anbinden, nicht nur über Datei-URLs im Text.

---

## 12. Referenzen

- `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`
- `.claude/docs/technical/MEDIA_UPLOAD_SPEC.md` (Limits, MIME, Embed-Typen im aktuellen Backend)
- `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md`
- `backend/routers/exercises.py` – Ist-Zustand `exercise_media` bis Refactor
