- 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>
36 KiB
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:
- R-01: Über die Legacy-API wird der Einlieferer als
membereiner fremden Firma angelegt. Damit bekommt er Schreibrecht auf alle Meldungen dieser Firma. - 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.
- K-01: Der Scheduler veröffentlicht Meldungen, die nach E3 auf eine manuelle Prüfung warten.
- 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.
- R-01: Über die Legacy-API wird der Einlieferer als
- 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_atjeder aufgerufenen Meldung neu, das verfälscht Sitemap-lastmodunddateModified. 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
-
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.
-
Qualitätsgates ausgeführt: Pest seriell und parallel, Larastan über den vollen Scope,
pint --test,schedule:list, Migrationsliste, Abgleich.env.examplemit den Config-Schlüsseln. -
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.
-
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.phpundPressReleasePublishModalPhase8iTest.phprufenmakeCustomerForShowPhase8a()auf. Die Funktion ist intests/Feature/PressReleaseShowPhase8aTest.php:24definiert. Einzeln oder parallel ausgeführt scheitern die beiden Dateien, seriell laufen sie nur zufällig in der richtigen Reihenfolge. Fix: Helfer nachtests/Pest.phpoder 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 $selfist kein Subtyp von$this. Fix: Die Annotation entfernen undstatic::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::faketeiltstorage/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 = memberan die bestehende Firma gehängt (:92). - Folge: Nach
PressReleasePolicy::canManage()darf einmemberalle 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_atund das Activity-Log („API-Firma … zugeordnet") prüfen und fremde Mitglieder entfernen.
- Die Zuordnung nur für freigeschaltete Einlieferer erlauben (Allowlist in
- 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_tokenund laufende Sessions bleiben dabei unverändert. - Szenario:
- Ein Angreifer registriert
pressestelle@firma.demit 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.
SyncCompanyMembershipsOnLoginvergibt dabei zusätzlich die Firmenrechte zu dieser Adresse.
- Ein Angreifer registriert
- Fix:
- Bei der Verifikation über OAuth das Passwort auf
nullsetzen,remember_tokenrotieren und fremde Sessions löschen. - Alternativ unverifizierte Selbstregistrierungen verwerfen und neu anlegen.
- Zusätzlich unverifizierte Konten nach N Tagen löschen.
- Bei der Verifikation über OAuth das Passwort auf
- 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
reviewweiter bearbeiten undscheduled_atsetzen. - Die Bearbeitung löst einen Recheck ohne Routing aus, der
auto_check_attemptsnicht 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_queueeingeschaltet ist.
- Fix:
whereNull('manual_review_required_at')in der Scheduler-Abfrage.- In
publish()unter der Sperre bei den QuellenkiundschedulerrequiresManualReview()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,altundtitle.- 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()inbaseQuery()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:208setztpublished_ever_atschon bei vorhandenempublished_at, auch an Meldungen inrejectedoderdraft. Richtig wäre nur beistatus = 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-originalsgibt es keinen Orphan-Sweep. - B-12: Der v2-Manager fängt nur
ImageUploadException. DieRuntimeExceptionausImageService(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_atschützt den Altbestand“ trifft nicht zu. Auf der Dev-Kopie haben 126.383 abgelehnte Alt-Meldungen allepublished_ever_at = NULL; der Backfill kann „offline genommen“ nicht erkennen.purge-draftsgreift 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 Zweigrejected; jede künftige Logik „nie veröffentlicht“ musslegacy_idberücksichtigen. - A-07: Beim Aufheben eines Legal Hold wird die Hold-Historie überschrieben.
archive_access_log.admin_idsteht aufnullOnDelete. - A-08: Der Zugriffszweck steht in der GET-URL und landet damit in den nginx-Logs.
- A-09:
label_correctedhat 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 mitcompliance:manage. - A-12: Die Anonymisierung löscht Anhänge nur auf der Disk
public, privatisierte Anhänge liegen aber auflocal. 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_KEYgibt 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:86lädtcompanyundcontactsmitPortalScope. 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
LogicExceptionund einem Retry, also zweitem OpenAI-Aufruf und doppeltem Audit. Fix: vor dem Routingrefresh(). - K-06: In
admin/press-releases/show.blade.php:32-190rufenpublish,reject,archive,correctImage, Precheck undtoggleWithheldkeinauthorize()auf; Index- und Bearbeiten-Seite verlangenpress-releases:publish.publishfängtQuotaExceededExceptionbzw.LogicExceptionnicht ab, das gibt einen 500er.runAiChecklehnt 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_atgesetzt. - Beim Magic Link mit 2FA geht das Landeziel verloren.
- Google erzwingt
remember=true. magic-loginläuft hinterguest: Ein eingeloggter Kunde landet über den Bildbestätigungslink nicht auf der Meldung.
- Nach der 2FA-Challenge wird kein
- K-09:
withoutOverlapping()hält die Sperre standardmäßig 24 h. Nach einem Absturz wirdpublish-scheduledbzw.aggregate-viewsbis zu einen Tag still übersprungen. Fix:withoutOverlapping(10)bzw.(15).archive:purgeundpurge-eventsstarten 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.txtwird vonWebCachesnicht geleert.- Der Feed-Teaser entschärft Entities doppelt (13 von 3.451 Meldungen).
MergedNewsroomLookupfolgt 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 unterpublic/api/v1.ymlliegt. Offen ist die Entscheidung: Löschung committen oder wiederherstellen. Der Entwicklungsplan 6.3 hattedocs/README.mdnoch gepflegt. - Q-06:
dev/roadmap.md(Stand 20.07.) unddev/README.mdnennen die Ordnerdev/2026-09-*nicht. Wer dort einsteigt, sieht einen veralteten Stand. - Q-07:
IMAGE_PRECHECK_MODELfehlt 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:
.env:FEED_FETCHES_RECORDED_SINCEundCRAWLER_HITS_RECORDED_SINCEauf2026-09-21, danachconfig:cache(Protokoll Teil 11). Ohne diese Werte nennt die Absenderansicht ein falsches Startdatum.livewire:publish --assets,queue:restart, FPM-Neustart nach dem 21.09.company-memberships:audit --classify --before=…: die Schlusszeile fehlte in der Ausgabe; bei Bedarf wiederholen.- User 19076: in Admin ▸ Zahlungen prüfen, ob die ersetzende MAN-Rechnung offen ist.
- Bounce-Postfach
bounce@pressekonto.com,MAIL_RETURN_PATHundBOUNCE_IMAP_*auf Live aktiv? Der Scheduler holt alle 10 Minuten ab. ARCHIVE_KEYgetrennt vom DB-Backup gesichert? Nach A-01 zusätzlich per Systemstatus sichtbar machen.- PHP-/nginx-Uploadgrenzen ≥ 16 MiB (B-03) und
memory_limitgegen ein 8000×8000-Bild (B-11). - Restore-Übung (DB alt/Disk neu, Disk alt/DB neu, beide alt) wurde nie durchgespielt, siehe A-03 und A-06.
- 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).