b2in/dev/displays-11-05-2026/01-status.md
Kevin Adametz 6c6d683b9a Display CMS Optimierungen 29-05-2026
- Mediathek: Video-Vorschaubilder statt Icons (FFmpeg-Thumbnails + Backfill-Command), Kategorie "Sonstiges"
- B2in Media-Picker zeigt alle Medientypen, Typ wird automatisch erkannt; Thumbnail-Preview vor allen Medien-URL-Feldern
- B2in Marke/Footer: Footer ein/aus, Logo+Claim frei positionierbar (Ecken) mit Constraints, separate Anzeige-Schalter
- Angebote-Modul dynamisch: kein Slide-Typ mehr, einheitliches Detail-Layout mit ein-/ausblendbaren Bloecken, Logo/Brand pro Slide, Streichpreis-Option
- Player: leere Module stoppen Endlosschleife, dynamische Layout-Anpassung bei verstecktem Footer/Header
- Fix: Script-Ladereihenfolge (Livewire vor Flux), entfernte stale public/flux/flux.js, Modal-Crash beim Aktualisieren behoben

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:57:33 +00:00

375 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

# Cabinet Displays Implementierungs-Status
> Konzept: siehe `00-entwicklungskonzept.md`. Diese Datei wird je Phase fortgeschrieben.
## Übersicht
| Phase | Inhalt | Status |
|---|---|---|
| **0** | Konzept-Freigabe | ✅ 11.05.2026 |
| **1** | Datenmodell + Daten-Migration | ✅ 11.05.2026 |
| **2** | API & Player (config + preview + module preview) | ✅ 12.05.2026 |
| **3** | Admin-UI: Displays-Liste mit Live/Entwurf | ✅ 12.05.2026 |
| **4** | Admin-UI: Entwurf-Editor (Iframe-Vorschau) | ✅ 12.05.2026 |
| **5** | Modul-Editor: 3-stufige Vorschau | ✅ 12.05.2026 |
| **6** | Umbenennung Versionen → Module + Onboarding | ✅ 12.05.2026 |
| **7** | Aufräumen + alte Pivot-Tabelle entfernen | ✅ 13.05.2026 |
| **8** | Review: Fehler / Optimierungen / Erweiterungen | 🟡 29.05.2026 (Befundaufnahme) |
Legende: ✅ fertig · 🟡 in Arbeit · ⏳ offen · ⛔ blockiert
---
## Defaults aus §10 (Konzept-Freigabe vom 11.05.2026)
Der User hat das Konzept freigegeben. Da keine abweichende Wahl getroffen wurde, gelten die im Konzept empfohlenen Defaults:
1. **Test-Display:** genau 1 Datensatz per Seeder angelegt, weitere können bei Bedarf erstellt werden.
2. **Entwurf verwerfen:** löscht die Draft-Playlist. Beim erneuten Anlegen wird die Reihenfolge aus Live kopiert.
3. **Module bleiben *shared*:** Modul-Änderungen wirken sofort auf alle Displays, die das Modul live einsetzen. Modul-eigene Versionierung ist *out of scope*.
4. **Polling-Mechanismus** der Player (alle 60 s) reicht; kein Push-Refresh.
---
## Live-Roll-out Hinweise
Auf dem Live-Server reicht für jede Phase:
```bash
git pull
composer install --no-dev --optimize-autoloader # falls neue Composer-Deps
php artisan migrate --force # bringt neue Migrations ein
php artisan config:cache # Caches frisch
php artisan view:clear
```
Alle strukturellen Änderungen liegen **ausschließlich** in `database/migrations/` als datierte Dateien vor auf der Live-DB reicht `php artisan migrate --force`. Datenverlust gibt es nicht: bestehende Pivot-Daten aus `display_display_version` werden in die neue Struktur überführt.
---
## Phase 1 Datenmodell
**Ziel:** neue Tabellen `display_playlists` + `display_playlist_items`, erweiterte `displays`-Spalten (`is_test`, `preview_token`), vollständige verlustfreie Migration der heutigen Pivot-Daten.
**Dateien:**
- `database/migrations/2026_05_11_*_create_display_playlists_table.php`
- `database/migrations/2026_05_11_*_create_display_playlist_items_table.php`
- `database/migrations/2026_05_11_*_add_test_flag_and_preview_token_to_displays_table.php`
- `database/migrations/2026_05_11_*_migrate_pivot_to_display_playlists.php`
- `app/Models/DisplayPlaylist.php`
- `app/Models/DisplayPlaylistItem.php`
- `app/Models/Display.php` (neue Relations, alte `versions()` bleibt kompatibel)
- `database/factories/DisplayPlaylistFactory.php`
- `database/factories/DisplayPlaylistItemFactory.php`
- `tests/Feature/DisplayPlaylistMigrationTest.php`
**Wichtig:** Die alte Tabelle `display_display_version` und die Relation `Display::versions()` bleiben in dieser Phase **erhalten** (Removal erst in Phase 7), damit keine bestehenden Funktionen brechen.
### Stand 11.05.2026 ✅ abgeschlossen
#### Gelieferte Migrationen (Live-Reihenfolge)
| Reihenfolge | Datei | Zweck |
|---|---|---|
| 1 | `2026_05_11_113300_add_test_flag_and_preview_token_to_displays_table.php` | `displays.is_test` + `displays.preview_token` (unique) |
| 2 | `2026_05_11_113310_create_display_playlists_table.php` | Neue Tabelle für Live/Entwurfs-Bespielung pro Display |
| 3 | `2026_05_11_113320_create_display_playlist_items_table.php` | Geordnete Module pro Bespielung |
| 4 | `2026_05_11_113330_migrate_pivot_to_display_playlists.php` | Übernimmt bestehende Pivot-Daten als Published-Playlists (idempotent) |
Die Daten-Migration ist **idempotent** wenn pro Display bereits eine Published-Playlist existiert, wird sie übersprungen. Die alte Pivot-Tabelle `display_display_version` bleibt erhalten.
#### Neue Modelle & Relations
- `App\Models\DisplayPlaylist` mit Scopes `published()`/`draft()`, Relation `modules()` (geordnet)
- `App\Models\DisplayPlaylistItem`
- `App\Models\Display` erweitert um `playlists()`, `livePlaylist()`, `draftPlaylist()`, `liveModules()`, `ensurePreviewToken()`
- `Display::versions()` ist als `@deprecated` markiert, bleibt aber funktional
#### Tests
```text
tests/Feature/DisplayPlaylistMigrationTest.php 11 passed
tests/Feature/DisplayListTest.php ok
tests/Feature/DisplayVersionTest.php ok
tests/Feature/DisplayVersionApiTest.php ok
tests/Feature/DisplayMediaTest.php ok
```
Insgesamt 67 grüne Tests rund um Displays (11 neu + 56 bestand). Keine Linter-Warnungen, Pint clean.
#### Roll-out auf Live
```bash
git pull
php artisan migrate --force
```
Reihenfolge stimmt durch die Timestamps automatisch. Daten-Migration ist idempotent, kann beliebig oft laufen.
---
## Phase 2 API & Player
**Ziel:** bestehende Player-Konfiguration liest künftig aus der Published-Playlist. Zusätzlich gibt es öffentliche Vorschau-Endpunkte für Display-Entwürfe per Token und einzelne Module. Das JSON-Schema bleibt rückwärtskompatibel (`playlist[]`, `updated_at`).
### Stand 12.05.2026 ✅ abgeschlossen
Geplante/aktuelle Dateien:
- `app/Services/DisplayPlaylistConfigBuilder.php`
- `app/Http/Controllers/Api/DisplayVersionApiController.php`
- `app/Http/Controllers/Api/DisplayPreviewController.php`
- `app/Http/Controllers/Api/ModulePreviewController.php`
- `routes/domains.php`
- `public/_cabinet/display/index.html`
- `tests/Feature/DisplayVersionApiTest.php`
Umsetzung:
- Live-Config: `GET /api/display/{display}/config` liest `livePlaylist`
- Live-Check: `GET /api/display/{display}/check` bezieht sich auf die Published-Playlist
- Draft-Preview: `GET /api/display/preview/{token}` liefert die Draft-Playlist
- Modul-Preview: `GET /api/display/module/{module}/preview` liefert ein Einzelmodul im Player-Schema
- Player-Preview-Seiten: `/preview/{token}` und `/preview/module/{module}`
#### Tests
```text
tests/Feature/DisplayVersionApiTest.php 13 passed
tests/Feature/DisplayPlaylistMigrationTest.php ok
```
Insgesamt 24 grüne Tests für Phase 2 und die Playlist-Grundlage. Pint clean.
#### Hinweis
In der lokalen Umgebung musste der alte Route-Cache einmal mit `php artisan route:clear` geleert werden, damit die neuen Preview-Routen sichtbar wurden. Für Live bleibt beim Roll-out `php artisan config:cache` bzw. ein frischer Route-/Config-Cache relevant.
---
## Phase 3 Admin-UI: Displays-Liste mit Live/Entwurf
**Ziel:** Die Display-Liste zeigt pro physischem Display den Live-Stand und optionalen Entwurf nebeneinander. Entwürfe können aus Live angelegt, verworfen und veröffentlicht werden. Das Test-Display ist sichtbar hervorgehoben.
### Stand 12.05.2026 ✅ abgeschlossen
Dateien:
- `app/Livewire/Admin/Cms/DisplayList.php`
- `resources/views/livewire/admin/cms/display-list.blade.php`
- `tests/Feature/DisplayListTest.php`
Umsetzung:
- Display-Karten zeigen `Live` und `Entwurf` als zwei getrennte Spalten
- Der globale Bearbeiten-Button im Display-Kopf wurde entfernt; Live und Entwurf haben jeweils eigene Bearbeiten-Buttons
- Die Live-Card zeigt die Player-URL direkt als kopierbares Feld; der API-Link ist weniger prominent unten rechts platziert
- Live-Spalte nutzt `livePlaylist.modules`
- Entwurf-Spalte nutzt `draftPlaylist.modules`
- Live-Bearbeitung speichert nur die Published-Playlist
- Entwurfs-Bearbeitung speichert nur die Draft-Playlist und erzeugt bei Bedarf den Preview-Token
- Die Modul-Auswahl im Bearbeiten-Dialog zeigt nur noch Module, die in der aktuell bearbeiteten Bespielung noch nicht enthalten sind
- Der Plus-Button fügt auch dann das erste verfügbare Modul hinzu, wenn der Select-Wert wegen des Platzhalters noch nicht explizit gesetzt wurde
- Aktion `Entwurf anlegen` kopiert den aktuellen Live-Stand und erzeugt bei Bedarf den Preview-Token
- Aktion `Veröffentlichen` ersetzt die Published-Playlist durch den Draft und synchronisiert die alte Pivot-Tabelle weiterhin kompatibel
- Aktion `Verwerfen` löscht die Draft-Playlist
- Test-Displays (`is_test`) werden in der Liste hervorgehoben
- Der bestehende Bearbeiten-Dialog pflegt die Live-Bespielung weiter und synchronisiert bis Phase 7 zusätzlich `display_display_version`
#### Tests
```text
tests/Feature/DisplayListTest.php 14 passed
```
Die neuen Tests decken Draft anlegen, verwerfen, veröffentlichen, getrennte Live-/Entwurfs-Bearbeitung, gefilterte Modul-Auswahl sowie die Darstellung von Live-/Entwurf-Modulen ab.
---
## Phase 4 Admin-UI: Entwurf-Editor mit Iframe-Vorschau
**Ziel:** Beim Bearbeiten eines Entwurfs ist die Player-Vorschau direkt sichtbar. Moduländerungen im Entwurf werden sofort in die Draft-Playlist geschrieben und laden die Vorschau neu.
### Stand 12.05.2026 ✅ abgeschlossen
Dateien:
- `app/Livewire/Admin/Cms/DisplayList.php`
- `resources/views/livewire/admin/cms/display-list.blade.php`
- `tests/Feature/DisplayListTest.php`
Umsetzung:
- Der Entwurfs-Editor zeigt rechts eine 9:16-Iframe-Vorschau
- Die Iframe-Vorschau sitzt dauerhaft unterhalb der Aktualisieren-Aktion, damit sie im Desktop-Modal nicht mit dem Formular überlappt
- Die Vorschau lädt `/preview/{token}` mit Cache-Bust-Parameter
- Hinzufügen, Entfernen und Sortieren von Modulen persistiert bei Draft-Bearbeitung sofort in `display_playlist_items`
- Nach jeder Moduländerung wird die Iframe-Vorschau per `wire:key` neu aufgebaut
- Vollbild-Vorschau ist aus dem Editor heraus verlinkt
#### Tests
```text
tests/Feature/DisplayListTest.php 21 passed
```
Die Tests decken das Rendern der Vorschau-URL und das sofortige Persistieren von Draft-Änderungen für Preview-Reloads ab.
---
## Phase 5 Modul-Editor: 3-stufige Vorschau
**Ziel:** Module können beim Bearbeiten visuell geprüft werden: kleine Vorschau je Item, eingebettete Player-Vorschau und Vollbild-Vorschau.
### Stand 12.05.2026 ✅ abgeschlossen
Dateien:
- `app/Livewire/Admin/Cms/DisplayVersionEditor.php`
- `resources/views/livewire/admin/cms/display-version-editor.blade.php`
- `resources/views/livewire/admin/cms/partials/version-editor-video.blade.php`
- `resources/views/livewire/admin/cms/partials/version-editor-b2in.blade.php`
- `resources/views/livewire/admin/cms/partials/version-editor-offers.blade.php`
- `tests/Feature/DisplayVersionTest.php`
Umsetzung:
- Modul-Editor zeigt eine 9:16-Iframe-Vorschau über `/preview/module/{module}`
- Vollbild-Vorschau ist direkt aus dem Editor verlinkt
- Das Item-Bearbeiten-Modal ist breiter und zeigt unterhalb der Aktualisieren-Aktion ebenfalls eine 9:16-Iframe-Vorschau
- `Aktualisieren` im Item-Bearbeiten-Modal schließt das Modal nicht mehr, sondern speichert den Inhalt und lädt die Iframe-Vorschau neu
- Die Modal-Aktionen stehen unterhalb der Iframe-Vorschau und bieten `Aktualisieren`, `Abbrechen` und `Schließen`
- Die Vorschau im Item-Bearbeiten-Modal nutzt eine eigene Item-Preview und zeigt nur den aktuell bearbeiteten Slide statt das komplette Modul
- Der Display-Player rendert seinen Viewport strikt als 9:16-Fläche und skaliert Slide-, Footer- und QR-Elemente proportional zur Player-Fläche
- Harte Player-Elemente wie B2in-Headerlogo/Claim/Footer/QR, Offers-Logo/Brandtext/QR-Labels und Video-Footer-QR-Label sind jetzt als Modul-Einstellungen im CMS pflegbar
- Die Modul-Meta-Einstellungen sind als sichtbarer Block unterhalb der Media-/Slide-Liste editierbar; Angebote vererben Footer-Claim und Web/QR-URL automatisch an alle Slides
- Logo-Alt-Text-Felder wurden aus den Modul-Meta-Einstellungen entfernt; Player nutzen weiterhin feste Fallback-Alt-Texte
- Video-Display unterstützt Mediathek-Upload-URLs wie `/storage/...` ohne Legacy-`assets/`-Prefix
- Die Display-Mediathek und der schnelle Media-Picker akzeptieren SVG-Dateien als Bild-Uploads
- Änderungen an Name, Einstellungen, Items, Reihenfolge und Aktiv-Status laden die Modul-Vorschau neu
- Video/Footer/Media/Slide-Listen zeigen Inline-Mini-Previews je Item; Slide-Previews sind größer dargestellt
- Einzelmodul-Vorschau nutzt weiterhin den Player aus Phase 2
---
## Phase 6 Umbenennung Versionen → Module + Onboarding
**Ziel:** Die Admin-UI verwendet den fachlich korrekten Begriff „Module“. Alte URLs wurden während der Übergangsphase per 301 weitergeleitet und in Phase 7 entfernt.
### Stand 12.05.2026 ✅ abgeschlossen
Dateien:
- `routes/admin.php`
- `app/Livewire/Admin/Cms/DisplayVersionList.php`
- `resources/views/livewire/admin/cms/display-version-list.blade.php`
- `resources/views/livewire/admin/cms/display-version-editor.blade.php`
- `resources/views/livewire/admin/cms/display-dashboard.blade.php`
- `resources/views/components/layouts/app/sidebar.blade.php`
- `tests/Feature/DisplayVersionTest.php`
Umsetzung:
- Neue Routen: `admin/cms/display-modules` und `admin/cms/display-modules/{displayVersion}/edit`
- Neue Routennamen: `admin.cms.display-modules` und `admin.cms.display-module-edit`
- Alte `display-versions`-Routen waren während der Übergangsphase als 301-Redirects aktiv und wurden in Phase 7 entfernt
- Sidebar, Dashboard, Listen- und Editor-Texte verwenden „Module“
- Technische Modell-/Klassennamen bleiben bei `DisplayVersion`, da sie fachlich weiterhin die wiederverwendbaren Module abbilden
#### Tests
```text
tests/Feature/DisplayVersionTest.php ok
tests/Feature/DisplayVersionApiTest.php ok
tests/Feature/DisplayListTest.php ok
tests/Feature/DisplayPlaylistMigrationTest.php ok
```
Insgesamt 64 grüne Tests für Phasen 5/6 und die angrenzenden Display-Flows. Pint clean.
---
## Phase 7 Technisches Aufräumen & Optimierung
**Ziel:** Nach Stabilisierung des neuen Playlist-Flows wird die alte Pivot-Kompatibilität entfernt und der Modul-Editor weiter vereinheitlicht.
### Stand 13.05.2026 ✅ umgesetzt
Dateien:
- `app/Models/Display.php`
- `app/Models/DisplayVersion.php`
- `app/Livewire/Admin/Cms/DisplayList.php`
- `app/Console/Commands/MigrateLegacyDisplays.php`
- `app/Support/DisplayModuleSettings.php`
- `app/Services/DisplayPlaylistConfigBuilder.php`
- `app/Livewire/Admin/Cms/DisplayVersionEditor.php`
- `app/Livewire/Admin/Cms/DisplayVersionList.php`
- `routes/admin.php`
- `database/migrations/2026_05_13_103600_drop_display_display_version_table.php`
- `resources/views/livewire/admin/cms/display-list.blade.php`
- `resources/views/livewire/admin/cms/display-version-editor.blade.php`
- `resources/views/livewire/admin/cms/partials/version-editor-video.blade.php`
Umsetzung:
- Alte Pivot-Tabelle `display_display_version` wird per Migration entfernt
- Legacy-Relationen `Display::versions()` und `DisplayVersion::displays()` wurden entfernt
- Display-Bearbeitung, Draft-Veröffentlichung und Legacy-Migrations-Command schreiben ausschließlich in `display_playlists` und `display_playlist_items`
- Alte `display-versions`-Redirect-Routen wurden entfernt; die Admin-UI nutzt nur noch `display-modules`
- Modul-Settings-Defaults liegen zentral in `App\Support\DisplayModuleSettings` und werden von Editor, Listen-Erstellung und API-Config-Builder gemeinsam genutzt
- Admin-Iframes laden per `loading="lazy"` verzögert, um die parallelen Player-Vorschauen leichter zu halten
- Video-Display-Items zeigen im Editor sichtbar an, ob die Quelle aus der Mediathek oder aus einem Legacy-Dateinamen kommt
---
## Phase 8 Review 29.05.2026 (Befundaufnahme)
Geprüft wurden die drei Navigationspunkte:
- `admin/cms/display-media` → Volt `admin.cms.display-media-library`
- `admin/cms/display-modules``App\Livewire\Admin\Cms\DisplayVersionList` (+ Editor `DisplayVersionEditor`)
- `admin/cms/displays``App\Livewire\Admin\Cms\DisplayList`
**Vorgehen:** Code-Review der Komponenten/Views/Services/Player + Laravel-Logs + DB-Stand. Eine Browser-Sichtprüfung war in dieser Session nicht möglich (Browser-MCP nicht verfügbar). **Alle Befunde (#1#11) wurden inzwischen umgesetzt** (Details je Punkt unten).
**Aktueller Datenstand (DB):** 5 Displays (davon **0** Test-Displays), 6 Module, 7 Playlists (5 Live + 2 Entwürfe), 13 Medien.
**Tests:** `DisplayListTest`, `DisplayMediaTest`, `DisplayVersionTest`, `DisplayVersionApiTest`, `DisplayPlaylistMigrationTest`**95 passed** (276 Assertions). Keine CMS-Fehler in `storage/logs` (vorhandene Log-Fehler betreffen unrelated `immobilien-azizi`-Routen).
### 🔴 Fehler (sollten behoben werden)
1. **Mediathek-Suche hebelt alle Filter aus.****behoben 29.05.2026**
In `display-media-library.blade.php` (`$media`-Computed) und in `DisplayMediaPicker::resolveMediaItems()` wurde die Suche als `->where('filename','like',…)->orWhere('title','like',…)` ohne Gruppierung an die vorherigen `when()`-Filter gehängt. Durch SQL-Präzedenz (`AND` bindet stärker als `OR`) wurde die Query zu `… AND filename LIKE … OR title LIKE …`. Sobald gesucht wurde, wurden **Typ-, Quelle-, Sammlungs- und (beim Picker) der `active()`-Filter ignoriert**, sobald ein Treffer über `title` zustande kam.
*Behebung:* Neuer gekapselter Scope `DisplayMedia::scopeSearch()` (Closure um `filename`/`title`), den Library und Picker gemeinsam nutzen. Tests: `keeps preceding filters when combined with the search scope`, `search scope respects the active filter on the media picker` in `DisplayMediaTest.php`.
2. **Inhalt-Bearbeiten reaktiviert deaktivierte Items.****behoben 29.05.2026**
In `DisplayVersionEditor` übergab `loadItemContent()` nur das `content`-Array an `loadVideoContent()/loadFooterContent()/loadMediaContent()/loadSlideContent()`. Alle vier setzten `…IsActive = true` fest das tatsächliche `is_active` des Items wurde nie geladen. Wer ein zuvor per Auge-Icon deaktiviertes Item öffnete und „Aktualisieren" klickte, hat es über `getActiveFlag()` **ungewollt wieder aktiviert**.
*Behebung:* `loadItemContent()` reicht `(bool) $item->is_active` an die Loader durch, die es in die jeweilige `…IsActive`-Property schreiben. Test: `editing an inactive item keeps it inactive` in `DisplayVersionTest.php`.
### 🟡 Optimierungen / Aufräumen
3. **Toter Code im Video-Editor.****behoben 29.05.2026** `DisplayVersionEditor::loadAvailableVideos()`, Property `$availableVideos`, der `mount()`-Aufruf und der ungenutzte `Illuminate\Support\Facades\File`-Import wurden entfernt (im aktiven Editor-Blade ungenutzt; das einzige Blade mit `availableVideos` gehört zur Legacy-`CabinetDisplay`). Spart den Dateisystem-Scan von `public/_cabinet/assets` bei jedem Editor-Aufruf.
4. **Modul-Löschen ohne Schutz.****behoben 29.05.2026** `DisplayVersionList::deleteVersion()` ermittelt jetzt den `displays_count` (distinct Displays über Playlists) und **blockiert** das Löschen, solange das Modul in irgendeiner Bespielung (Live oder Entwurf) genutzt wird; es erscheint ein roter Flash-Hinweis mit Anzahl. Module ohne Nutzung lassen sich weiterhin löschen. Test: `cannot delete a display version that is used by a playlist` in `DisplayVersionTest.php`.
5. **Doppelte Such-Logik** zwischen Library und Picker (siehe #1) → ein gemeinsamer Query-Scope `DisplayMedia::scopeSearch(string $term)` verhindert künftige Divergenz. ✅ **erledigt 29.05.2026** (im Zuge von #1).
6. **Preview-Token bleibt nach Veröffentlichen bestehen.****behoben 29.05.2026**
`DisplayList::publishDraft()`/`discardDraft()` ließen `displays.preview_token` gesetzt, obwohl `/preview/{token}` danach 404 lieferte (kein Draft mehr); ein Rotations-Button fehlte ganz.
*Behebung:* Neue Model-Methoden `Display::rotatePreviewToken()` und `Display::clearPreviewToken()`. `publishDraft()` und `discardDraft()` setzen den Token jetzt zurück (alter Link wird ungültig). Im Entwurf gibt es einen „Link erneuern"-Button (`rotatePreviewToken()` mit `wire:confirm`), der bei offenem Entwurf zugleich die Iframe-Vorschau aktualisiert. Tests: `publishing a draft clears the preview token`, `can discard a draft playlist` (erweitert) und `can rotate the preview token of a draft` in `DisplayListTest.php`.
### 🟠 Konfiguration / Dev-Umgebung
7. **Live-Vorschau zeigt im lokalen Dev auf die Produktion.****behoben 29.05.2026**
`config/display.php` `player_url` defaultete auf `https://cabinet.b2in.eu/display`, und `DISPLAY_PLAYER_URL` war in `.env` nicht gesetzt. In `display-list.blade.php` nutzten „Vorschau", „Live-URL zum Kopieren" und „Display-Übersicht öffnen" diese Produktions-URL, während die Entwurfs-„Test-URL" über `url('/preview/…')` lokal lief. Lokal sah man Live also den Produktionsstand, Entwurf aber lokale Daten inkonsistent und irreführend.
*Behebung:* Player-Seite (`public/_cabinet/display/`) **und** die Display-API liegen auf der Portal-Domain, daher ist der Player lokal unter `https://portal.b2in.test/_cabinet/display` erreichbar (der Player ermittelt `BASE_URL` aus `window.location.origin` und ruft die API auf derselben Domain auf).
- `.env`: `DISPLAY_PLAYER_URL=https://portal.b2in.test/_cabinet/display` gesetzt (Config-Cache geleert).
- `.env.example`: dokumentiert (Live-Wert als Kommentar).
- `phpunit.xml`: `DISPLAY_PLAYER_URL=https://cabinet.b2in.eu/display` gepinnt, damit die bestehenden URL-Tests (`DisplayListTest`, `DisplayVersionApiTest`) deterministisch gegen den Produktionswert prüfen, unabhängig von der lokalen `.env`.
- Der Produktions-Fallback in `config/display.php` (und damit das Live-Verhalten) bleibt unverändert.
### 🟢 Erweiterungen / Verbesserungen (UX)
8. **Test-Display existiert nicht.****behoben 29.05.2026** Neuer idempotenter `Database\Seeders\TestDisplaySeeder` legt genau **ein** Test-Display an (überspringt, falls schon vorhanden) und ist im `DatabaseSeeder` registriert. In der Dev-DB wurde das Test-Display angelegt. Tests: `seeds exactly one test display`, `is idempotent and does not create a second test display` in `TestDisplaySeederTest.php`.
9. **Externe Bilder ohne Thumbnail.****behoben 29.05.2026** Grid, Listenansicht und Detail-Sidebar (`display-media-library.blade.php`) rendern externe Bilder jetzt direkt via `<img src="{{ external_url }}">` (gemeinsame `$thumbSrc`-Logik: Upload-Thumbnail → sonst `external_url`). Nicht-Bild-Externe behalten das Link-Icon. Test: `renders external image media with an inline thumbnail` in `DisplayMediaTest.php`.
10. **Mediathek-Pagination ohne `resetPage`.****behoben 29.05.2026** Die Volt-Komponente nutzt jetzt `WithPagination` und Update-Hooks (`updatedSearch/updatedFilterType/updatedFilterSource/updatedFilterCollection`), die bei jeder Filter-/Suchänderung `resetPage()` aufrufen. Test: `resets pagination to page one when a filter changes` in `DisplayMediaTest.php`.
11. **Modul-Hinzufügen nur einzeln.****behoben 29.05.2026** Der Bespielungs-Dialog (`display-list.blade.php`) nutzt jetzt einen durchsuchbaren Flux-Listbox-Multi-Select (`versionsToAdd`) plus „Hinzufügen"-Button (`addSelectedVersions()`), der alle ausgewählten Module auf einmal übernimmt (Duplikate werden übersprungen). Die programmatische `addVersion()`-Methode bleibt für Einzel-Adds/Tests erhalten. Test: `can add multiple modules to a playlist at once` in `DisplayListTest.php`.
### Empfohlene Reihenfolge
1. #1 + #2 (echte Fehler, klein, mit Tests) → 2. #7 (Dev-Konfiguration, blockiert lokales Testen) → 3. #3/#4/#5/#6 (Aufräumen/Robustheit) → 4. #8#11 (UX-Erweiterungen). Jeder Punkt wird gemäß Projekt-Regeln mit Test abgesichert und mit Pint formatiert.
**Stand 29.05.2026:** Alle Befunde (#1#11) sind umgesetzt, getestet und mit Pint formatiert.