presseportale/dev/bildrechte/bildkennzeichnung-entwicklungsplan-2026-09-21.md
Kevin Adametz 8e3554bf91 dev: Doku aufgeräumt, neue Roadmap als einzige Liste offener Punkte
- dev/roadmap.md neu: alle offenen Punkte aus Plänen, Audits und Protokollen,
  gegen Code und Git abgeglichen (Stand v1.7.0)
- Abgeschlossenes ins Archiv: 2026-09-01 → archive/2026-09-nutzergewinnung,
  2026-09-11-audit → archive/2026-09-audit, lagacy-rechnungen →
  archive/legacy-rechnungen, Deploy-Protokoll v1.1.0, Umami-Snippets,
  Go-Live-Roadmap vom 20.07.
- Aktiv bleiben eigene Ordner bildrechte/ (Plan Fassung 2, Decision-Update)
  und changelog/ (Decision-Update Changelog)
- dev/README.md als Ordnerkarte neu, Verweise und zwei Code-Kommentare auf
  die neuen Pfade

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:25:09 +00:00

332 lines
52 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.

# Entwicklungsplan: Bildnachweis, KI-Kennzeichnung, Rechtebestätigung, Beweisarchiv
**Stand:** 22.09.2026, Fassung 2 (Entscheidungen getroffen; Blöcke A bis H und K umgesetzt und committet, Deploy offen; Blöcke I und J stehen aus)
**Grundlage:** [Umsetzungsvorschlag 15.09.](../archive/2026-09-audit/bildkennzeichnung-2026-09-15.md), [Feedback-Audit 15.09.](../archive/2026-09-audit/bildkennzeichnung-feedback-audit-2026-09-15.md), [Decision-Update 22.09.](Decision-Update_Bildrechte_Beweisarchiv_2026-09-22.md) und die Antworten des Betreibers vom 21.09. auf die Entscheidungsfragen der Fassung 1. Bei Widerspruch gilt das Decision-Update (im Folgenden „DU"). Code-Abgleich an HEAD `e620ef3` auf `technik/betriebsfixes-2026-09`.
**Zweck:** Alle Punkte aus den vier Quellen, die am heutigen Code umsetzbar sind, in einer abarbeitbaren Reihenfolge mit Fix, Test und Aufwand. Gilt zuerst für das Titelbild einer Pressemitteilung; Newsroom-Bilder (Roadmap 1b.3) nutzen denselben Unterbau.
Die Fassung 1 vom 21.09. enthielt eine Fragenliste und vier Textanlagen für den Anwalt. Beides ersetzt das Decision-Update (Abschnitte 3 und 7) und ist hier gestrichen.
---
## 1. Fazit
Die Richtung des Vorschlags trägt, die Korrekturen des Feedback-Audits sind übernommen, und das Decision-Update hat die offenen Geschäfts- und Rechtsfragen mit Defaults belegt. Der Umfang ist damit klar und größer als in Fassung 1:
- **Formular und Beweisstand für Bilder** wie geplant: drei Blöcke, zwei Pflichtangaben plus Bestätigung, Original privat, Metadaten gelesen, Zustimmung mit rekonstruierbarem Wortlaut. Sperre nur bei geprüfter Signatur (Phase 2).
- **Beweisarchiv** als eigener Baustein, der über Bilder hinausgeht: verschlüsselter, zugriffsprotokollierter Datensatz zu jeder entfernten Veröffentlichung (Bild, Depublikation, DSGVO-Änderung, Kennzeichnungskorrektur), drei Jahre zum Jahresende, `legal_hold`, Löschung nach Restore. Nie veröffentlichte Entwürfe werden sofort gelöscht.
- **Vertragliche Einbeziehung**: Bestätigungstext an die AGB-Fassung gebunden; zusätzlich ein Zustimmungsgate je Dokumentversion vor jeder Einreichung.
- **Öffentliche Sprache**: „automatisierte Prüfung" statt „geprüft" und „qualitätsgeprüft"; kein Pro-Prüfversprechen.
- **Reihenfolge**: Deploy 1b (Formular, Archiv, Protokoll) geht **vor** der Anwaltsantwort live. AGB v0.7 und DSE v0.4 folgen danach.
### Was der Code-Abgleich ergeben hat
| Befund | Konsequenz |
|---|---|
| Dev-Kopie: 14.601 Bilder. 14.554 Legacy-Importe, davon 3.786 an veröffentlichten PMs und **10.768 an inaktiven** (10.463 `rejected` = im Altsystem „offline genommen", 301 archiviert, 4 Entwürfe). 47 Bilder aus dem neuen System (32 an Entwürfen, 15 veröffentlicht), 45 davon mit Rechtebestätigung, 17 als KI gekennzeichnet | E-B11: die 10.768 werden gelöscht (E.5). Der Bestand betrifft danach nur noch 3.786 Bilder an veröffentlichten PMs. |
| Weder `exiftool` noch `c2patool` im Container. Imagick 6.9 liest IPTC-, XMP- und EXIF-Profile und kann WebP; PHP-Erweiterung `exif` vorhanden | Phase 1 ohne neue Binärdatei. `c2patool` erst Phase 2, dann in Sail-Image und auf dem Ploi-Server. |
| `ImageService::storePressReleaseImage()` löscht das Original nach der Variantenerzeugung und fällt bei fehlender Cover-Variante auf die Quelldatei zurück | Uploader sichert das Original vorher; der Rückfall wird zum Fehler. GD schreibt keine Metadaten in Varianten, die Bereinigung ist heute schon gegeben (Test friert das ein). |
| Der heutige Bestätigungstext („Rechte bestätigt" plus Beschreibungssatz im Bildmanager, Zeile 710) hat keine Kennung. 45 Bestätigungen tragen nur `rights_confirmed_at` | DU S3: dieser Wortlaut wird rückwirkend als `img-confirm-v1` archiviert, die 45 Zeilen bekommen einen Verweis darauf. Neue Kennungen ab `img-confirm-v2`. |
| Bildmanager kann bestehende Bilder nur löschen, nicht bearbeiten. `assertAiDeclarationComplete()` blockt das Einreichen jeder PM mit Bild ohne `ai_declaration` | Nach E.5 gibt es keine Legacy-Bilder an wiedereinreichbaren PMs mehr; kein Bestandsschutz nötig. |
| Legacy-API: `POST /service/pressreleases/{id}/images` setzt `original` fest, keine Statusprüfung. connektar liefert keine Bilder | E-B4: Endpunkt wird abgeschaltet (C.2). |
| Öffentliche Wortwahl: „qualitätsgeprüft" in `lang/de/web.php` (Zeilen 189, 231, 233, 249, 255), Footer-Tagline, `presseecho.blade.php:43`; „Geprüfte Mitteilungen" in `lang/de/static.php:533`; „geprüft" in `richtlinien.blade.php:36`; „redaktionelle Qualität" in `agb.blade.php:139, 206`. Die „priorisierte redaktionelle Prüfung" ist im `PlanSeeder` bereits gestrichen | DU S10 (D.6). Die AGB-Stellen gehen mit v0.7 an den Anwalt. |
| Depublikation, Archivierung, Statuswechsel und Löschung laufen über `PressReleaseService` (`depublish`, `archive`, `changeStatusFromAdmin`, `deleteFromAdmin`); DSGVO-Änderungen über `AnonymizationService` und `CorrectionService` | Das sind die Anbindungspunkte des Beweisarchivs (F.4). |
| OpenGraph-Bild ist heute die Detail-Variante (`release-detail.blade.php:168`); Teaser nutzen `card`/`thumb` ohne Nachweis; RSS-Feeds ohne Bilder; JSON-LD `image` als URL-Liste | DU E6: Feeds bleiben ohne Bilder. DU E7: OG-Variante mit Nachweisbalken und Teaser mit Nachweis (H). |
| Kundenoberfläche durchgehend „Sie"; AGB sagen „Anbieter" | Alle neuen Texte in Sie-Form mit „Anbieter". |
| Im Decision-Update referenziert, aber nicht im Repository: `Umsetzung_Bildnachweis_KI-Kennzeichnung.md`, `handover_loesch-anfragen.md` | DU S19 (Löschanfragen-Handover) kann erst bearbeitet werden, wenn das Dokument im Ordner liegt. |
---
## 2. Entscheidungen
Alle Fragen der Fassung 1 sind beantwortet (Betreiber, 21.09.2026). Wo das Decision-Update vom 22.09. abweicht, gilt es.
| Nr. | Frage | Entscheidung | Quelle |
|---|---|---|---|
| E-B1 | Phase 1 ohne automatische Sperre, nur Vorbelegung mit protokollierter Abweichung; Sperre in Phase 2 nach C2PA-Test | ja | Betreiber; DU E9 |
| E-B2 | Aufbewahrungsfrist | drei Jahre nach dem 31.12. des Entfernungsjahres, `legal_hold` bei Streit und Regress; nie veröffentlichte Uploads sofort löschen. Anwalt bestätigt (DU A4) | Betreiber; DU E3, E4 |
| E-B3 | Bestand ohne Bildnachweis | **revidiert durch DU 3.4:** ohne Bestätigungsprotokoll keine Verantwortlich-Zeile. Bestand zeigt „Foto: [Nachweis]", ohne Nachweis „Quelle: [Firma]" | DU 3.4 |
| E-B4 | Legacy-API ohne die neuen Pflichtfelder | **Legacy-Bildupload abschalten.** Wird nicht genutzt; wer Bilder liefern will, nimmt v1 | Betreiber |
| E-B5 | `algorithmicallyEnhanced` | kein KI-Hinweis, nur interner Vermerk | Betreiber |
| E-B6 | Upload-Grenze | Formular und API 16 MiB, Pixelgrenze 8.000 × 8.000, vor der Prüfung | Betreiber |
| E-B7 | Widerspruch gegen Vorbelegung oder Sperre | Kontaktweg für den Kunden, protokollierter Admin-Korrekturweg mit Zwischenzustand „vorläufig zurückgehalten" | Betreiber; DU S17 |
| E-B8 | Anrede | Sie | Betreiber |
| E-B9 | Admin-Upload | **revidiert durch DU E10:** Veröffentlichung erst nach Kundenbestätigung per Magic-Link „Bild bestätigen", gleiches Protokoll | DU E10 |
| E-B10 | Hartes Löschen | Original und Bestätigung überleben das Löschen bis zum Fristende (im Beweisarchiv) | Betreiber; DU 4 |
| E-B11 | Wiedereinreichen alter PMs mit Legacy-Bild | **kein Bestandsschutz.** Legacy-Bilder an nicht veröffentlichten PMs (`rejected`, `archived`, `draft`) werden gelöscht; eine erneute Einreichung läuft ohne Bild | Betreiber |
| E-B12 | Bestätigungstabelle und Uploader polymorph für Newsroom-Bilder | ja | Betreiber |
| E-B13 | Overlay-Icon | eigenes „i" im Kreis, kein „cr"-Logo | Betreiber |
Aus dem Decision-Update zusätzlich übernommen (dort E1 bis E14): Kontoinhaber ist Vertragspartner, Firmenprofil nur Zuordnung (E1); Original immer speichern plus veröffentlichte Fassung (E2); „veröffentlicht" = hatte irgendwann eine öffentliche URL, auch kurz (E5); RSS ohne Bilder (E6); OG-Variante mit Nachweisbalken, Teaser mit Nachweis (E7); kein Erstattungsausschluss bei Bildentfernung (E8); „automatisierte Prüfung", kein Pro-Prüfversprechen (E11); Zustimmungsgate je Dokumentversion (E12); UrhDaG nur interner Vermerk (E13); Deploy 1b vor der Anwaltsantwort (E14).
### Offene Punkte (entschieden 22.09.2026)
| Nr. | Punkt | Entscheidung |
|---|---|---|
| O1 | DU 4.1: „Einreichung im Freigabelauf abgelehnt, nie veröffentlicht: Mediendateien sofort löschen." Eine abgelehnte PM kann der Kunde korrigieren und erneut einreichen; das Bild wäre dann weg. | **Entschieden:** Medien erst löschen, wenn die abgelehnte PM durch den bestehenden Entwurfs-Purge fällt oder gelöscht wird, nicht im Moment der Ablehnung. |
| O2 | Zustimmungsgate für API-Nutzer (DU S12): je Request, je Token oder je Kontoinhaber? | **Entschieden:** einmal je Kontoinhaber und Dokumentversion über `POST /api/v1/legal/acceptances`; ohne gültige Annahme antwortet `submit` mit 428 und den geforderten Versionen. |
| O3 | Die 301 Legacy-Bilder an archivierten PMs: darunter können depublizierte Meldungen sein, für die das Beweisarchiv künftig drei Jahre gälte. | **Entschieden:** mit E-B11 löschen; der Altbestand hat kein Bestätigungsprotokoll, das ein Archiv tragen würde. |
| O4 | Schrift für den eingebrannten Nachweisbalken (H.1) muss als TTF im Repository liegen. | **Entschieden:** Inter liegt bereits unter `resources/fonts/` (TTF, 18pt-Schnitte) mit `OFL.txt` (SIL Open Font License 1.1). H.1 nutzt `Inter_18pt-SemiBold.ttf`, kein neuer Font. **Achtung:** der Ordner ist heute untracked (nicht ignoriert, nur nie eingecheckt); mit H.1 werden `Inter_18pt-SemiBold.ttf` und `OFL.txt` committet, sonst fehlt die Schrift auf Live. |
| O5 | Pro-Tarif nach Streichung des Prüfversprechens (DU 11.5) | Offen, wird geklärt, wenn D.6 die Preisseite anfasst. Betreiber. |
---
## 3. Abgleich mit dem Decision-Update (Backlog S1 bis S24)
| DU | Inhalt | Im Plan | Bemerkung |
|---|---|---|---|
| S1 | Originalarchiv und Snapshot der veröffentlichten Fassung | A.6, E.2 | Snapshot beim Veröffentlichen, nicht erst beim Entfernen |
| S2 | Bestätigungsprotokoll | A.2 | polymorph (E-B12) |
| S3 | Tabelle unveränderlicher Bestätigungstexte, heutige Kennung | A.3 | heutiger Wortlaut = `img-confirm-v1` |
| S4 | API v1 Pflichtfelder, 422 | C.1 | |
| S5 | Legacy-Default entfernen, Altbestand `legacy` | C.2, A.1 | Endpunkt wird abgeschaltet (E-B4); `legacy` = `rules_version IS NULL` |
| S6 | Kundenangabe und Portalkennzeichnung getrennt | A.1 | `ai_declaration` bleibt die Portalkennzeichnung (alle Leser), dazu `ai_declaration_user` und `ai_declaration_source` |
| S7 | Admin-Upload mit Kundenbestätigung per Magic-Link | B.4 | |
| S8 | `published_ever` an PM und Bild | E.1 | |
| S9 | Sofortlöschung nie veröffentlichter Uploads | E.3 | mit O1 |
| S10 | Wortwahl „automatisierte Prüfung" | D.6 | Fundstellen in Abschnitt 1 |
| S11 | Bestätigungstext mit aufgelöster AGB-Fassung rendern | A.3, B.1 | |
| S12 | `legal_document_acceptances`, Zustimmungsgate | G | mit O2 |
| S13 | `archive_entries`, `archive_access_log`, Löschjob, `legal_hold`, Post-Restore, Tombstone-Export | F.1 bis F.3 | |
| S14 | Archivierung bei Depublikation, DSGVO-Änderung, Korrektur | F.4 | |
| S15 | OG-Variante, Teaser mit Nachweis | H.1, H.2 | |
| S16 | Beschnitt-Steuerung | H.3 | |
| S17 | Korrekturweg mit Zwischenzustand und Archiveintrag | E.6 | |
| S18 | Zugriffsprotokoll Originalarchiv | E.4 | |
| S19 | Löschanfragen-Handover ergänzen | – | Dokument nicht im Repository |
| S20 | C2PA-Verifikation | I.1 bis I.4 | |
| S21 | Metadaten in Varianten schreiben | I.5 | Imagick statt exiftool, gleiche Wirkung |
| S22 | Re-Sync-Sperre für Bilder | I.6 | |
| S23 | Bildklausel im Syndikationsvertrag | – | Betreiber |
| S24 | KI-Vorab-Check für Bilder (DU Abschnitt 12, E15) | K.1 bis K.6 | Sichtung auf offensichtliche Probleme, keine Rechteprüfung; Stufe 1 nur Stock-Wasserzeichen, Stufe 2 alle Kategorien; direkt nach Deploy 1b |
### Nicht aufgenommen
- Externe Bild-URLs und Sammelupload `images[]` in der API (Feedback F04).
- Umbenennung `copyright` in `credit`; vierter Enum-Wert `not_declared` (`NULL` leistet dasselbe, alle Leser prüfen darauf).
- Inhaltliche Rechteprüfung von Bildern; Nachprüfung des veröffentlichten Bestands. (Die eng zugeschnittene Sichtung auf offensichtliche Probleme aus DU Abschnitt 12 ist seit dem Update vom 22.09.2026 als Block K aufgenommen; sie prüft keine Rechte und wird öffentlich nicht als Bildprüfung beschrieben.)
- Bilder in RSS-Feeds (DU E6).
- Reduziertes Original bei Bildern ohne C2PA (DU 8: nur als Reaktion, falls der Anwalt die Datenminimierung anders sieht).
---
## 4. Aufwandsskala und Arbeitsregeln
| Kürzel | Bedeutung |
|---|---|
| S | eine Stunde bis ein halber Tag |
| M | ein bis drei Tage |
| L | länger oder abhängig von einer Entscheidung |
Wie im Audit-Entwicklungsplan: erst der Test, der am heutigen Code rot ist, dann der Fix, dann die betroffene Testdatei, am Blockende die gesamte Suite, PHPStan und Pint. Vor Testläufen `php artisan config:clear` (Config-Cache-Falle).
Der Flag `IMAGE_RIGHTS_V2` bleibt als Deploy-Sicherung: Deploy 1 geht mit `false` live, wird auf Live geprüft und dann eingeschaltet. Er wartet nicht auf den Anwalt (DU E14).
---
## Block A — Datenmodell, Texte, Prüfung, Uploader
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| A.1 | Migration `press_release_images`, alles nullable: `ai_declaration_user` (Angabe des Kunden), `ai_declaration_source` (Enum: `customer`, `iptc_prefill`, `c2pa_verified`, `admin_correction`), `ai_declaration_locked` (bool, default false), `credit_source` (`customer`, `metadata`, `api`), `provenance_status` (Enum A.4), `provenance` (json), `provenance_checked_at`, `provenance_tool_version`, `original_disk`, `original_path`, `original_sha256` (64), `published_snapshot_path`, `published_snapshot_sha256`, `published_ever_at`, `customer_confirmation_requested_at` (B.4), `rules_version` (string; `NULL` = Bestand, `2026-10` = Formular v2), `focus_x`/`focus_y` (tinyint, H.3), `no_crop` (bool, H.3). `ai_declaration` bleibt die **Portalkennzeichnung**, weil alle Leser (Artikelseite, Kunden- und Adminansicht, PDF, Snapshot in `press_release_ai_declarations`) daran hängen. `license_url` auf 2048 Zeichen. Bestand unangetastet. Test: vorhandene Werte unverändert. | S | keine |
| A.2 | Tabelle `image_rights_confirmations`, insert-only (Observer wirft bei Update oder Delete, Ausnahme E.3): `subject_type`/`subject_id` (polymorph, Phase 1 `PressReleaseImage`), `press_release_id`, `company_id` (Zuordnung), `user_id` (Kontoinhaber = Vertragspartner, DU E1), `personal_access_token_id`, `channel` (`form`, `api`, `magic_link`), `legal_text_id` (A.3), `text_snapshot` (gerenderter Wortlaut mit aufgelöster AGB-Fassung und Link), `agb_version`, `locale`, `file_sha256`, `declaration_snapshot` (json: `copyright`, `license_url`, `source_url`, `ai_declaration_user`, `ai_declaration`, `ai_declaration_source`, `locked`, `provenance_status`, `provenance`), `ip_hmac` (HMAC-SHA256 mit eigenem Schlüssel `CONFIRMATION_IP_KEY` und Versionskennung), `user_agent` (255), `confirmed_at`. Fremdschlüssel auf Bild und PM mit `nullOnDelete`, Snapshot-Spalten tragen die Werte weiter (E-B10). Test: hartes Löschen des Bildes lässt die Bestätigung stehen. | S | A.1 |
| A.3 | Textarchiv `legal_texts` (insert-only): `key` (z. B. `img-confirm-v2`), `type` (`image_rights_confirmation`), `locale`, `title`, `body` (mit Platzhaltern `{agb_version}`, `{agb_date}`, `{agb_url}`, `{portals}`, `{sections}`, `{indemnity_sections}`), `bound_agb_version`, `body_sha256`, `effective_from`, `note`. Seeder: `img-confirm-v1` = heutiger Wortlaut aus dem Bildmanager (Zeile 710), gebunden an AGB 0.6; `img-confirm-v2` = Langtext und Kurzform aus DU 3.3 mit Verweis „§ 8 und § 9" / „§ 8 Abs. 2", gebunden an AGB 0.6; `img-confirm-v3` = dieselben Texte mit „§ 8, § 8a und § 9" / „§ 8 Abs. 2 und § 8a Abs. 5", gebunden an AGB 0.7, `effective_from` leer bis Deploy 3. Kennungen werden nie wiederverwendet. `LegalText::currentFor($type, $agbVersion, $locale)`. Backfill: die 45 bestehenden Bestätigungen bekommen je eine Zeile in A.2 mit `legal_text_id` = v1, `agb_version` = 0.6, `confirmed_at` = `rights_confirmed_at`, `channel` = `form`, ohne IP und UA. Test: Wortlaut byte-genau rekonstruierbar, auch nachdem v3 aktiv ist. | S | A.2 |
| A.4 | Enums: `ImageProvenanceStatus` (`absent`, `unsigned_metadata`, `validated_trusted`, `validated_untrusted`, `invalid`, `unsupported`, `error`), `AiDeclarationSource` (wie A.1), `CreditSource`. `AiImageDeclaration::rank()` (0, 1, 2) für die Portalregel „Kennzeichnung = max(geprüfter Befund, Kundenangabe)", mit Kommentar, dass das eine Portalregel ist (Feedback F11). `label()` von `Original` wird „Ohne generative KI erstellt oder bearbeitet"; `publicLabel()` unverändert. | S | keine |
| A.5 | `App\Services\Image\ImageProvenanceInspector`: Imagick (`getImageProfile('iptc')`, `getImageProfile('xmp')`) und `exif_read_data()`. Felder: IPTC 2:110 Credit, 2:80 By-line, 2:116 Copyright; XMP `photoshop:Credit`, `dc:creator`, `dc:rights`, `Iptc4xmpExt:DigitalSourceType`; EXIF `Artist`, `Copyright`. IPTC-URIs normalisiert (Lang- und Kurzform). Zuordnung: `trainedAlgorithmicMedia` → Vorschlag `ai_generated`; `compositeWithTrainedAlgorithmicMedia` → `ai_edited`; `algorithmicallyEnhanced` → nur `enhancement_hint`, kein Vorschlag (E-B5); `digitalCapture`, `negativeFilm`, `positiveFilm`, `print` → Vorschlag `original`; sonst keiner. Nachweis-Vorschlag: Credit → dc:creator → By-line → EXIF Artist → CopyrightNotice, auf 255 gekürzt mit Vermerk. Status in Phase 1 nur `absent`, `unsigned_metadata` oder `error`. Dauer gemessen und im json abgelegt; Ausnahme oder über 10 s → `error` mit Fehlerklasse. Nicht ausgewertet: `CreatorTool`, Dateiname, Bildinhalt. Fixtures unter `tests/Fixtures/images/` (mit Imagick erzeugt): Kamera-JPEG mit IPTC, JPEG mit XMP `trainedAlgorithmicMedia`, JPEG mit `algorithmicallyEnhanced`, PNG ohne Metadaten, WebP mit XMP, beschädigte Datei. | M | A.4 |
| A.6 | `App\Services\PressRelease\PressReleaseImageUploader::store(UploadedFile, HasImages $subject, ImageSubmission, Actor)`: (1) Größe und Pixel prüfen (E-B6), Bytes einmal lesen, SHA-256; (2) Original auf Disk `local` nach `press-release-originals/{pm}/{uuid}.{ext}`, Schreibvorgang prüfen; (3) Prüfung auf dieselben Bytes; (4) Derivate über `ImageService` (A.7), bei Fehler alles zurückrollen; (5) `ai_declaration_user` = Angabe, `ai_declaration` = max(Vorschlag aus geprüftem Befund, Angabe) (Phase 1: Vorschlag aus `unsigned_metadata` fließt **nicht** ein, nur Vorbelegung; Abweichung nach unten wird im json als `deviated_from_prefill` vermerkt), `ai_declaration_source` = `customer`; (6) Transaktion mit `lockForUpdate()` auf der PM: ein-Titelbild-Regel, Bildzeile, Bestätigung (außer Kanal Admin, B.4), `rules_version`; (7) DB-Fehler → Dateien löschen. `ImageSubmission`: `copyright`, `licenseUrl`, `sourceUrl`, `title`, `description`, `aiDeclaration`, `rightsConfirmed`, `legalTextKey`, `focus`, `noCrop`. `Actor`: User, Token, Kanal. Test: DB-Fehler hinterlässt keine Datei; zwei gleichzeitige Uploads ergeben ein Titelbild; Hash von Original, Bestätigung und Prüfung identisch. | M | A.1 bis A.5, A.7 |
| A.7 | `ImageService::storePressReleaseImage()`: kein Rückfall auf die Quelldatei, `RuntimeException` stattdessen; neue Variante `og` (1200 × 630, H.1); `resizeCover` mit Fokuspunkt und `no_crop` (H.3). Quelldatei im Derivat-Ordner wird nach der Erzeugung gelöscht (Original liegt separat). Test: fehlende Cover-Variante landet nie öffentlich; Varianten ohne EXIF-GPS und Seriennummer. Erwartung „Originaldatei gelöscht" in `PressReleaseImageLicenseTest` umschreiben. | S | keine |
---
## Block B — Formular v2 und Admin-Upload
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| B.1 | `press-release-images-manager.blade.php` umbauen (Flag an): Block 1 Bild mit Vorschau, Prüfhinweis, Fokuspunkt-Wahl (H.3); Block 2 Bildnachweis (Pflicht, vorbelegt aus Metadaten, sonst Firmenname, sichtbar als „Vorschlag, bitte prüfen"), KI-Kennzeichnung (Pflicht, drei Optionen mit neuem Label, Erklärtext), aufklappbar „Optionale Angaben" (Titel, Bildunterschrift, Lizenz-URL, Quellen-URL getrennt); Block 3 Bestätigung als `flux:switch` mit Kurzform aus DU 3.3 (inkl. AGB-Datum), „Details" klappt den gerenderten Langtext aus A.3 auf, Kontaktweg für Widerspruch daneben. Vier Zustände: A `validated_trusted` (Phase 2), B `absent`, C `unsigned_metadata` („Vorbelegt aus den Metadaten der Datei, Sie können die Auswahl ändern"), D `error` („Herkunftsdaten konnten nicht geprüft werden", Speichern erlaubt, Vermerk). Entfallen: `author` als Pflicht, `license_type`, `license_detail`, Personen-, Marken-, Notizfelder, „Anbieter-Bedingungen geprüft". | M | A.6 |
| B.2 | Bindung an die Datei (Feedback F09): Prüfung nach `updatedNewImage` auf der Temp-Datei, Ergebnis und Hash in `#[Locked]`-Properties; beim Speichern rechnet der Uploader neu, Abweichung → Abbruch. Dateiwechsel setzt Vorbelegung und Schalter zurück. Manuell geänderter Nachweis wird nicht überschrieben. Effektive Werte serverseitig. | S | B.1 |
| B.3 | Tests: `PressReleaseImageRightsV2Test` ersetzt die Lizenz-Tests (alte bleiben für Flag aus, bis der Flag fällt); `PressReleaseAiDeclarationTest` um Snapshot-Erwartungen (Kundenangabe und Portalkennzeichnung getrennt). | M | B.1, B.2 |
| B.4 | Admin-Upload (DU E10, S7): Admin lädt hoch ohne Bestätigung; Bild bekommt `customer_confirmation_requested_at`, Status in der Karte „wartet auf Kundenbestätigung"; Mail „Bild bestätigen" mit Magic-Link an den Kontoinhaber (bestehende Magic-Link-Infrastruktur, Herkunft `image_confirmation`, erneut anforderbar); Landeseite zeigt Bild, Angaben und den Schalter mit Langtext; Bestätigung mit `channel = magic_link` in A.2. Veröffentlichungsgate (E.1) lässt ein Bild ohne Bestätigung nicht öffentlich werden; der Admin sieht das beim Freigeben. Admin-Bildtausch an einer veröffentlichten PM läuft genauso (neue Bestätigung zur neuen Datei). Test: ohne Klick keine Bestätigung, PM nicht veröffentlichbar mit Bild; nach Klick Protokoll mit Kontoinhaber. | M | A.2, A.6 |
---
## Block C — API
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| C.1 | `StorePressReleaseImageRequest` (Flag an): `copyright` required, `ai_declaration` required, `rights_confirmed` accepted, `confirm_text_version` required und aktive Kennung (sonst 422 mit `current_version`), `license_url`, `source_url`, `focus_x`, `focus_y`, `no_crop` optional; Grenze 16 MiB (E-B6). Controller ruft den Uploader mit `channel = api` und Token-ID. `PressReleaseImageResource` kompatibel ergänzt: `ai_declaration` (Portal), `ai_declaration_user`, `ai_declaration_source`, `provenance_status`, `rights_confirmation {version, confirmed_at}`, `warnings[]` (`provenance_check_failed`, Phase 2 `ai_declaration_raised_by_metadata`). `urls.original` bleibt das Cover, dokumentiert. Tests in `PressReleaseImageApiTest`. | M | A.6 |
| C.2 | Legacy-Bildupload abschalten (E-B4): `POST /service/pressreleases/{id}/images` antwortet im Legacy-Fehlerformat mit 410 und Hinweis auf v1; `GET …/images` bleibt. Controller und Legacy-Tests entfernen bzw. auf 410 umstellen; `dev/betrieb/api-legacy-paritaet.md` ergänzen. `StorePressReleaseImageRequest` verliert die Legacy-Ausnahme. | S | keine |
| C.3 | API-Doku (`dev/api/`, öffentliche API-Seite): Pflichtfelder, 422, Bestätigungstext mit Kennung, AGB-Fassung über die API-Nutzungsbedingungen (DU 3.3), Abschaltung des Legacy-Uploads. | S | C.1, C.2 |
---
## Block D — Ausgabe und öffentliche Sprache
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| D.1 | `App\Support\ImageCredit::for(PressReleaseImage, ?Company)`: eine Auflösung für alle Ausgaben. Nachweiszeile: „Foto: [copyright]", sonst „Foto: [author]", sonst (Bestand) „Quelle: [Firma]" (DU 3.4). KI-Zusatz aus `publicLabel()`, Bestand-Fallback `is_ai_generated`. Verantwortlich-Zeile „Bild und Angaben: [Firma], eingereicht am [Datum]" **nur** mit Bestätigungsprotokoll. Lizenz- und Quellenlink nur `http`/`https`. Ersetzt die vier Einzelimplementierungen (Artikelseite, Kunden- und Adminansicht, `ProofPdfRenderer::imageCreditLines()`). | S | A.2 |
| D.2 | Overlay auf der Artikelseite (beide Portale, anonyme Vorschau): rundes Icon unten links, 28 px, eigenes „i" (E-B13). Alpine.js, `aria-haspopup`, `aria-expanded`, Fokus hinein und zurück, Escape, unter `sm` Bottom-Sheet. Inhalt: Nachweis, Lizenz/Quelle, KI-Hinweis mit **Quelle der Kennzeichnung** („Angabe des Einreichers" / „aus geprüften Herkunftsdaten" / „aus Metadaten vorbelegt", DU 3.4), Verantwortlich-Zeile (nur mit Protokoll), „Bild melden". Keine Content-Credentials-Zeile in Phase 1. Manuelle Abnahme mit Tastatur und Screenreader. | M | D.1 |
| D.3 | Nachweiszeile unter dem Bild bleibt ohne Klick sichtbar. Kunden- und Adminansicht zeigen zusätzlich Bestätigungskennung, AGB-Fassung, Zeitpunkt und Kanal; bei Admin-Uploads den Zustand „wartet auf Kundenbestätigung". | S | D.1 |
| D.4 | `ProofPdfRenderer`: Zeilen aus D.1 plus Bestätigungskennung und Zeitpunkt; alte Felder bleiben für Bestandsbelege lesbar. | S | D.1 |
| D.5 | Web-Cache: `WebCaches::forgetPublicPages()` (bzw. die gezielte Variante für die Detailseite) bei Bild-Upload, -Löschung, Kundenbestätigung und Korrektur. | S | A.6 |
| D.6 | Wortwahl (DU E11, S10): `richtlinien.blade.php:36` durch den Absatz aus DU 3.5 ersetzen und den Abschnitt „Bilder" aus DU 3.5 einfügen, Version `richtlinien` in `legal_documents` hochzählen; „qualitätsgeprüft" und „geprüfte" in `lang/de/web.php` (189, 231, 233, 249, 255), `static.php:533`, `presseecho.blade.php:43` und der Footer-Tagline auf „automatisierte Prüfung" bzw. neutrale Formulierung; Ablehnungs- und Feedbackmails auf „Unsere automatisierte Prüfung hat …" (Fundstellen in den Mail-Templates und `lang/de` beim Umbau suchen). Die beiden AGB-Stellen (139, 206) bleiben bis v0.7. Test: `LegalDocumentTest` für die neue Richtlinien-Version; Snapshot-Test der geänderten Lang-Keys. | S | keine |
---
## Block E — Lebenszyklus, Original, Korrektur, Bereinigung
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| E.1 | `published_ever_at` an PM und Bild (DU S8, E5): gesetzt in `publish()`, `changeStatusFromAdmin()` nach `Published`, `restoreFromGone()` und für Bilder beim Upload an eine bereits veröffentlichte PM. Backfill: PMs mit `published_at` oder Status-Log nach `published`; deren Bilder ebenso. Veröffentlichungsgate: `publish()` verlangt für jedes Bild mit `rules_version` eine Bestätigung zur aktuellen `original_sha256` (A.2); fehlt sie (Admin-Upload ohne Kundenbestätigung), Abbruch mit klarer Meldung. Test je Pfad. | S | A.1, A.2 |
| E.2 | Snapshot der veröffentlichten Fassung (DU E2, S1): beim ersten Veröffentlichen der PM wird je Bild die Cover-Variante plus ein JSON mit gerendertem Nachweis, Portalkennzeichnung und Quelle nach `press-release-published/{pm}/{image}-{sha}.{ext|json}` auf Disk `local` kopiert, `published_snapshot_*` gesetzt. Bei Bildtausch an veröffentlichter PM neuer Snapshot. Test: Snapshot entspricht der ausgelieferten Variante. | S | E.1 |
| E.3 | Sofortlöschung nie veröffentlichter Uploads (DU E4, S9, mit O1): `remove()`, v1 `destroy()`, Ersatz durch Upload, `PurgeExpiredPressReleaseDrafts` und Löschung der PM löschen bei `published_ever_at IS NULL` Original, Derivate, Snapshot **und** Bestätigung (harte Löschung der Bestätigungszeile ist hier gewollt; der Observer aus A.2 lässt genau diesen Fall zu). Bei gesetztem `published_ever_at`: Übergabe an das Beweisarchiv (F.4), danach Derivate löschen, Soft Delete. Test je Fall. | M | E.1, F.4 |
| E.4 | Zugriff auf Original und Snapshot nur über `GET /admin/press-releases/{pm}/images/{image}/original` und `…/published` mit Admin-Recht, jeder Abruf in `archive_access_log` (F.1) mit Pflichtangabe „Zweck". Medienroute, Vorschau-Signaturen und API-Resource kennen die Pfade nicht. Architekturtest: `original_path` und `published_snapshot_path` nur in Uploader, Snapshot-Service, Admin-Route, Archiv. | S | A.1, F.1 |
| E.5 | Legacy-Bereinigung (E-B11): `press-release-images:purge-inactive-legacy` (Standard Dry-Run, `--execute`): löscht Bildzeilen und Dateien für Legacy-Bilder (`legacy_id` gesetzt, `rules_version NULL`) an PMs mit Status `rejected`, `archived` oder `draft`; auf der Dev-Kopie 10.768 Bilder. Bericht mit Zahl je Status und Speicherplatz. Live-Schritt nach Deploy 1. Test mit Fixture-Zeilen. | S | keine |
| E.6 | Korrekturweg (E-B7, DU S17): Admin ändert `copyright` und `ai_declaration` (Portal) mit Pflichtgrund; `ai_declaration_source = admin_correction`; Zwischenzustand „Bild vorläufig zurückgehalten" (Bild aus der öffentlichen Seite, Platzhalter aktiv) bis zur Entscheidung; Archiveintrag `label_corrected` mit alt/neu/Quelle/Grund/Bearbeiter (F.4). Kundenangabe und ursprüngliche Bestätigung bleiben unverändert. Kontaktweg im Formular und im Overlay. | S | A.1, F.4 |
| E.7 | Betrieb: `config/images.php` (Größen, Pixel, Timeout, Vertrauensliste für Phase 2); Zähler `provenance_check_failed` im Admin-Systemstatus; Laufzeit der Prüfung auf Live mit 16-MiB-Bild messen und im Protokoll notieren. | S | A.5 |
---
## Block F — Beweisarchiv (DU Abschnitt 4)
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| F.1 | Tabellen `archive_entries` (`subject_type`, `subject_id`, `event` Enum `image_removed`, `image_replaced`, `pm_depublished`, `gdpr_change`, `label_corrected`; `actor_type`, `actor_id`, `reason_category` Enum, `payload_path`, `payload_hash`, `retain_until`, `legal_hold`, `legal_hold_note`, `legal_hold_set_by`, `legal_hold_set_at`, `deleted_at` als Tombstone, `created_at`) und `archive_access_log` (`entry_id`, `admin_id`, `accessed_at`, `purpose` Pflicht). Append-only per Observer. Disk `archive` (`storage/app/archive`, außerhalb des Web-Roots), Payload als ZIP, verschlüsselt mit eigenem Schlüssel `ARCHIVE_KEY` aus der Umgebung über `Illuminate\Encryption\Encrypter` (nicht `APP_KEY`, damit ein Schlüsselwechsel der App das Archiv nicht berührt). `retain_until` = 31.12. des Ereignisjahres + 3 Jahre. | M | keine |
| F.2 | `App\Services\Archive\EvidenceArchive`: `record(event, subject, payload[], actor, reason)` (packt, verschlüsselt, hasht, schreibt Eintrag), `open(entry, admin, purpose)` (entschlüsselt, protokolliert), `export(entry)` (signiertes ZIP mit Metadaten und Hash für den Anwalt), `setLegalHold()`/`releaseLegalHold()` (nur Admin, Pflichtvermerk; nach Aufhebung `retain_until` neu ab Aufhebungsdatum). Admin-Seite: Liste, Detail, Zugriff mit Zweck, `legal_hold`-Schalter, Export. Das Audit-Log der Anwendung referenziert nur `archive_entries.id`, nie Altwerte. | M | F.1 |
| F.3 | Jobs: `archive:purge` täglich (Einträge mit `retain_until < heute` und ohne `legal_hold`: Payload löschen, Tombstone setzen, Log ohne Inhalt); `archive:export-tombstones` täglich (JSON der Tombstone-IDs auf die Archiv-Disk, Teil der Sicherung); `archive:reconcile-after-restore` (liest den Export, entfernt Payloads zu Tombstones erneut; Bestandteil des Restore-Runbooks in `dev/betrieb/deploy-runbook.md`). Test: Purge im Dry-Run ändert nichts; `legal_hold` blockt; Reconcile löscht wiederhergestellte Payloads. | S | F.2 |
| F.4 | Anbindung (DU 4.1, S14): `image_removed`/`image_replaced` aus E.3 (Original, Snapshot, Bestätigung); `pm_depublished` in `depublish()`, `archive()`, `changeStatusFromAdmin()` weg von `Published`, `deleteFromAdmin()` einer veröffentlichten PM und Ende der Laufzeit (letzter veröffentlichter Stand: Text, Bilder-Snapshots, Kontaktfelder, Einreicher, Zeitpunkte, Grund als Kategorie); `gdpr_change` in `AnonymizationService` und `CorrectionService` (Diff alt/neu, Referenz auf Antrag); `label_corrected` aus E.6. Ablehnung im Freigabelauf: kein Eintrag (nur Vertragsdaten in der normalen DB). Test je Auslöser: Eintrag vorhanden, Payload entschlüsselbar, Hash stimmt. | M | F.2, E.2, E.3, E.6 |
| F.5 | DSE-Zusagen prüfen: Zugriffsprotokoll (F.1), Tombstone nach Restore (F.3) und die dokumentierte Interessenabwägung (DU 4.4 als Datei im Rechtsordner, Betreiber) müssen stehen, bevor DSE 9b in Deploy 3 veröffentlicht wird. Checkliste im Protokoll. | S | F.1 bis F.4 |
---
## Block G — Zustimmungsgate (DU E12, S12)
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| G.1 | Tabelle `legal_document_acceptances` (`user_id`, `legal_document_id`, `type`, `version`, `accepted_at`, `context` Enum `submit`, `booking`, `api`, `gate`, `ip_hmac`, `user_agent`). Insert-only. Bestehende `booking_confirmations` bleiben, werden nicht migriert. | S | keine |
| G.2 | Gate vor jeder Einreichung: die vier Kunden-Submit-Pfade (`create`, `edit`, `show`, `index`) prüfen über einen gemeinsamen Trait, ob für AGB und Richtlinien in der aktuellen Version eine Annahme vorliegt; sonst Modal mit aktivem Klick je Dokument, Link auf den Text, kein „Weiternutzung gilt als Zustimmung". Bestandskunden werden beim nächsten Einreichen einmalig durchgeschleust. API (O2): `POST /api/v1/legal/acceptances`, `submit` antwortet ohne gültige Annahme mit 428 und `required_versions`. Test: Submit ohne Annahme blockiert, mit Annahme durch; neue Version fordert erneut. | M | G.1 |
| G.3 | Aktivierung erst in Deploy 3 mit AGB v0.7 (bis dahin hinter `LEGAL_ACCEPTANCE_GATE=false`), damit Bestandskunden gleich die neue Fassung annehmen und nicht zweimal klicken. | S | G.2 |
---
## Block H — OpenGraph, Teaser, Beschnitt (DU E7, S15, S16)
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| H.1 | Variante `og` (1200 × 630) mit eingebranntem Nachweisbalken unten: halbtransparenter Balken, Text „Foto: [Nachweis]" plus KI-Zusatz, per GD `imagettftext` mit `resources/fonts/Inter_18pt-SemiBold.ttf` (vorhanden, OFL 1.1, O4). Wird beim Upload und beim Korrekturweg neu erzeugt (Nachweis kann sich ändern). `release-detail.blade.php:168` liefert `og` statt `detail`. Bestand ohne `og`-Variante: `press-release-images:regenerate-variants --only=og` für die 3.786 veröffentlichten Legacy-Bilder (Nachweis oder „Quelle: [Firma]"). Test: Variante existiert, Balken vorhanden (Farbprüfung der unteren Zeilen). | M | D.1, A.7 |
| H.2 | Teaser und Listen: `alt` und `title` der Vorschaubilder tragen den Nachweis („[Titel] · Foto: [Nachweis]"); Listen-Karten bekommen eine Bildunterschrift-Zeile mit dem Nachweis, wo Platz ist (Startseite, Kategorie, Newsroom, Suche, Archiv). Beide Portale. | S | D.1 |
| H.3 | Beschnitt-Steuerung (§ 9 Abs. 1 letzter Satz): Fokuspunkt je Bild (`focus_x`/`focus_y` in Prozent, Neun-Punkte-Wahl im Formular) oder `no_crop` (Letterbox mit neutralem Rand). `resizeCover` berücksichtigt beides für alle Varianten. Test: Fokus rechts oben schneidet links unten ab; `no_crop` zeigt das volle Bild. | M | A.7, B.1 |
---
## Block I — Phase 2 und 3: C2PA, Metadaten, Partner
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| I.1 | `c2patool` (Release-Binary, Version und Prüfsumme im Repo) in Sail-Image und per Ploi-Skript auf den Server; Aufruf über `Symfony\Process` mit Argumentliste, `--detailed`, 10 s, 1 MiB Ausgabe, keine externen Manifestverweise. Vertrauensliste `config/c2pa.php` (Aussteller; Geschäftsentscheidung). | M | E.7 |
| I.2 | Inspector-Erweiterung: `validated_trusted`, `validated_untrusted`, `invalid`, `unsupported`; nur das aktive Manifest zählt, Bestandteile werden nicht übernommen; Konflikt C2PA gegen XMP → C2PA gewinnt mit Vermerk; Aussteller nur aus dem Zertifikat. Testkorpus: OpenAI, Adobe Firefly, Google, Kamera mit Content Credentials, gebrochene Bindung, zwei Manifeste, Fotomontage mit KI-Bestandteil. | M | I.1 |
| I.3 | Sperre (DU E9): `ai_declaration_locked` nur bei `validated_trusted` mit `trainedAlgorithmicMedia` oder `compositeWithTrainedAlgorithmicMedia`; `ai_declaration` = max(Befund, Kundenangabe), `ai_declaration_source = c2pa_verified`; Höherstufung möglich; Unterschreitung verworfen und als `lock_bypass_attempt` geloggt (12 Monate, ohne Nutzdaten); API-Warnung `ai_declaration_raised_by_metadata`. Zustand A im Formular und Content-Credentials-Zeile im Overlay nur bei `validated_trusted`. | S | I.2 |
| I.4 | Messung auf Live (Laufzeit je Dateigröße, Fehlerquote nach vier Wochen). Revisit: mehr als eine Handvoll Widersprüche pro Monat → Korrekturweg neu bewerten. | S | I.3 |
| I.5 | Metadaten in die Varianten (DU S21): Imagick `setImageProfile('xmp', …)` mit `dc:rights`, `photoshop:Credit`, `Iptc4xmpExt:DigitalSourceType` für JPEG, PNG, WebP; nach dem Schreiben wieder auslesen; Variantenname mit Versionsanteil wegen `max-age=86400`. | M | D.1 |
| I.6 | Re-Sync-Sperre für Bilder (DU S22): sobald ein API-Partner Bilder liefert, dürfen Importer-Läufe entfernte oder korrigierte Bilder nicht wiederherstellen (analog `lockSync()` der PM). Erst bauen, wenn der Fall eintritt. | S | Partner |
---
## Block J — Ausblick: Bilder im Newsroom (Roadmap 1b.3)
Unverändert aus Fassung 1: Tabelle `company_images` mit denselben Spalten (gemeinsames Migrations-Makro), Logos ohne Kennzeichnung; Uploader und Bildmanager über `HasImages` verallgemeinert, Bestätigungen mit `subject_type = Company`; Newsroom-Bilder sind sofort öffentlich, deshalb Bestätigung vor dem Speichern; öffentliche Galerie mit Nachweiszeile und Overlay; Kontingent je Newsroom (Geschäftsentscheidung). Aufwand M. Voraussetzung: Deploy 1 und 2 live.
---
## Block K — KI-Vorab-Check für Bilder (DU Abschnitt 12, E15, S24)
Grundlage ist die Text-Prüfstrecke: OpenAI über `Http` (Konfiguration `services.openai`), Manager/Treiber-Muster (`ClassificationManager`), deterministischer Fallback, unveränderliches `ki_audits`-Protokoll. Der Bild-Check bekommt denselben Aufbau mit eigenem Manager, eigenem Config-Block `images.precheck` und Bild-Eingabe (`image_url` mit Data-URI). Drei Regeln aus DU 12.2 gelten wörtlich: jeder Befund hat einen definierten Ausgang; öffentlich bleibt es „automatisierte Prüfung"; die KI setzt nie die Kennzeichnung.
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|---|---|---|---|
| K.1 | `ImagePrecheckManager` mit Treibern `openai` und `deterministic` (liefert nie einen Befund); Vertrag `check(string $absolutePath): ImagePrecheckResult`. Vorbereitung als Datenminimierung: Imagick, 1024 px lange Kante, `stripImage()`, JPEG 82, Base64. Antwortschema DU 12.4 über `response_format json_object`; der Prompt fragt nur „Person(en) erkennbar", nie eine Identifizierung. Fehler, Timeout, unbrauchbare Antwort → Ergebnis `error`: durchlassen mit Vermerk, nie blockieren. Config: `enabled` (`IMAGE_PRECHECK`, Standard aus), `stage` (`IMAGE_PRECHECK_STAGE`, 1 = nur `stock_watermark`, 2 = alle Kategorien), `provider`, `model` (leer = `services.openai.model`), `timeout` 20 s, `reject_threshold` 0,85, `queue_threshold` 0,6. | S | A |
| K.2 | Auswertung nach DU 12.3 in `ImagePrecheckOutcome` (`reject`, `queue`, `hint`, `pass`, `error`): `reject` bei `stock_watermark`, `nudity_or_violence`, `document_or_id` ab 0,85; `queue` bei `screenshot_or_collage`, `third_party_logo_prominent`, `license_plate_readable` ab 0,6 und bei den drei Ablehnungskategorien zwischen 0,6 und 0,85; `hint` bei `persons_visible`, bei `looks_ai_generated` nur wenn die Kundenangabe „ohne KI" lautet, und bei `technical_defect`; sonst `pass`. Stufe 1 wertet ausschließlich `stock_watermark`. `ai_declaration` bleibt unberührt (Regel 3). | S | K.1 |
| K.3 | Einbau in den Uploader: Check auf den Originalbytes **vor** dem Schreiben von Original und Derivaten. Formular: bereits in `preflight()` nach der Dateiauswahl, Ergebnis mit Hash-Bindung im Komponentenzustand; `store()` rechnet nur neu, wenn kein Ergebnis oder der Hash nicht passt. API: in `store()`. `reject` → `ImageUploadException` mit dem Grund in einem Satz (Formular unter dem Dateifeld, API 422 Feld `image`), nichts gespeichert, `ki_audits`-Eintrag `image_precheck` an der PM (Kategorie, Konfidenz, Modell, ohne Datei). Gespeicherte Bilder: Spalten `precheck` (JSON: Kategorien mit Konfidenz, Modell, Version, Zeitpunkt, Ausgang, Stufe), `precheck_outcome`, `precheck_review_required_at`, `precheck_reviewed_at`, `precheck_reviewed_by`, `precheck_review_note`; `ki_audits`-Eintrag je Check; `precheck_snapshot` als eigenes Feld im Bestätigungsprotokoll (DU 12.5); Archiv-Payload `imageRemoved` um die Spalten erweitert. | M | K.2 |
| K.4 | Admin-Queue: `queue` setzt `precheck_review_required_at` und `withheld_at`. Die Meldung bleibt im Freigabelauf (DU 12.3), nur das Bild ist bis zur Sichtung zurückgehalten (bestehender Zustand E.6, Platzhalter öffentlich). Kunde sieht „Wird kurz gesichtet" an der Titelbild-Karte. Admin-Entscheidung mit Vermerk: Freigabe (Bild wird sichtbar, OG-Variante bleibt) oder Ablehnung (Entfernen über `PressReleaseImageRemover`, Grund `own_decision`); beide als `ki_audits`-Eintrag `image_precheck_review` und in den `precheck_reviewed_*`-Spalten. | M | K.3 |
| K.5 | Oberfläche: Formular v2 zeigt Hinweise aus `hint` (Personen: Einverständnis-Satz aus DU 12.3; „wirkt KI-generiert" nur, wenn „ohne KI" gewählt ist; technischer Mangel als Notiz); Admin-PM-Seite bekommt den Sichtungsblock mit Befund, Freigabe/Ablehnung und Vermerk; Admin-PM-Liste Ansicht „Bild-Sichtung" mit Zähler; Systemstatus-Zähler offener Sichtungen. | S | K.4 |
| K.6 | Datenschutz und Betrieb (DU 12.6): AVV mit dem Anbieter und DSE-Absatz zur Bildsichtung gehören zu Deploy 3 (Betreiber, Anwalt); bis dahin bleibt der Check aus. Einschalten: `IMAGE_PRECHECK=true`, `IMAGE_PRECHECK_STAGE=1` direkt nach Deploy 1b, Stufe 2 nach einer Woche ohne Fehlurteile. Wiedervorlage DU 12.8: Kategorie mit gehäuften Fehlurteilen per Config von `reject` auf `queue` herabstufen (`images.precheck.downgrade_to_queue`), nicht abschalten. | S | Betreiber |
Nicht aufgenommen: Nachsichtung des Bestands; Bewerbung des Checks nach außen (würde den Hoster-Status berühren, DU 12.8); Sperre der Kennzeichnung aus dem Modellurteil (E9).
---
## Reihenfolge und Deploys
| Deploy | Inhalt | Live-Schritte | Bedingung |
|---|---|---|---|
| **1** | Blöcke A bis E, D.6; Flag `IMAGE_RIGHTS_V2=false` | `migrate` (Sekunden), Seeder `LegalTextSeeder` (v1, v2, v3 ohne Datum) und Richtlinien-Version, Backfill A.3 und E.1, `press-release-images:purge-inactive-legacy` erst Dry-Run dann `--execute`, `.env`: `CONFIRMATION_IP_KEY`, `IMAGE_RIGHTS_V2=false` | keine |
| **1b** | Flag einschalten | `IMAGE_RIGHTS_V2=true`, `config:cache`; Legacy-Upload antwortet ab hier 410 | Prüfung auf Live mit Testkonto. **Wartet nicht auf den Anwalt** (DU E14). Bestätigungstext `img-confirm-v2` mit Verweis auf AGB 0.6. |
| **2** | Blöcke F, G (Gate aus), H | `migrate`, `.env`: `ARCHIVE_KEY`, Disk `archive` anlegen, Scheduler (`archive:purge`, `archive:export-tombstones`), Restore-Runbook ergänzen, `press-release-images:regenerate-variants --only=og` | parallel zur Anwaltsfrist |
| **2b** | Block K (Check aus) | `migrate`, `.env`: `IMAGE_PRECHECK=false`; nach AVV und DSE-Absatz: `IMAGE_PRECHECK=true`, `IMAGE_PRECHECK_STAGE=1`, später `2` | Deploy 1b live; AVV mit dem Anbieter; DSE-Absatz DU 12.6 (Deploy 3) |
| **3** | AGB v0.7 (§ 8a, § 9), DSE v0.4 (9a, 9b, 13), `img-confirm-v3` aktiv, Gate an | Seeder `legal_documents` (AGB 0.7, DSE 0.4), `legal_texts` v3 `effective_from`, `LEGAL_ACCEPTANCE_GATE=true`, Interessenabwägung als Dokument abgelegt | Anwaltsantwort, F.5 |
| **4** | Block I | `c2patool` auf dem Server, `config/c2pa.php` | vier Wochen Deploy 1b ohne Auffälligkeiten, Testkorpus |
| **5** | Block J | `migrate` | Kontingententscheidung |
Aufwand: Deploy 1 etwa zehn bis vierzehn Arbeitstage, Deploy 2 etwa acht bis zehn, Deploy 3 zwei, Deploy 4 fünf bis sieben.
**Live-Schritte für Deploy 1 und 2 zusammen (Stand 22.09.2026, beide Blöcke sind gebaut):**
1. Dateien hochladen, `php artisan view:clear && php artisan view:cache`, `php artisan config:cache`.
2. `.env`: `IMAGE_RIGHTS_V2=false`, `CONFIRMATION_IP_KEY=<eigener Wert>`, `ARCHIVE_KEY=<php artisan archive:key>`, `LEGAL_ACCEPTANCE_GATE=false`, `IMAGE_PRECHECK=false`; `ARCHIVE_KEY` getrennt sichern.
3. `php artisan migrate --force` (neun Migrationen, alle additiv; press_release_images und press_releases ohne FULLTEXT, Sekunden).
4. `php artisan db:seed --class=LegalTextSeeder --force` und `php artisan db:seed --class=LegalDocumentSeeder --force` (Richtlinien 0.2).
5. `php artisan image-rights:backfill-confirmations` und `php artisan press-releases:backfill-published-ever` (je erst `--dry-run`).
6. `php artisan press-release-images:purge-inactive-legacy` (Trockenlauf), dann mit `--execute`.
7. `php artisan queue:restart`; Scheduler läuft `archive:purge` und `archive:export-tombstones` automatisch.
8. Prüfung auf Live mit Testkonto (Flag noch aus: altes Formular sichtbar), dann `IMAGE_RIGHTS_V2=true` + `config:cache` (Deploy 1b). Ab dann antwortet der Legacy-Bildupload 410.
9. Bestand: `php artisan press-release-images:regenerate-variants --only=og --published` (Trockenlauf), dann mit `--execute` — erzeugt die OG-Variante mit eingebranntem Nachweis für die 3.786 veröffentlichten Bestandsbilder; bis dahin fällt og:image auf die Detail-Variante zurück.
10. Vorab-Check (Block K, Deploy 2b): erst nach AVV mit OpenAI und DSE-Absatz (DU 12.6) `IMAGE_PRECHECK=true`, `IMAGE_PRECHECK_STAGE=1` + `config:cache`; nach einer Woche ohne Fehlurteile `IMAGE_PRECHECK_STAGE=2`. Nutzt `OPENAI_API_KEY` der Text-Prüfstrecke; das Modell muss Bilder verarbeiten können (`IMAGE_PRECHECK_MODEL` bei Bedarf). Offene Sichtungen: Pressemitteilungen ▸ Bild-Sichtung.
---
## Abnahmetests
| Bereich | Fälle | Block |
|---|---|---|
| Migration | vorhandene Werte unverändert; 45 Bestätigungen mit v1-Verweis; `published_ever_at`-Backfill stimmt mit Status-Log überein | A.1, A.3, E.1 |
| Bereinigung | Dry-Run ändert nichts; `--execute` entfernt nur Legacy-Bilder an `rejected`/`archived`/`draft`; veröffentlichte Legacy-Bilder bleiben | E.5 |
| Metadaten | IPTC Credit, XMP `trainedAlgorithmicMedia`, `algorithmicallyEnhanced` ohne Vorschlag, PNG ohne Metadaten, WebP mit XMP, beschädigt → `error` | A.5 |
| Formular | Pflichtfelder; Vorbelegung überschreibt keine Eingabe; Dateiwechsel setzt Schalter zurück; Zustand D speichert mit Vermerk; Kundenangabe und Portalkennzeichnung getrennt gespeichert | B |
| Admin-Upload | ohne Kundenklick keine Bestätigung und keine Veröffentlichung mit Bild; Magic-Link bestätigt mit Kontoinhaber; Bildtausch an veröffentlichter PM verlangt neue Bestätigung | B.4, E.1 |
| Atomarität | DB-Fehler nach Dateispeicherung; parallele Titelbild-Uploads; Hash identisch in Original, Bestätigung, Prüfung | A.6 |
| Zugriff | Original und Snapshot weder über Medienroute noch Vorschau noch API; Admin-Route mit Zweck im Zugriffslog | E.4 |
| API | v1 Pflichtfelder 422, falsche Kennung 422 mit Hinweis, Statusgrenze, kompatible Response; Legacy 410 | C |
| Bestätigung | Wortlaut mit AGB-Fassung byte-genau rekonstruierbar; User, Token, Kanal korrekt; Firmenwechsel ändert alte Bestätigung nicht | A.2, A.3 |
| Lebenszyklus | nie veröffentlicht → sofort alles weg; veröffentlicht → Archiveintrag, Derivate weg; Depublikation, Statuswechsel, Löschung, DSGVO-Änderung, Korrektur erzeugen je einen Eintrag; Purge, `legal_hold`, Reconcile | E.3, F |
| Gate | Submit ohne Annahme blockiert; mit Annahme durch; neue Version fordert erneut; API 428 | G |
| Ausgabe | beide Portale, Kunden-/Adminansicht, Vorschau, PDF; Nachweiszeile ohne Klick; Verantwortlich-Zeile nur mit Protokoll; Bestand „Quelle: [Firma]"; Overlay per Tastatur; OG-Variante mit Balken; Teaser-`alt` mit Nachweis; Fokus und `no_crop` | D, H |
| Sprache | keine „qualitätsgeprüft"/„Geprüfte Mitteilungen" mehr außerhalb der AGB; Richtlinien-Version hochgezählt | D.6 |
| Vorab-Check | Stock-Wasserzeichen ab 0,85 lehnt ab, nichts gespeichert, Audit ohne Datei; 0,7 geht in die Queue mit zurückgehaltenem Bild; Personen → Hinweis, kein Eingriff; „wirkt KI" ändert die Kennzeichnung nie; Modellfehler lässt durch mit Vermerk; Stufe 1 ignoriert alle anderen Kategorien; Hash-Bindung zwischen Vorprüfung und Speichern; Freigabe macht sichtbar, Ablehnung entfernt; Snapshot im Protokoll und im Archiv-Payload; Check aus = keine Anfrage | K |
| Phase 2 | vertrauenswürdig signiert sperrt; untrusted nicht; gebrochene Bindung `invalid`; Bestandteil-Typ nicht übernommen; Höherstufung möglich, Unterschreitung geloggt | I |
---
## Anwalt
Agenda, Prüfannahmen, finale Texte und Risiken stehen im Decision-Update (Abschnitte 3, 7, 8). Dieser Plan liefert dazu den Screenshot von Formular und Nachweiszeile nach Deploy 1b. Was der Anwalt kippen könnte, ist dort mit Reaktionen ohne Umbau belegt; der Plan hat dafür keine gesonderten Pakete.
---
## Umsetzungsprotokoll
| Datum | Punkte | Commit | Bemerkung |
|---|---|---|---|
| 22.09.2026 | O4 / Schriften | `b624f80` | resources/fonts eingecheckt (20 Dateien, OFL 1.1) |
| 22.09.2026 | A.1–A.8 | `e9bdff3` | Migrationen, legal_texts (v1/v2/v3), image_rights_confirmations, ImageProvenanceInspector mit echten Fixtures, PressReleaseImageUploader, ImageService ohne Rückfall; 30 Tests |
| 22.09.2026 | B.1–B.5 | `40953d6` | press-release-images-manager-v2 hinter IMAGE_RIGHTS_V2, Admin-Upload mit Magic-Link-Bestätigung, Remover; 11 Tests |
| 22.09.2026 | C.1–C.3 | `65ac72c` | v1-Pflichtfelder, Resource, Legacy 410, API-Doku de/en, Paritätsdokument; 7 Tests |
| 22.09.2026 | D.1–D.6 | `2124ffd` | ImageCredit, Overlay, Kunden-/Adminansicht, PDF, Richtlinien 0.2, Wortwahl; 5 Tests; Baseline bereinigt |
| 22.09.2026 | E.1–E.7 | `46b89e1` | Gate, published_ever_at, Snapshot, Sofortlöschung (O1), Backfill, Legacy-Bereinigung (E-B11, 10.768 auf Dev), Beweiszugriff, Korrekturweg, Systemstatus; 14 Tests |
| 22.09.2026 | F.1–F.5 | `0240c14` | archive_entries/archive_access_log, EvidenceArchive, PressReleaseArchiver, Hooks, Scheduler, Admin-Seite, Runbook; 9 Tests. F.5-Checkliste: Zugriffsprotokoll ✓, Restore-Abgleich ✓, Interessenabwägung als Dokument (Betreiber) offen |
| 22.09.2026 | G.1–G.3, H.1–H.3 | `62723b5` | Zustimmungsgate (aus bis AGB 0.7, API 428/POST acceptances), OG-Balken, OG-Neuaufbau, Teaser-Nachweis; 8 Tests. Gesamtsuite nach Block E: 2.037 grün |
| 22.09.2026 | H.1 Bestand | `3b613fa` | `press-release-images:regenerate-variants --only=og`, Trockenlauf Standard; Testhelfer umbenannt (Kollision mit ManualReviewGateTest) |
| 22.09.2026 | K.1–K.6 | `0e5cd20` | ImagePrecheckManager (openai/deterministic), Auswertung 12.3, Uploader-Einbau mit Hash-Bindung, ki_audits `image_precheck`/`image_precheck_review`, Queue mit `withheld_at`, Admin-Sichtung (Liste, PM-Seite, Systemstatus), Formular-Hinweise, API 422/Warnung; 17 Tests. K.6 (AVV, DSE-Absatz, Einschalten) Betreiber |
| 25.09.2026 | Test auf Dev, Nacharbeiten | `6d558cd`, `d99f55e` | KI-Badge am Artikelbild rechts unten (Überlappung mit Nachweis-Zeichen); Admin ▸ Management ▸ Bilder: Liste aller Titelbilder mit PM-Link, Nachweis, Bestätigung, KI-Check, Filter; 2 Tests |
| 25.09.2026 | Umsetzungsaudit 25.09. nachgearbeitet | `4e34c3d`, `48129e9` | Block 8 im Audit-Entwicklungsplan: Freigabezustand `isPubliclyDeliverable()`, Ability `confirmImageRights`, Archiv-Sperren/Fehlerauswertung, `archive_pending_at` + Nachholer, Anonymisierung über den Remover, Rejected-Purge, App-Kopien fallen mit dem Tombstone; N09 offen |
| 28.09.2026 | **Deploy 1, 2 und 2b live** | — | Betreiber: 12 Migrationen (längste `archive_pending_at` 12 s), Seeder, Backfill 63 Bestätigungen / 35.459 `published_ever_at`, `purge-inactive-legacy --execute` 10.769 Bilder (~2,98 GB), `purge-drafts` 2 Entwürfe / 0 Abgelehnte (Altbestand korrekt geschützt), Archiv-Nachholer und Orphan-Sweep leer, vier Archiv-Tasks im Scheduler, OG-Varianten für 3.807 Bestandsbilder erzeugt, Caches neu |
| 28.09.2026 | **Deploy 1b live** | — | `IMAGE_RIGHTS_V2=true`; `IMAGE_PRECHECK` und `LEGAL_ACCEPTANCE_GATE` bleiben aus (AVV/DSE bzw. AGB 0.7). Betreiber hat den Bild-Upload mit dem neuen Formular über ein Testkonto auf Live geprüft: funktioniert. Legacy-Bildupload antwortet ab jetzt 410. Vier-Wochen-Frist für Block I (C2PA) läuft ab heute, also ab 26.10.2026 |
| 29.09.2026 | Umsetzungsaudit 29.09. (Block 9 im Audit-Entwicklungsplan) | `5ec252d` (Bilder), `7cd84b2` (Archiv) | Feed/Card-Helfer/PDF nur freigegebene Bilder (B-01/B-02), Livewire-Upload 16 MiB (B-03), Snapshot versioniert inkl. OG-Variante (N09), Bestätigung an Bild-ID/SHA gebunden (B-04), Entfernen unter Sperre (B-05), Korrektur über Archivauftrag (B-06/A-09), `precheck_withheld_at` (B-07), Magic-Link-Zweck `image_confirmation` (B-10), Sweep verwaister Originale (B-11). **Korrektur zur Zeile vom 28.09.:** „Altbestand korrekt geschützt" war nicht belegt — `published_ever_at` ist für alle abgelehnten Alt-Meldungen NULL (Backfill kann „offline genommen" nicht erkennen); `purge-drafts` griff nur nicht, weil die Bilder vorher entfernt waren. Jetzt schützt `whereNull('legacy_id')` bzw. `PressRelease::wasEverPublished()` den Altbestand ausdrücklich (A-05) |