# Umsetzungs-Audit und Bestandsaufnahme vom 29.09.2026 **Prüfdatum:** 29.09.2026 **Geprüfter Code:** `98c465b` auf `technik/betriebsfixes-2026-09` (entspricht dem Live-Stand nach Deploy 1/1b/2/2b vom 28.09.2026) **Vergleich:** Schwerpunkt `d99f55e..98c465b` (Nacharbeit Block 8: `4e34c3d`, `48129e9`), dazu ein neuer Durchgang über die seit 01.09. gebauten Roadmap- und Bildfunktionen **Grundlagen:** [Entwicklungsplan Blöcke 1–8](ENTWICKLUNGSPLAN.md), [Umsetzungsaudit 25.09.](UMSETZUNGSAUDIT-2026-09-25.md), [Bildkennzeichnungsplan Fassung 2](bildkennzeichnung-entwicklungsplan-2026-09-21.md), [Decision-Update 22.09.](Decision-Update_Bildrechte_Beweisarchiv_2026-09-22.md), [Roadmap](../2026-09-01/roadmap-2026.md) mit Anhängen 1 und 2, [Umsetzungsprotokoll](../2026-09-01/umsetzung-protokoll.md) **Umsetzung:** Am selben Tag als Block 9 umgesetzt — Stand, Entscheidungen E19–E34 und Live-Schritte im [Entwicklungsplan](ENTWICKLUNGSPLAN.md#block-9--umsetzung-des-umsetzungsaudits-vom-29092026). Der Bericht selbst bleibt als Befundstand erhalten; Korrektur: Q-07 war ein Fehlalarm. **Zweck:** Stand der Dinge nach dem Live-Gang aufnehmen, die Block-8-Nacharbeit gegenprüfen, neue Fehler und Optimierungen belegen und alles Offene an einer Stelle sammeln, damit die Entwicklung hier weitergehen kann. Nichts an Code, Tests, Konfiguration oder Daten wurde geändert. Datenbankabfragen liefen nur lesend gegen die Dev-Kopie. --- ## 1. Ergebnis in Kürze - **Block 8 hält im Kern.** Von den 22 Befunden des 25.09. sind 15 vollständig behoben. N01, N06, N11, N17 und N19 sind teilweise behoben, N09 ist wie geplant offen. N13 ist technisch umgesetzt, beruht aber auf einer Annahme, die nicht stimmt (A-05). - **Qualitätsgates:** Die Gesamtsuite läuft seriell grün mit **2.122 bestandenen und 1 übersprungenen Test** (393 s). Im Parallelmodus scheitern 9 Tests an einem Testhelfer, der in einer anderen Datei definiert ist (Q-01). Larastan Level 4 meldet über den ganzen Scope 1 Fehler in Altcode (Q-02). Pint ist sauber. - **Neu gefunden: 4 × P1, 22 × P2** sowie eine Reihe von P3-Punkten und Optimierungen. Die P1-Befunde sind klein im Fix und groß in der Wirkung: 1. **R-01:** Über die Legacy-API wird der Einlieferer als `member` einer *fremden* Firma angelegt. Damit bekommt er Schreibrecht auf alle Meldungen dieser Firma. 2. **K-04:** Über den Google-Login lässt sich ein Konto vorab übernehmen. Ein fremd angelegtes, unverifiziertes Konto wird aktiviert, und das Passwort des Angreifers bleibt gültig. 3. **K-01:** Der Scheduler veröffentlicht Meldungen, die nach E3 auf eine manuelle Prüfung warten. 4. **B-01:** Der Startseiten-Feed beider Portale ist eine Lücke im N01-Fix. Er zeigt Bilder, die noch nicht freigegeben sind, und cacht sie. - **Seit dem 28.09. live und deshalb dringlich:** B-03 (das Formular lehnt Dateien über 12 MiB ab, zugesagt sind 16 MiB), N09 (der Veröffentlichungsnachweis stimmt nach einer Korrektur nicht mehr) und B-04, B-05 und B-07 an der Bildfreigabe. - **SEO:** R-02/K-03 und R-03 wirken sich schon heute auf die Suche aus. Der Aufrufzähler setzt `updated_at` jeder aufgerufenen Meldung neu, das verfälscht Sitemap-`lastmod` und `dateModified`. Die Sitemap führt englische Meldungen unter `/de/`, auf der Dev-Kopie sind das 303 URLs mit 404. ### Bewertungsmaßstab | Kennzeichnung | Bedeutung | |---|---| | P1 | Vor weiterem Betrieb bzw. vor Aktivierung des betroffenen Ablaufs beheben | | P2 | Konkreter Fehler bei einem nachvollziehbaren Eingabe-, Zustands-, Fehler- oder Parallelitätsfall | | P3 | Klein, latent oder Hygiene | | OPT | Optimierung ohne Fehlerfolge | | Nachweis „geprüft" | Am Code in dieser Sitzung zweimal unabhängig gelesen oder per Abfrage belegt | | Nachweis „nachgestellt" | Mit einem temporären Test auf SQLite reproduziert (Test wieder entfernt) | | Nachweis „gelesen" | Einmal am Code belegt, nicht dynamisch reproduziert | --- ## 2. Vorgehen 1. Dokumentation gesichtet: Roadmap samt Anhängen, Protokoll Teil 1–11, AUDIT, Entwicklungsplan Blöcke 1–8, beide Umsetzungsaudits, Bildplan Fassung 2 mit Protokoll bis zum Live-Gang am 28.09., Runbook. 2. Qualitätsgates ausgeführt: Pest seriell und parallel, Larastan über den vollen Scope, `pint --test`, `schedule:list`, Migrationsliste, Abgleich `.env.example` mit den Config-Schlüsseln. 3. Code-Prüfung in vier parallelen, nur lesenden Durchgängen: - Bild-Pipeline - Archiv, Löschung und Anonymisierung - KI, Veröffentlichung, Rechte und Hintergrundjobs - Berichte, Feeds, Canonical und die Roadmap-Funktionen Jeder Durchgang hat die Block-8-Fixes gegengeprüft und neue Befunde gesucht. 4. Die P1-Befunde und die wichtigsten P2-Befunde habe ich danach selbst am Code nachgelesen, dazu gezielte Zählungen auf der Dev-Kopie. **Grenzen:** Es wurde nur auf SQLite getestet. MySQL-Sperrverhalten mit zwei Verbindungen wurde nicht dynamisch geprüft. Den Live-Server habe ich nicht gesehen; Betriebsangaben stammen aus dem Protokoll. Die Mengen stammen aus der Dev-Kopie und nicht aus Live. --- ## 3. Qualitätsgates | Prüfung | Ergebnis | Bemerkung | |---|---|---| | `php artisan test --compact` (seriell) | 2.122 grün, 1 übersprungen, 9.766 Assertions, 393 s | Deckt sich mit dem Protokoll nach Block 8 | | `php artisan test --parallel --processes=6` | 9 Fehlschläge | Q-01 | | Larastan Level 4, voller Scope | 1 Fehler | Q-02; das Protokoll sprach nur von „geänderten Dateien" | | `pint --test` | bestanden | | | Scheduler | 18 Tasks, alle vier Archiv-Tasks vorhanden | K-09, A-13 | | `.env.example` | alle neuen Schalter vorhanden außer `IMAGE_PRECHECK_MODEL` | P3 | - **Q-01 (P3):** `tests/Feature/PressReleaseWithdrawalWaiverTest.php` und `PressReleasePublishModalPhase8iTest.php` rufen `makeCustomerForShowPhase8a()` auf. Die Funktion ist in `tests/Feature/PressReleaseShowPhase8aTest.php:24` definiert. Einzeln oder parallel ausgeführt scheitern die beiden Dateien, seriell laufen sie nur zufällig in der richtigen Reihenfolge. **Fix:** Helfer nach `tests/Pest.php` oder in einen Support-Helfer verschieben. Danach die Suite parallel fahren (etwa 80 s statt 390 s). - **Q-02 (P3):** `app/Models/Concerns/HasUniqueSlug.php:90`: `@var Model $self` ist kein Subtyp von `$this`. **Fix:** Die Annotation entfernen und `static::query()` verwenden. - **Q-03 (P3):** Volt-Komponenten (`resources/views/livewire/**`) liegen außerhalb des PHPStan-Scopes. Genau dort sitzen mehrere der neuen Befunde (B-04, K-06, K-07). Die Baseline hat 143 Einträge. - **Q-04 (Hinweis):** `Storage::fake` teilt `storage/framework/testing/disks`. Laufen mehrere Testprozesse gleichzeitig, scheitern Bildtests sporadisch mit „No such file". Das ist kein Codefehler, erklärt aber sporadisch rote Läufe. --- ## 4. Verifikation der Block-8-Nacharbeit | ID | Stand | Beleg | Rest | |---|---|---|---| | N01 | **teilweise** | `PressReleaseImage.php:283-302`, `PressReleaseImageMediaController.php:33`, `ReleaseDetailPageData.php:43,110`, `HomePageData.php:62`, `CategoryPageData.php:100`, `NewsroomDetailPageData.php:153` | Livewire-Startseiten-Feed (B-01) und Nachweis-PDF (B-02) umgehen den Freigabezustand | | N02 | behoben | `EvidenceArchive.php:205-247, 410-437` | Nur mit einer Verbindung getestet | | N03 | behoben | `ClassifyPressRelease.php:111-173`, `ScorePressRelease` analog | – | | N04 | behoben | `PressReleaseService.php:264-310, 605-635` | Folgefall K-05 | | N05 | behoben | `PressReleasePolicy.php:15-24, 90-98`, `PressReleaseImageUploader.php:261-280` | Die Bestätigung ist nicht an das angezeigte Bild gebunden (B-04) | | N06 | **teilweise** | `InteractsWithAiDeclaration.php:43, 96-98` | `legalAcceptanceVersions` ist nicht `#[Locked]`; bei Versionswechsel wird still übersprungen (K-07) | | N07 | behoben (Upload) | `PressReleaseImageUploader.php:174` | Der Entfernen-Pfad hat dieselbe Lücke (B-05) | | N08 | behoben | `PressReleaseImageUploader.php:231-249, 314-330` | Das Muster fehlt in Korrektur und Sichtung (B-06) | | N09 | **offen** (geplant) | `PublishedImageSnapshot::capture()` unverändert | Siehe Abschnitt 5.1 – seit dem 28.09. live relevant | | N10 | behoben | `EvidenceArchive.php:289-319`, `FileDeletion.php` | Reconcile setzt den Tombstone auch bei gescheiterter Dateilöschung; der FAILURE-Exit erzwingt die Wiederholung | | N11 | **teilweise** | `EvidenceArchive.php:97-130`, `PendingArchiveRetry.php` | A-01, A-02, A-04, A-09 | | N12 | behoben | `AnonymizationService.php:161-217` | Restfälle A-10, A-12 | | N13 | technisch behoben | `PurgeExpiredPressReleaseDrafts.php:43-48` | Die Annahme im Protokoll ist falsch (A-05) | | N14 | behoben (unter E18) | `ExpireGrandfathering.php:174-200` | Atomar nur mit DB-Queue; ein Assert fehlt (OPT) | | N15 | behoben | `ViewDistributionReport.php:137-146, 176` | – | | N16 | behoben | `FeedFetchReport.php:37-43` | – | | N17 | **teilweise** | `VisibilityStats.php:170-186` | „Gelistet seit" zeigt `published_at` statt des Rollenwechsels; die EN-URL in der Sitemap führt ins Leere (R-03) | | N18 | behoben | `IndexStatusReport.php:73, 87-89, 217-245` | Eine Meldung auf beiden Portalen zählt auf dem nicht bevorzugten Portal als `source` | | N19 | **teilweise** | `CanonicalAssigner.php:143-159` | `archive()` (`PressReleaseService.php:647`) ruft `reassignCopiesOf()` nicht auf – **geprüft**; Merge, Unmerge und Textänderung bewerten ebenfalls nicht neu (R-06) | | N20 | behoben | `OpenAiImagePrecheckDriver::assertSchema()`, `ImagePrecheckResult::normalize()` | – | | N21 | behoben | `AggregatePressReleaseViews.php:89, 177-186`, `AppSetting.php:50-68` | – | | N22 | behoben | `tests/TestCase.php:31-54` | – | | 5.1 a | teilweise | `EvidenceArchive.php:444-469` | Nur beim regulären Purge, nicht beim Restore-Abgleich (A-06) | | 5.1 b | behoben | `ImageCorrectionService.php:110-115` | – | | 5.1 c | teilweise (E16) | Runbook `:291-308` | Nach einem DB-Restore gibt es eine neue Lücke (A-03) | | 5.1 d | teilweise | `PressReleaseArchiver.php:84-127` | Eine Depublikation aus dem Meldeverfahren trägt keine `legal_request_id` | | 5.1 e | weitgehend | `PressReleaseImageRemover.php:57-66, 93-111` | A-12 | | 5.2 Kategorienfeed, Dry-Run | behoben | `categories/edit`, `PurgePressReleaseEvents.php:80-85` | `create` leert die Navigation nicht (bis zu 1 h) | Die älteren Punkte U02, U03, U04 und U11–U13 sind am aktuellen Code weiterhin behoben. --- ## 5. Neue Befunde ### 5.1 P1 — vor weiterem Betrieb beheben #### R-01 · Legacy-API macht Einlieferer zu Mitgliedern fremder Firmen · geprüft `app/Services/Company/ApiNewsroomAssignment.php:65-94`, aufgerufen aus `LegacyCompanyController::store()` (`POST /service/users/me/companies`). - **Ablauf:** Stimmen die Firmen-E-Mail und die Namensregel überein, wird der Token-Nutzer ohne weitere Prüfung mit `role = member` an die bestehende Firma gehängt (`:92`). - **Folge:** Nach `PressReleasePolicy::canManage()` darf ein `member` alle Meldungen der Firma lesen, bearbeiten, einreichen und löschen, auch Entwürfe und Meldungen mit Sperrfrist. - **Szenario:** Firmenname und Kontaktadresse stehen öffentlich im Newsroom bzw. auf der Meldungsseite. Jeder Kunde mit API-Schreibrecht kann damit einen fremden Newsroom übernehmen. Auf der Dev-Kopie haben **413 Nutzer** ein Token mit `press-releases:write`. Nicht geprüft ist, wie viele davon den Legacy-Pfad tatsächlich erreichen. - **Fix:** - Die Zuordnung nur für freigeschaltete Einlieferer erlauben (Allowlist in `config/newsroom.php`, heute nur connektar) oder nur, wenn der Nutzer schon Owner oder Mitglied dieser Firma ist. - In allen anderen Fällen wie früher eine neue Firma anlegen. - Zuordnungen seit dem 15.09. über `company_user.created_at` und das Activity-Log („API-Firma … zugeordnet") prüfen und fremde Mitglieder entfernen. - **Test:** Ein Token eines fremden Nutzers mit passender Adresse und passendem Namen erzeugt keinen Pivot. #### K-04 · Vorab-Übernahme eines Kontos über den Google-Login · geprüft `app/Services/Auth/SocialAuthService.php:47-58`. - **Ablauf:** Ein unverifiziertes Konto mit derselben E-Mail wird beim Google-Login auf verifiziert und aktiv gesetzt. Passwort, `remember_token` und laufende Sessions bleiben dabei unverändert. - **Szenario:** - Ein Angreifer registriert `pressestelle@firma.de` mit eigenem Passwort; die Registrierung loggt direkt ein. - Die echte Pressestelle meldet sich später mit Google an. Das Konto wird aktiv. - Der Angreifer kommt per Passwort oder mit seiner Session hinein. - `SyncCompanyMembershipsOnLogin` vergibt dabei zusätzlich die Firmenrechte zu dieser Adresse. - **Fix:** - Bei der Verifikation über OAuth das Passwort auf `null` setzen, `remember_token` rotieren und fremde Sessions löschen. - Alternativ unverifizierte Selbstregistrierungen verwerfen und neu anlegen. - Zusätzlich unverifizierte Konten nach N Tagen löschen. - **Test:** Ein vorregistriertes Konto ist nach dem Google-Login nicht mehr per altem Passwort erreichbar. #### K-01 · Scheduler veröffentlicht Meldungen mit ausstehender manueller Prüfung · geprüft, nachgestellt `PublishScheduledPressReleases.php:66-76`: Die Abfrage filtert nicht auf `manual_review_required_at`. Auch `publish()` und `autoPublishApproved()` prüfen die Marke nicht. - **Szenario:** - Eine Meldung ist nach dem dritten automatischen Lauf gesperrt (E3). - Der Kunde darf sie im Status `review` weiter bearbeiten und `scheduled_at` setzen. - Die Bearbeitung löst einen Recheck ohne Routing aus, der `auto_check_attempts` nicht erhöht. - Bei Grün veröffentlicht der Scheduler, ohne dass ein Mensch entschieden hat. Der Kunde kann also so lange umformulieren, bis die KI Grün liefert. - Dasselbe gilt bei Gelb, wenn `yellow_to_manual_queue` eingeschaltet ist. - **Fix:** - `whereNull('manual_review_required_at')` in der Scheduler-Abfrage. - In `publish()` unter der Sperre bei den Quellen `ki` und `scheduler` `requiresManualReview()` prüfen und abbrechen. #### B-01 · Startseiten-Feed umgeht den Bild-Freigabezustand (Lücke in N01) · geprüft `resources/views/livewire/web/press-release-feed.blade.php:123-126` lädt `images` ohne `publiclyVisible()`. `PressRelease::cardImageUrl()` und `cardImageCredit()` (`:273-285`) nehmen das erste Bild. - **Szenario:** An einer veröffentlichten Meldung wartet ein Admin-Upload auf die Kundenbestätigung, oder ein Bild ist zurückgehalten. Der Feed rendert trotzdem ``, Nachweis, `alt` und `title`. - Gäste sehen ein kaputtes Bild statt des Platzhalters. - Eingeloggte Nutzer mit `view`-Recht sehen das nicht freigegebene Bild. - Das Ergebnis liegt im `WebPageCache`. - **Nebenbefund:** `cardImageUrl()` lädt ohne Vorladen alle Bilder lazy nach. - **Fix:** - `->publiclyVisible()` in `baseQuery()` ergänzen. - Die Card-Helfer am Modell lesen nur vorgeladene Relationen oder filtern selbst. - **Test:** Den Feed mit zurückgehaltenem und mit unbestätigtem Bild prüfen. ### 5.2 P2 — Bild-Pipeline (seit 28.09. live) | ID | Befund | Beleg | Szenario | Fix | Nachweis | |---|---|---|---|---|---| | **N09** | Veröffentlichungssnapshot veraltet oder nur scheinbar vorhanden | `PublishedImageSnapshot::capture()` `:47-49, 62-63, 79-83` | Dedup nur über Bildbytes: Nach einer Korrektur von Credit oder KI-Kennzeichnung bleibt das Sidecar-JSON alt. Fester Pfad, also keine Historie. `put()` ungeprüft (`throw=false`), der Pfad wird trotzdem gesetzt (Phantom-Snapshot). Die OG-Variante mit eingebranntem Nachweis ist nicht Teil des Snapshots. Zurückgehaltene Bilder bekommen `published_ever_at`. Der Aufruf in `publish()` ist ungesichert: wirft er, entfallen `assignCanonical` und die Autorenmail. | Snapshot versioniert (Bytes + Metadaten-Hash im Pfad), `put()` prüfen, OG-Variante einschließen, nur `isPubliclyDeliverable()`-Bilder erfassen, Aufruf nach dem N08-Muster absichern | gelesen | | B-02 | Nachweis-PDF nimmt das erste Bild ungefiltert | `ProofPdfRenderer.php:893-900` | Das gekaufte PDF zeigt ein zurückgehaltenes oder unbestätigtes Bild als veröffentlichtes Titelbild | `publiclyVisible()` | gelesen | | B-03 | Formular-Upload bricht bei 12 MiB ab | `config/livewire.php:68` `rules => null`, Livewire-Standard `max:12288` | Das Formular verspricht 16 MiB (E-B6); Dateien zwischen 12 und 16 MiB scheitern schon beim temporären Upload, die API nimmt sie an | `temporary_file_upload.rules = ['required','file','max:16384']`; auf Live `upload_max_filesize`, `post_max_size` und nginx `client_max_body_size` prüfen | geprüft | | B-04 | Kundenbestätigung nicht an das angezeigte Bild gebunden | `customer/press-releases/show.blade.php:124-160` | Der Kunde sieht Bild A, der Admin ersetzt es durch B, der Kunde klickt „bestätigen“: protokolliert wird B | `#[Locked]` Bild-ID und SHA beim Rendern setzen, beim Bestätigen vergleichen | gelesen | | B-05 | Entfernen ohne Sperre gegen gleichzeitiges Veröffentlichen | v2-Manager `:328-343`, API `destroy` `:102-128`, `PressReleaseImageRemover.php:41-67` | Veraltetes `published_ever_at = NULL` führt zur Sofortlöschung eines öffentlich gewesenen Bildes, ohne Archiveintrag | Im Remover PM und Bild mit `lockForUpdate` neu laden und die Regeln erneut prüfen (wie N07) | gelesen | | B-06 | Korrektur: Nacharbeiten nach dem Commit ungeschützt | `ImageCorrectionService.php:100-115` | Scheitert OG-Neubau, Snapshot oder Cache, fehlt der `label_corrected`-Eintrag dauerhaft; kein Nachholer | Archiv zuerst und mit Marker; OG, Snapshot und Cache wie N08 | gelesen | | B-07 | Vorab-Sichtung und Korrektur teilen sich `withheld_at` | `ImagePrecheckReviewService.php:31`, `ImageCorrectionService.php:48,55`, Admin `toggleWithheld()` | Ein Admin hält ein Bild wegen einer Beschwerde zurück, ein anderer gibt die Sichtung frei, und das Bild ist öffentlich | Gründe getrennt führen; `isPubliclyDeliverable()` prüft beide | gelesen | ### 5.3 P2 — Archiv und Löschung | ID | Befund | Beleg | Szenario | Fix | Nachweis | |---|---|---|---|---|---| | A-01 | Ohne `ARCHIVE_KEY` löscht der Nachholer die Marker und meldet SUCCESS | `PendingArchiveRetry.php:74-97`, `EvidenceArchive.php:71-78` | `record()` gibt `null` zurück, `attempt()` zählt das weder als Erfolg noch als Fehler, und bei `failed === 0` wird der Marker gelöscht. Nach einem `config:cache` ohne Schlüssel gehen alle offenen Nachweise still verloren. Der Systemstatus prüft den Schlüssel nicht. | Ohne `isConfigured()` mit FAILURE abbrechen; „übersprungen“ getrennt zählen; Systemstatus-Kachel für den Schlüssel | geprüft | | A-02 | Ein späterer Vorgang überschreibt oder löscht einen offenen Marker | `AnonymizationService.php:138,173`, `CorrectionService.php:178,254,322`, `PressReleaseService.php:813`, `PendingArchiveRetry.php:74` | Die Depublikation scheitert beim Archiv, der Marker steht auf T1. Die Anonymisierung setzt T2 und löscht ihn nach Erfolg. Das `pm_depublished` mit dem ungeschwärzten Stand entsteht nie. | Marker nur setzen, wenn er NULL ist; bedingt löschen (`WHERE archive_pending_at = ?`); besser eine Auftragstabelle je Ereignis | gelesen | | A-03 | Nach einem DB-Restore löscht `archive:sweep-orphans` echte Beweis-Payloads | `EvidenceArchive.php:380-403`, Scheduler 03:40, Runbook `:305-308` | Einträge nach dem Backup-Zeitpunkt verlieren ihre Zeile; ihre Payloads sind älter als 24 h und fallen in der nächsten Nacht. Bei Bildentfernungen ist das Original damit endgültig weg. | Runbook: Sweep bis nach dem Abgleich aussetzen; Wiederregistrierung aus `manifest.json` oder Quarantäne statt Löschung | gelesen | | A-04 | Ein nachgeholtes `pm_depublished` bildet einen späteren Stand ab | `PendingArchiveRetry.php:103-128`, `PressReleaseArchiver.php:84` | Archiviert wird der aktuelle Text, nach einer Bearbeitung oder Anonymisierung also nicht mehr der zuletzt veröffentlichte Stand; jeder Marker erzeugt ein `pm_depublished`. | Vorher-Stand bei der Depublikation persistieren; Nachholer je Ereignistyp | gelesen | | A-06 | Der Restore-Abgleich löscht die Anwendungskopien nicht erneut | `EvidenceArchive.php:345-350` | Bestätigungen, Korrekturen und weich gelöschte Bildzeilen kommen mit dem DB-Backup zurück und bleiben stehen (E13, DSGVO) | `purgeApplicationCopies()` auch im Abgleich | gelesen | ### 5.4 P2 — KI, Veröffentlichung und Jobs | ID | Befund | Beleg | Szenario | Fix | Nachweis | |---|---|---|---|---|---| | K-02 | Kein fachlicher Ausgang für Kontingent- und Bildsperre | `PublishScheduledPressReleases.php:127`, `ClassifyPressRelease.php:179-197` | Eine geplante Meldung mit erschöpftem Kontingent erzeugt alle 5 Minuten Exit 1 und eine Alarm-Mail (288 am Tag), der Kunde erfährt nichts. Ein Admin-Bildtausch in `review` lässt den KI-Job dreimal wiederholen (drei OpenAI-Aufrufe) und in `failed_jobs` enden. Nach der Kundenbestätigung wird die Meldung nicht erneut geroutet. | Beide Ausnahmen als fachlichen Ausgang behandeln (Exit 0, Marke, einmalige Mail); im Job abfangen; nach der Bildbestätigung erneut routen | nachgestellt | | R-02 / K-03 | Aufrufaggregation und Merge setzen `updated_at` | `AggregatePressReleaseViews.php:192-195` (Eloquent-`increment()` setzt `updated_at`), `CompanyMergeService.php:344` | Sitemap-`lastmod`, JSON-LD `dateModified` und `article:modified_time` melden bei jeder aufgerufenen Meldung eine Änderung. Auf der Dev-Kopie tragen 29.663 veröffentlichte Meldungen von vor 2026 ein `updated_at` nach dem 01.08. | `->toBase()->increment()`; für lastmod eine eigene Spalte `content_updated_at` bzw. `content_hash`-Zeitpunkt, weil `updated_at` nicht mehr rekonstruierbar ist | geprüft, nachgestellt | | R-03 | `sitemap.xml` führt englische Meldungen unter `/de/` | `SitemapController.php:163` | Die Detailseite filtert nach Sprache, deshalb 404. Auf der Dev-Kopie 303 URLs (311 veröffentlichte EN-Meldungen). Die News-Sitemap macht es richtig (`NewsSitemapController.php:74`). | Edition aus `language` ableiten, Test mit `language=en` | geprüft | | N19-Rest | `archive()` bewertet Kopien nicht neu | `PressReleaseService.php:647-661` | Der Admin archiviert die Erstquelle A. Kopie B hat Self-Canonical, bleibt aber aus der Sitemap. | `reassignCopiesOf()` wie in den anderen Pfaden, Test | geprüft | ### 5.5 P2 — Roadmap-Funktionen und Betrieb | ID | Befund | Beleg | Szenario | Fix | Nachweis | |---|---|---|---|---|---| | R-04 | Gefälschte Zustellberichte sperren beliebige Adressen | `DsnParser.php:32-66`, `BounceMailProcessor.php:57-90`, `EmailSuppressionService.php:130-161` | Jede Mail ans Return-Path-Postfach mit `multipart/report` und `Action: failed` führt zur Hard-Sperre. `X-Pk-Ref` ist optional und nicht signiert. `GateOutgoingMail` verwirft danach Magic Links, Resets und Rechnungen. | Hard-Sperre nur mit HMAC-signiertem `X-Pk-Ref`, das zum Empfänger passt; alles andere `soft` bzw. zur Sichtung | gelesen | | R-05 | Unmerge überschreibt spätere Kundenänderungen | `CompanyMergeService.php:432` | Der Merge füllt `description`, der Kunde beansprucht den Newsroom und schreibt neu, der Unmerge setzt NULL. Das widerspricht dem Docblock von `revert()`. | Nur zurücksetzen, wenn der aktuelle Wert noch der Füllwert ist | gelesen | | E9-Rest | Newsletter-Versand ist technisch nicht gesperrt | `admin/newsletter/*` → `CampaignSender::start` | E9 schiebt U05–U07 auf, bis das Kampagnenmodul genutzt wird. Jeder mit `users:manage` kann aber heute schon starten. U05–U07 sind unverändert offen. | Konfigurationsschalter `newsletter.sending_enabled` (Standard aus), bis U05–U07 umgesetzt sind | gelesen | ### 5.6 P3 — klein, latent, Hygiene **Bilder** - B-08: `PressReleaseImageUploader.php:208` setzt `published_ever_at` schon bei vorhandenem `published_at`, auch an Meldungen in `rejected` oder `draft`. Richtig wäre nur bei `status = published`. - B-09: Ein Admin, der selbst Eigentümer ist, lädt bei einer veröffentlichten Meldung als Kunde hoch; das Bild ist sofort öffentlich, ohne Snapshot. - B-10: Der Bestätigungslink ist ein voller Login-Link mit 72 h Gültigkeit. Jeder spätere Login-Link entwertet ihn, und der Kunde sieht „Link ungültig“. - B-11: Das Original wird vor der Verarbeitung geschrieben. Ein Fatal Error (memory_limit bei 8000², GD-Puffer ≈ 256 MB) hinterlässt eine Datei ohne Zeile. Für `press-release-originals` gibt es keinen Orphan-Sweep. - B-12: Der v2-Manager fängt nur `ImageUploadException`. Die `RuntimeException` aus `ImageService` (z. B. animiertes WebP) gibt im Formular einen 500er, in der API ein 422. - B-14: `confirmExisting()` läuft ohne Sperre; ein Doppelklick erzeugt zwei Bestätigungen. **Archiv** - A-05: Die Annahme „`published_ever_at` schützt den Altbestand“ trifft nicht zu. Auf der Dev-Kopie haben **126.383 abgelehnte Alt-Meldungen alle `published_ever_at = NULL`**; der Backfill kann „offline genommen“ nicht erkennen. `purge-drafts` greift heute nur deshalb nicht, weil diese Meldungen keine Bilder mehr haben (`whereHas('images')`). Die Protokollzeile vom 28.09. („Altbestand korrekt geschützt“) ist zu korrigieren. **Fix:** `whereNull('legacy_id')` im Zweig `rejected`; jede künftige Logik „nie veröffentlicht“ muss `legacy_id` berücksichtigen. - A-07: Beim Aufheben eines Legal Hold wird die Hold-Historie überschrieben. `archive_access_log.admin_id` steht auf `nullOnDelete`. - A-08: Der Zugriffszweck steht in der GET-URL und landet damit in den nginx-Logs. - A-09: `label_corrected` hat keinen Marker und keinen Nachholer. - A-10: Scheitert bei der Anonymisierung die Sofortlöschung eines nie veröffentlichten Bildes, bleibt das Bild dauerhaft liegen. Weder der Nachholer noch der Systemstatus erfassen das. - A-11: Lokale Restnachweise (`routes/admin.php:49-52`) sind für jeden Admin abrufbar, nicht nur mit `compliance:manage`. - A-12: Die Anonymisierung löscht Anhänge nur auf der Disk `public`, privatisierte Anhänge liegen aber auf `local`. Latent, weil es heute 0 Anhänge gibt. - A-13: `snapshots:purge` (30 Tage) löscht Snapshots auch bei offenem Marker. - A-14: Ein defektes Bild bricht den ganzen `purge-drafts`-Lauf ab. - A-15: Für `ARCHIVE_KEY` gibt es keine Schlüsselrotation und keine Schlüssel-ID je Eintrag. Der HMAC des Exports nutzt denselben Schlüssel wie die Verschlüsselung. - Verdacht: `PressReleaseArchiver.php:86` lädt `company` und `contacts` mit `PortalScope`. Aus einem Request mit Portal-Kontext könnten Firma oder Kontakte im Manifest fehlen. **KI, Rechte, Jobs** - K-05: Ein veralteter Status im KI-Job führt zu `LogicException` und einem Retry, also zweitem OpenAI-Aufruf und doppeltem Audit. **Fix:** vor dem Routing `refresh()`. - K-06: In `admin/press-releases/show.blade.php:32-190` rufen `publish`, `reject`, `archive`, `correctImage`, Precheck und `toggleWithheld` kein `authorize()` auf; Index- und Bearbeiten-Seite verlangen `press-releases:publish`. `publish` fängt `QuotaExceededException` bzw. `LogicException` nicht ab, das gibt einen 500er. `runAiCheck` lehnt bei Rot ab, obwohl der Kommentar „ändert nie den Status“ sagt. - K-07: N06-Rest, siehe Tabelle; zusätzlich wirft der Rendering-Hook bei aktivem Gate und fehlendem Dokument auf allen vier Seiten. - K-08: Die drei Login-Abschlüsse sind uneinheitlich (Plan 1.8 offen): - Nach der 2FA-Challenge wird kein `last_login_at` gesetzt. - Beim Magic Link mit 2FA geht das Landeziel verloren. - Google erzwingt `remember=true`. - `magic-login` läuft hinter `guest`: Ein eingeloggter Kunde landet über den Bildbestätigungslink nicht auf der Meldung. - K-09: `withoutOverlapping()` hält die Sperre standardmäßig 24 h. Nach einem Absturz wird `publish-scheduled` bzw. `aggregate-views` bis zu einen Tag still übersprungen. **Fix:** `withoutOverlapping(10)` bzw. `(15)`. `archive:purge` und `purge-events` starten beide um 03:20. Das Runbook nennt 6 von 18 Tasks. **Berichte und Roadmap** - R-06: Merge und Unmerge bewerten Canonicals nicht neu; `assignAll()` setzt veraltete Verweise nie zurück. - R-07: Crawler zählen als „Aufrufe“ (`ReleaseDetailPageData.php:90-100`), Crawler-Hits vertrauen allein dem User-Agent. - R-08: Der Mail-Klick wird synchron und ungedrosselt gezählt. Link-Scanner blähen Stufe 1 auf, und `?src=…&pr=…` ist frei manipulierbar. - R-09: Der Claim-Tageszähler je Firma (3) zählt vor der Empfängerprüfung. Dritte können den echten Inhaber 24 h aussperren. - R-10: `email_bounces` (Adressen, Diagnosetexte) hat keine Aufbewahrungsfrist. - R-11: - Die Canonical-Zuordnung hängt am DE-Slug der Kategorie; eine Slug-Änderung schaltet sie still ab. - `llms.txt` wird von `WebCaches` nicht geleert. - Der Feed-Teaser entschärft Entities doppelt (13 von 3.451 Meldungen). - `MergedNewsroomLookup` folgt keinen Merge-Ketten. **Repository** - Q-05: Der ganze Ordner `docs/` (36 versionierte Dateien) ist lokal gelöscht, aber nicht committet. Laufzeit ist nicht betroffen, weil die API-Doku unter `public/api/v1.yml` liegt. Offen ist die Entscheidung: Löschung committen oder wiederherstellen. Der Entwicklungsplan 6.3 hatte `docs/README.md` noch gepflegt. - Q-06: `dev/roadmap.md` (Stand 20.07.) und `dev/README.md` nennen die Ordner `dev/2026-09-*` nicht. Wer dort einsteigt, sieht einen veralteten Stand. - Q-07: `IMAGE_PRECHECK_MODEL` fehlt in `.env.example`. ### 5.7 Optimierungen (belegt) | Nr. | Stelle | Vorschlag | |---|---|---| | OPT-1 | `FeedFetchReport::topFeeds()`, `fetchesFor()` | In SQL gruppieren und limitieren, eine Abfrage statt einer je Feed | | OPT-2 | `press_release_events` (Dev 2,24 Mio. Zeilen in 75 Tagen, bei 395 Tagen ~11 Mio.) | Index auf `created_at` oder Grenz-ID bestimmen und nach `id` löschen | | OPT-3 | `ImageProvenanceInspector.php:156` | `pingImage()` statt `readImage()`; heute zweimal voll dekodiert (preflight + store) | | OPT-4 | Uploader | `hash_file` läuft bis zu dreimal auf denselben Bytes; Hash durchreichen | | OPT-5 | `PrecheckImagePreparer.php:37` | `readImage($path.'[0]')`, Imagick nach `mergeImageLayers` mit `clear()` freigeben | | OPT-6 | Archiv | `exportTombstones()`/`sweepOrphans()` auf `cursor()` bzw. Streaming; Index auf `archive_entries.payload_path`; Export per `->then()` an den Purge hängen | | OPT-7 | `InteractsWithAiDeclaration` | Rendering-Hook stellt 4 Abfragen je Render, auch bei ausgeschaltetem Gate; je Request zwischenspeichern | | OPT-8 | KI-Jobs | `backoff` setzen, damit 429-Antworten nicht sofort dreimal wiederholt werden | | OPT-9 | `ExpireGrandfathering` | E18 als Assert (`queue.default === 'database'`), statt nur als Kommentar | Hot-Path-Prüfung ohne Befund: View-, Crawler- und Feed-Erfassung schreiben per `defer()` nur in Append-Tabellen mit passenden Indexen. Kein Request schreibt auf `press_releases`. Die Feeds nutzen `Cache::flexible`. Der Sitemap-Aufbau dauert etwa 0,25 s, eine Stampede ist derzeit kein Risiko. Die Admin-Bilderliste ist ohne N+1. --- ## 6. Was offen ist — Stand der Dinge ### 6.1 Aus dem Audit-Entwicklungsplan (Blöcke 1–8) | Punkt | Stand | |---|---| | N09 Snapshot-Versionierung | offen, jetzt dringlicher (5.2) | | 1.8 Login-Abschlüsse als ein Service | offen, siehe K-08 | | 6.6 MySQL-/Worker-Integrationstests | aufgeschoben; sinnvoller Einstieg jetzt: N02, A-02, K-02 und ExpireGrandfathering mit zwei Verbindungen | | 7.6 Fokusführung Login-Modal, Cmd/Ctrl+K | manuelle Browserabnahme offen | | U05–U07 Newsletter | nach E9 aufgeschoben; Versand technisch nicht gesperrt (5.5) | | 5.2 Messpunkte | Zeitbudget des synchronen Bildchecks, Dekodieraufwand bei 8000², OG-Cache bei Korrektur – ungemessen | | F21 Legacy-Importer | Notiz bleibt; nur relevant bei erneutem Delta-Lauf | ### 6.2 Aus dem Bildkennzeichnungsplan | Punkt | Stand | Wartet auf | |---|---|---| | Deploy 3: AGB 0.7, DSE 0.4, `img-confirm-v3`, `LEGAL_ACCEPTANCE_GATE=true` | gebaut, aus | Anwaltsantwort, F.5 | | F.5 Interessenabwägung (DU 4.4) als Dokument | fehlt im Repo | Betreiber | | K.6 Vorab-Check einschalten (Stufe 1, nach einer Woche Stufe 2) | gebaut, `IMAGE_PRECHECK=false` | AVV mit OpenAI, DSE-Absatz; vorher Zeitbudget messen (bis ca. 60 s synchron) | | Block I C2PA (Deploy 4) | nicht begonnen | frühestens **26.10.2026**; `c2patool`, Testkorpus, Vertrauensliste | | Block J Newsroom-Bilder (Deploy 5, Roadmap 1b.3) | Unterbau polymorph vorbereitet | Kontingententscheidung | | E16 externes Tombstone-Ziel | nicht gebaut | Betreiber (Storage Box/S3) | | O5 Pro-Tarif ohne Prüfversprechen | offen | Betreiber | | S19 Löschanfragen-Handover | Dokument fehlt im Repo | Betreiber | | S23 Bildklausel im Syndikationsvertrag | offen | Anwaltstermin (Roadmap 3.5) | ### 6.3 Aus der Roadmap | Punkt | Stand | Vorarbeit im Code | |---|---|---| | 1c.5 / E8 Indexstatus für Kunden | wartet auf Daten; seit N18 ist die Verteilung belastbar auswertbar | `GSC_CUSTOMER_VISIBLE=false`, Bericht vorhanden | | 2.2 Google News Publisher Center | Technik fertig | Betreiber meldet an | | 2.6 Outreach | extern, in Arbeit | – | | 3.2 Branchen-Digest mit Double-Opt-in | offen | Sperrliste, Abmeldung, Circuit Breaker und Branchenfeed-Abfrage wiederverwendbar; keine Digest-/DOI-Tabelle. Voraussetzung: R-04 behoben | | 3.3 Agenturmodell | offen | Pivot mit Rollen, `CustomerCompanyContext`, Sammeladressen aus 1a.2; **erst R-01 beheben** | | 3.4 Agenturansprache, 3.5 Syndikationsvertrag | offen, nicht technisch | – | | 3.6 IMKIS | Treffen am **01.10.2026** | nur BALD-Partnerbereich (1d.6) | | Abbruchbedingungen Blöcke 1–3 | Platzhalter „[Zeitraum]/[Schwellwert]“ | Messgrößen vorhanden (`conversion:funnel`, Feed-Bericht); Werte festlegen | | 6 Slug-Kollisionen aus 1a.3 | offen | kein Werkzeug; nur von Hand per Slug-Wechsel und `companies:merge --email=` | | connektar `ai_declaration` verpflichtend | angekündigt für ~Oktober 2026 | prüfen, ob die API das Feld schon erzwingt bzw. ab wann | ### 6.4 Betrieb — auf Live zu bestätigen Diese Punkte stehen im Protokoll als offen oder sind aus dem Code nicht belegbar: 1. `.env`: `FEED_FETCHES_RECORDED_SINCE` und `CRAWLER_HITS_RECORDED_SINCE` auf `2026-09-21`, danach `config:cache` (Protokoll Teil 11). Ohne diese Werte nennt die Absenderansicht ein falsches Startdatum. 2. `livewire:publish --assets`, `queue:restart`, FPM-Neustart nach dem 21.09. 3. `company-memberships:audit --classify --before=…`: die Schlusszeile fehlte in der Ausgabe; bei Bedarf wiederholen. 4. User 19076: in Admin ▸ Zahlungen prüfen, ob die ersetzende MAN-Rechnung offen ist. 5. Bounce-Postfach `bounce@pressekonto.com`, `MAIL_RETURN_PATH` und `BOUNCE_IMAP_*` auf Live aktiv? Der Scheduler holt alle 10 Minuten ab. 6. `ARCHIVE_KEY` getrennt vom DB-Backup gesichert? Nach A-01 zusätzlich per Systemstatus sichtbar machen. 7. PHP-/nginx-Uploadgrenzen ≥ 16 MiB (B-03) und `memory_limit` gegen ein 8000×8000-Bild (B-11). 8. Restore-Übung (DB alt/Disk neu, Disk alt/DB neu, beide alt) wurde nie durchgespielt, siehe A-03 und A-06. 9. Nachlauf Go-Live aus `dev/roadmap.md` (20.07.), Stand unbekannt: Go-Live-Mails, API-Kunden-Mailing, Stripe-Testartefakte, `MONITORING_ALERT_EMAIL`, Umami, Rotation der Connektar-Keys, Abschaltung des KAS-Altsystems. --- ## 7. Empfohlene Reihenfolge — Block 9 | Schritt | Inhalt | Aufwand | Abnahme | |---|---|---|---| | 9.1 | **R-01, K-04** (Rechte und Konto), dazu Bestandsprüfung der API-Zuordnungen seit 15.09. | S | Kein fremder Pivot über die Legacy-API; ein vorregistriertes Konto ist nach Google-Login nicht mehr über das alte Passwort erreichbar | | 9.2 | **K-01, B-01, B-02, B-03** | S | Keine Veröffentlichung mit `manual_review_required_at`; Feed und PDF nur mit freigegebenen Bildern; Upload mit 15 MiB geht durch | | 9.3 | **R-02/K-03, R-03, N19-Rest** (SEO) | S–M | `updated_at` bleibt beim Aufruf unverändert; EN-Meldungen stehen unter `/en/`; `archive()` ordnet Kopien neu zu | | 9.4 | **A-01, A-02, A-03** (Beweisarchiv, Runbook) | M | Nachholer ohne Schlüssel endet mit FAILURE; Marker gehen nicht verloren; Runbook setzt den Sweep nach Restore aus | | 9.5 | **N09, B-04–B-07, A-04, A-06** (Bildnachweis vollständig) | M | Snapshot entspricht der veröffentlichten Kennzeichnung; Bestätigung an das angezeigte Bild gebunden; keine Sofortlöschung eines öffentlich gewesenen Bildes | | 9.6 | **K-02, R-04, R-05, E9-Schalter** | M | Kein Alarm-Loop; gefälschte DSNs sperren nicht; Unmerge erhält Kundenänderungen; Newsletter ohne Freigabe nicht startbar | | 9.7 | **Q-01, Q-02, Q-05, Q-06**, P3-Liste nach Gelegenheit | S | Suite parallel grün; Larastan voller Scope grün; `docs/` entschieden; `dev/README.md` und `dev/roadmap.md` verweisen auf den aktuellen Stand | Danach geht es fachlich weiter mit: Deploy 3 (sobald der Anwalt antwortet), K.6 (nach AVV), Block I ab 26.10., Roadmap 3.2 bzw. 3.3 je nach Ergebnis des IMKIS-Treffens am 01.10. und der Festlegung der Abbruchbedingungen. Arbeitsregeln wie bisher: zuerst der Test, der am heutigen Code rot ist, dann der Fix. Vor Testläufen `config:clear`. Am Blockende die Gesamtsuite, Larastan und Pint. Migrationen nie auf `press_release_search` und keine FULLTEXT-berührenden ALTERs auf großen Tabellen (CLAUDE.md).