# 2026-10-26

## Signage-Steuerung umgesetzt (AP-12)

**Was:** Vorschlag `docs/SYMFONY-INTEGRATION-SIGNAGE-DPOHL.md` im test_projekt umgesetzt:
Verwaltung `/signage` (Geräte, Videos mit Chunk-Upload, Playlists, Kalender und Vorschau,
Veröffentlichen, Sofortwiedergabe, Protokoll) und die Geräte-API `/signage/api/v1/player/*`
für `signage01`. Das Manifest hat das Format des Players.

**Warum:** Der Player auf dem Mini-PC soll seine Inhalte vom Server holen. Danach soll das
Ganze auf dem Server laufen.

**Festlegungen:** MariaDB `LONGTEXT` statt `jsonb`. ffprobe läuft synchron. Alle nicht leeren
Playlists kommen in jedes Manifest. Ein unveränderter Entwurf erzeugt keine neue Version.
Geräte-Firewall stateless mit Bearer-Token, in der DB nur sha256-Hashes. IP-Allowlist über
`REMOTE_ADDR`.

**Gefundene Fehler während der Prüfung:**
- Sommerzeit: PHP wählt bei der doppelten Herbststunde das zweite Vorkommen, der Player
  (`fold=0`) das erste. `SignageSchedule::toUtc` hat deshalb eine eigene Auflösung.
- Zeitzone: `DATETIME` wird ohne Zone gespeichert. Ein UTC-Zeitstempel aus `'@'.time()`
  verschob Sofortaufträge um eine Stunde, sie galten sofort als abgelaufen. Jetzt gilt lokale Zeit.
- DQL: Die Medienabfrage für Geräte selektierte eine gejointe Entity ohne Root (500). Jetzt `EXISTS`.
- `stream()` kollidierte mit `AbstractController::stream`, umbenannt in `preview()`.

**Prüfung:** `lint:container`, `lint:twig`, `lint:yaml`, PHPUnit 7/7. 160 Zeitplan-Vergleiche
PHP gegen `schedule.py` ohne Abweichung, auch in Wochen mit Zeitumstellung. 66/66
API-/Verwaltungsprüfungen über den Kernel mit Wegwerf-SQLite und ffprobe-Stub. Das erzeugte
Manifest wird von `Release.model_validate_json` angenommen. Die Migration lief gegen die
Datenbank nur als `--dry-run` (24 Befehle), angewandt wurde sie nicht. Die temporären Skripte
sind gelöscht.

**Dateien:**
- Neu:
  - `src/Entity/Signage/*`, `src/Repository/Signage/*`, `src/Service/Signage/*`
  - `src/Security/Signage/*`, `src/EventListener/Signage/SignagePlayerApiSubscriber.php`
  - `src/Controller/Signage/*`, `templates/signage/**`
  - `migrations/Version20261026170000.php`, `tests/Service/Signage/SignageScheduleTest.php`
- Geändert:
  - `config/packages/security.yaml`, `config/services.yaml`, `src/Form/UserType.php`
  - `templates/base.html.twig`, `.mage.yml` (Link `shared/var/signage`), `.env.example`
- Doku: `docs/SYMFONY-INTEGRATION-SIGNAGE-DPOHL.md` Abschnitt 9 (Umsetzung, API-Verhalten,
  Inbetriebnahme), `docs/ARBEITSPAKETE.md`, `docs/TODO.md`

**Offen:** Server-Inbetriebnahme (ffmpeg, Deploy, Migration, Rechte, IP-Prüfung) und die
Synchronisation im Player. Kein Commit, kein Deploy.

## Signage: Planungskalender mit Drag & Drop und Text-Videos

**Was:**
- Geräteseite → Zeitplan → „Planen“: Playlists oder einzelne Videos in den Kalender ziehen,
  Termine verschieben, das Ende über die Unterkante ziehen. Bei Serien fragt die Seite
  „Ganze Serie“ oder „Nur dieser Tag“.
- Neuer Bereich „Text-Videos“: Vorlage aus Hintergrundvideo und Layout, Text eingeben. Der
  Server rendert ein neues MP4 in die Mediathek, mit Live-Vorschau als Standbild.

**Warum:** Wunsch des Nutzers: Zeitplan per Drag & Drop, Begrüßungstexte wie „Wir begrüßen die
Schulklasse 3b“. Entscheidungen per Rückfrage: Der Server rendert, der Player bleibt unverändert.
Bei Serien wird nachgefragt. Erst kam der Kalender, dann die Vorlagen.

**Festlegungen:**
- Jede Kalenderänderung prüft den Entwurf vorher und nachher. Ein neuer Konflikt führt zu einem
  Rollback und `409`.
- Ein Einzelvideo wird zur Playlist `video-<id>`.
- Gerendert wird in `kernel.terminate` nach der Antwort, mit atomarem Claim des Auftrags.
  Rückfall ist das Command `signage:text-video:render` (`--dry-run`, `--limit`).
- Der Text geht per `textfile` mit `expansion=none` an ffmpeg, damit Sonderzeichen keine
  Filter-Syntax auslösen.

**Gefundene Fehler während der Prüfung:**
- Die Tagesdifferenz beim Verschieben einer Serie mischte Zeitzonen. Jetzt wird beides in UTC
  geparst.
- PowerShell `Set-Content -Encoding utf8` setzte ein BOM in `_layout.html.twig`. Entfernt.
- Betrachter-Formular: `disabled`-Felder fehlen in `FormData`, deshalb jetzt `readonly`.
- Lange Titel in kurzen Terminen wurden in der zweiten Zeile abgeschnitten. Jetzt einzeilig mit
  „…“, der volle Name steht im Tooltip.
- Lokaler Test: Der PHP-Server (`php -S`) übernimmt Umgebungsvariablen nur mit
  `-d variables_order=EGPCS` in `$_ENV`. Ohne das hätte Dotenv die DB aus `.env.local` genommen.
  Das ist vor dem ersten App-Aufruf aufgefallen.

**Prüfung:**
- `lint:container`, `lint:twig`, `lint:yaml`, `doctrine:schema:validate --skip-sync`.
- 53/53 Kernel-Prüfungen auf Wegwerf-SQLite mit echtem ffmpeg:
  - Kalender: Feed, Drop, Konflikt, Serie (Einzeltag und ganze Serie), Mitternacht, CSRF,
    Rechte.
  - Text-Videos: Vorschau, Rendern, Fehlerfall, Command.
- Browsertest auf Wegwerf-SQLite: Playlist per Drag & Drop angelegt, Serientermin „nur dieser
  Tag“ verschoben (Dialog erscheint), Ende per Unterkante auf 16:00 gezogen.
- Gerenderte Frames visuell geprüft.
- Gegen die Server-DB lief nichts. Temporäre Skripte, die SQLite-DB, `var/signage` und der
  ffmpeg-Build sind gelöscht.

**Dateien:**
- Neu:
  - `src/Controller/Signage/SignageCalendarController.php`, `SignageTextController.php`
  - `src/Entity/Signage/SignageTextTemplate.php`, `SignageTextRender.php`
  - `src/Repository/Signage/SignageTextRenderRepository.php`
  - `src/Service/Signage/SignageTextRenderer.php`
  - `src/EventListener/Signage/SignageTextRenderListener.php`
  - `src/Command/SignageTextRenderCommand.php`
  - `templates/signage/text/*`
  - `public/js/admin/signage-calendar.js`, `signage-text.js`
  - `migrations/Version20261026220000.php`
- Geändert:
  - `SignageScheduleEvent` (`occursOn`, `isRecurring`, `shiftDays`)
  - `SignageDeviceController`, `SignageMediaController`, `SignageMediaRepository`
    (Vorlage blockiert Löschen)
  - `templates/signage/device/show.html.twig`, `_layout.html.twig`
  - `public/css/admin/signage.css`, `config/services.yaml`, `.env.example`
- Doku: Spezifikation Abschnitt 9, `docs/CRONJOBS.md`, `docs/TODO.md`, `docs/ARBEITSPAKETE.md`

**Offen:** Server-Schritte (ffmpeg und Schrift, Migration, gegebenenfalls Cron). Kein Commit,
kein Deploy.

## Signage: Hoch- und Querformat

**Was:**
- Ausrichtung je Ausgabe in den Geräteeinstellungen. Sie wird im Kopf der Geräteseite angezeigt
  und steht nicht im Manifest.
- `SignageMediaProbe` wertet die Drehung aus (Display-Matrix bzw. Tag `rotate`) und vertauscht
  bei 90°/270° Breite und Höhe.
- Mediathek und Playlists zeigen „hoch“/„quer“. Der Kalender markiert unpassende Playlists und
  Termine und warnt beim Hineinziehen.
- Text-Vorlagen haben ein Format (wie Hintergrund, Hochformat 1080×1920, Querformat 1920×1080)
  und eine Einpassung (zuschneiden oder unscharfer Rand). Die Vorschau zeigt das Hochformat
  hochkant.

**Warum:** Die Bildschirme an `signage01` hängen hochkant, Querformat soll später auch gehen.

**Gefundener Fehler:** Handyvideos mit Drehungs-Angabe wurden als Querformat gespeichert. Beim
Text-Video rechnete die Schriftgröße mit den vertauschten Maßen, ffmpeg drehte das Bild aber
richtig, deshalb wäre der Text zu groß geworden.

**Festlegungen:**
- Die Drehung des Bildschirms bleibt Sache des Geräts. Ein Feld im Manifest ginge nur zusammen
  mit dem Player (`extra="forbid"`).
- Spalten `video_format`/`video_fit` stehen direkt in `Version20261026220000`. Die Migration
  war noch nirgends angewandt.

**Prüfung:**
- PHP-Lint, `lint:twig`, `lint:container`; das Schema-Mapping hat keine Signage-Fehler.
- `node --check` für beide Skripte.
- 35/35 Kernel-Prüfungen auf Wegwerf-SQLite mit echtem ffmpeg:
  - gedrehtes Handyvideo wird als 1080×1920 erkannt;
  - Vorschauen in allen Format-/Einpassungs-Kombinationen mit richtigen Maßen;
  - Renderings 1080×1920;
  - Geräteeinstellung gespeichert, Manifest ohne `orientation`;
  - Kalender-Feed, Formular, Prüfung ungültiger Werte, Mediathek.
- Frames visuell geprüft.
- Die Markierungen im Kalender sind nicht im Browser getestet, nur ihre Daten serverseitig.
- Temporäre Dateien gelöscht.

**Hinweis:** Um 22:04 wurde `97fe2af` („signage first“) mit einem Zwischenstand committet und
gepusht. Entity und Migration enthalten schon `format`/`fit`, Controller und Renderer noch nicht.
Erst mit den danach geänderten Dateien ist der Stand stimmig.

**Nachtrag – Drehung im Manifest:** Der Player kann jetzt `outputs[].rotation`
(`normal|left|right|inverted`, per `xrandr`).
- Aus der Ausrichtung ist eine Drehung je Ausgabe geworden. Hochformat gilt für `left`/`right`.
- Das Manifest enthält das Feld nur, wenn nicht `normal` gedreht wird. „Fehlt“ heißt beim Player
  `normal`, so bleiben ältere Player ohne Drehung lauffähig.
- Im getrennten Modus verlangt das Veröffentlichen bei Drehung eigene Anschlüsse je Ausgabe.
- Ein alter Schlüssel `orientation` in der Geräte-Konfiguration wird ignoriert.
- Achtung: Die lokale Player-Kopie `h:\dev\dp signage pc` kennt `rotation` in `domain.py` noch
  nicht. Bekommt ein solcher Player eine gedrehte Version, lehnt er sie ab (`extra="forbid"`).
- Prüfung: 17/17 Kernel-Prüfungen (Manifest wie im Beispiel des Players, Einstellungen, getrennter
  Modus), PHPUnit 7/7, Lint. Geändert: `SignageDevice`, `SignageManifestBuilder`,
  `SignageDeviceController`, `device/show.html.twig`, Spezifikation Abschnitt 3 und 9, TODO.

**Dateien:**
- `src/Service/Signage/SignageMediaProbe.php`, `SignageTextRenderer.php`
- `src/Entity/Signage/SignageDevice.php`, `SignageMedia.php`, `SignagePlaylist.php`,
  `SignageTextTemplate.php`
- `src/Controller/Signage/SignageDeviceController.php`, `SignageCalendarController.php`,
  `SignageTextController.php`
- `migrations/Version20261026220000.php`
- `templates/signage/_macros.html.twig`, `_layout.html.twig`, `device/show.html.twig`,
  `text/form.html.twig`, `text/show.html.twig`
- `public/js/admin/signage-calendar.js`, `signage-text.js`, `public/css/admin/signage.css`
- Doku: Spezifikation Abschnitt 9, TODO, ARBEITSPAKETE
