- 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>
332 lines
52 KiB
Markdown
332 lines
52 KiB
Markdown
# 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) |
|