presseportale/dev/archive/2026-09-audit/UMSETZUNGSAUDIT-2026-09-29.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

36 KiB
Raw Blame History

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, Umsetzungsaudit 25.09., Bildkennzeichnungsplan Fassung 2, Decision-Update 22.09., Roadmap mit Anhängen 1 und 2, Umsetzungsprotokoll Umsetzung: Am selben Tag als Block 9 umgesetzt — Stand, Entscheidungen E19–E34 und Live-Schritte im Entwicklungsplan. 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 <img>, 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).