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

51 KiB
Raw Permalink Blame History

Umsetzungs-Audit zum Entwicklungsplan vom 18.09.2026

Prüfdatum: 18.09.2026

Geprüfter Code: fb03fb166d08fe891ae5ab3a221c8a232b7c8366

Vergleich: 24c1bbf..fb03fb1, insbesondere die fünf Umsetzungscommits 0ab53bf, a7f991f, 62202a4, f235156, 375f4df und die nachfolgenden Testkorrekturen.

Grundlagen: Entwicklungsplan einschließlich Umsetzungsprotokoll, ursprünglicher Audit.

Auftrag: Umsetzungen prüfen und dokumentieren. Keine Änderungen an Anwendung, Tests, Konfiguration, Datenbestand oder bestehenden Dokumenten.

1. Ergebnis und Einordnung

Die Umsetzung behebt zahlreiche konkrete Fehler des ersten Audits. Besonders nachvollziehbar sind die Abschaltung des unsicheren Fortify-Profilendpunkts, die verifizierungsgebundene Rechtevergabe, die Korrektur der Newsletter-Empfängersortierung, die gezielten Soft-Delete-Filter, die Prüfung fehlgeschlagener Storage-Schreibvorgänge und das nun tatsächlich prüfende Formatierungs-Gate.

Eine vollständige Abnahme des Entwicklungsplans ist dennoch nicht gerechtfertigt. Einige Arbeitspakete erfüllen zwar ihren lokalen Test, aber noch nicht das fachliche Ziel über den gesamten Ablauf. Die wichtigsten Beispiele:

  • Die Rückstufung auf member entzieht Firmenstammdatenrechte, lässt aber Schreibrechte auf Pressemitteilungen bestehen.
  • Die KI-Ergebnisse tragen jetzt einen Hash. Der entscheidende Veröffentlichungsschritt prüft diesen unter seiner Datenbanksperre jedoch weiterhin nicht.
  • Die Newsletter-Wiederaufnahme scheitert am regulären Endzustand einer vollständig fehlgeschlagenen Kampagne; ein harter Abbruch beim Aufbau der Empfängerliste lässt weiterhin Empfänger aus.
  • Der Übergang bestehender Kontingentzähler auf die neue Monatsspalte ist nicht konsistent.
  • Der neue Vertragsablauf-Command hat eigene Parallelitäts- und Benachrichtigungslücken.

Dieser Bericht enthält 13 priorisierte Rest- beziehungsweise Folgebefunde: 2 × P1 und 11 × P2. Davon ist U13 ausdrücklich an eine Laufzeit von mehr als zehn Minuten gebunden. Hinzu kommen getrennt gekennzeichnete Betriebsnachweise und kleinere Verbesserungen. Die Befunde sind anhand des Codes belegt; ihre Reproduktionsabläufe wurden in diesem Audit wegen der nicht verfügbaren Sail-Laufzeit nicht dynamisch ausgeführt. Es wird kein bereits eingetretener Produktionsschaden behauptet.

Bewertungsmaßstab

Kennzeichnung Bedeutung
P1 Vor fachlicher Abnahme des betroffenen Sicherheits- oder Veröffentlichungsablaufs beheben.
P2 Konkreter Fehler bei einem nachvollziehbaren Eingabe-, Zustands-, Fehler- oder Parallelitätsfall; gezielt nacharbeiten.
Im Code umgesetzt Änderung und passende vorhandene Tests sind nachvollziehbar; kein unabhängig bestätigter grüner Lauf dieses Audits.
Teilweise Wesentliche Korrektur vorhanden, fachliches Ziel oder Fehlerfall bleibt offen.
Betriebsnachweis offen Codeprüfung reicht nicht aus, um Migration, Worker-Neustart oder Produktivwirkung zu bestätigen.

Die Prioritäten beziehen sich auf diese Nachprüfung. Sie bedeuten insbesondere bei P1 nicht automatisch, dass ein unangemeldeter Internetnutzer den Fehler ausnutzen kann.

2. Vorgehen in abgeschlossenen Prüfschritten

  1. Bestand und Nachweise: Entwicklungsplan, Entscheidungen E1–E6, Commitfolge, Arbeitsverzeichnis und Umsetzungsprotokoll abgeglichen.
  2. Identität und Berechtigungen: Fortify-Routen, Google-Anmeldung, Livewire-Sicherheitsaktionen, Firmenzuordnungen, Policies und Admin-Grant-Actions verfolgt.
  3. Newsletter: Auswahl, Migration, Kampagnenstart, Versandclaim, Abschluss, Fehlerbehandlung und Wiederaufnahme als zusammenhängenden Zustandsablauf geprüft.
  4. Öffentliche Daten und API: Soft Deletes, Vorschau/Medien, Kontakt-Fallback, Newsroom-Cache, URL-Auflösung, API-Validierung und Storage-Reihenfolge geprüft.
  5. KI und Kontingent: Dispatch, Analyse, Hashvergleich, Ergebnispersistenz, Veröffentlichungslock, Re-Checks, Monatswechsel und Übergangsdaten geprüft.
  6. Abrechnung und Scheduler: Vertragsende, Ablaufbenachrichtigung, Rechnungsauswahl, Hold-Markierung, Queue-Zeitbudgets und Aggregationssperre geprüft.
  7. Entwicklungsqualität: Ergänzte Tests, CI-Gates, PHPStan-Scope, Entwicklungsstart, Dokumentation und Fokusführung gelesen.
  8. Betriebsabgleich: Ausschließlich lesende Boost-Datenbankabfragen zu Migrationen, neuen Spalten/Indizes, Newsletter-Dubletten und vorhandenen Kontingentzählern durchgeführt.

Die Backend-Prüfung von Newsletter, Abrechnung und Aggregation wurde zusätzlich separat gegengelesen. Für Framework-Verhalten wurden auch die tatsächlich installierten Laravel-/Fortify-Quelldateien herangezogen.

Grenzen der Verifikation

Der Entwicklungsplan dokumentiert 1.891 erfolgreiche Tests, einen übersprungenen Test und PHPStan ohne Meldung. Das ist ein vorhandener Entwicklungsnachweis, kein in diesem Audit wiederholtes Ergebnis.

vendor/bin/sail ps meldete, dass Docker oder Podman nicht läuft. Der gezielte Versuch

vendor/bin/sail artisan test --compact tests/Feature/Auth/ContactAccessTest.php tests/Feature/Admin/NewsletterCampaignResilienceTest.php tests/Feature/PressReleaseQuotaTest.php

endete ebenfalls vor dem Teststart mit Exit-Code 1 und Docker or Podman is not running. Es wurden deshalb keine Tests als bestanden gewertet, keine Umgehung über direktes PHP verwendet und keine Testdateien ergänzt. Auch PHPStan, Browserabläufe und echte parallele MySQL-/Queue-Prozesse wurden nicht neu ausgeführt.

Die Testkonfiguration erzwingt SQLite :memory: und verwendet eine synchrone Queue. Das schützt den regulären Testlauf vor Zugriff auf den Anwendungsdatenbestand, beweist aber keine MySQL-Sperrsemantik und keinen Worker-Abbruch. Die im Plan bewusst verschobene zweite CI-Stufe wird nicht nachträglich als unerfüllte ursprüngliche Zusage behandelt. Für die jetzt konkreten Restfehler ist sie allerdings besonders relevant.

3. Abgleich sämtlicher ursprünglicher Befunde

Alt-ID Ergebnis dieser Nachprüfung Begründung / verbleibender Punkt
F01 Einstieg im Code geschlossen; Bestandsbereinigung offen updateProfileInformation deaktiviert, Action/Registrierung entfernt, Sync verlangt aktives verifiziertes Konto. Vorhandene unberechtigte Zuordnungen verschwinden dadurch nicht automatisch.
F02 Sicherheitszweig umgesetzt Google übergibt bestätigte 2FA an die Challenge und authentifiziert vorher nicht. Geplante Zusammenführung der Login-Abschlüsse in einen Service fehlt.
F03 Teilweise #[Locked] und Nachweis in den Volt-Actions vorhanden; alternative Fortify-2FA-Routen bleiben bei Passwortbestätigung, siehe U09.
F04 Geprüfte Zielpfade korrigiert PortalScope wird gezielt entfernt; SoftDeletes bleiben wirksam. Kontakt-Fallback und Pressefach korrigiert, Vorschautest verwendet nun echtes Soft Delete. Whitelist bleibt eine bewusst begrenzte Absicherung.
F05 Auswahlfehler korrigiert ID-Sortierung und Test über 1.001 Empfänger vorhanden. Bereinigungsmigration muss Zustellzustände beachten, siehe U08.
F06 Kernregel umgesetzt; neuer Ablauf nicht vollständig robust Vertragsende in Auswahl und gesperrter Rechnungsprüfung berücksichtigt. Neue Fehler im Ablauf-Command: U11/U12.
F07 Teilweise Hashübergabe, Verwerfen und Audit vorhanden. Letzte atomare Schranke fehlt; roter Re-Check kann hängen bleiben, siehe U02/U03.
F08 Im Code umgesetzt Nutzerdetailroute besitzt Permission-Middleware; beide Grant-Actions autorisieren erneut.
F09 Geplante Panel-Sperre umgesetzt Globaler Superadmin-Kurzschluss verlangt is_active; Panel-Test vorhanden. Eigenständige Policy-Kurzschlüsse bleiben als Härtungspunkt.
F10 Teilweise Herkunft und Rückstufung vorhanden; member ist für Pressemitteilungen weiterhin schreibberechtigt, siehe U01.
F11 Teilweise Listenparameter und Sprache validiert, Kategorien beim Anlegen geprüft. Portalwechsel ohne neue Kategorie umgeht die Zuordnungsprüfung, siehe U10.
F12 Im Code umgesetzt Gemeinsamer Resolver prüft Host, Portal, Sprache und Sichtbarkeit; passende Mehrportal-/Sprachtests vorhanden.
F13 Im Code umgesetzt Firmen-Observer, vorgelagerte Sichtbarkeitsprüfung und Pressefach-Kontakt-Flush vorhanden; Tests mit warmem Cache ergänzt.
F14 Teilweise Atomarer Start und Unique-Schutz vorhanden; harter Abbruch beim Empfängeraufbau nicht vollständig wiederaufnehmbar, siehe U06.
F15 Teilweise Empfängerclaim, transaktionale Zähler und Suppressed-Zustand vorhanden; Abschluss/Resume und Fehler nach SMTP bleiben problematisch, siehe U05/U07.
F16 Im Code umgesetzt Nicht abrechenbare Fälle blockieren die erste Seite nicht mehr; Regression mit 50 Blockern vorhanden. Es bleibt ein bewusstes Limit von 500 erstellten Rechnungen je Lauf.
F17 Defaults korrigiert Klassifikation und Score haben 120 s Jobtimeout; Database/Redis standardmäßig 150 s retry_after. Wirksame ENV-Werte und Worker-Neustart gesondert prüfen.
F18 Teilweise Monatsbindung und idempotenter Reset für explizite Perioden vorhanden; NULL-Altbestand inkonsistent, siehe U04.
F19 Für normale Überlappungen korrigiert Gemeinsamer Lock im Command schützt auch manuelle Aufrufe. Schutz endet nach 600 s, siehe U13.
F20 Im Code umgesetzt Technische Publish-Fehler führen zu FAILURE; Test für Exit-Code vorhanden.
F21 Bewusst ausgeschlossen Stillgelegter Legacy-Importer entsprechend Plan nicht nachbearbeitet. Keine negative Bewertung für diesen ausdrücklich verschobenen Bereich.
F22 Code und Migration vorhanden Indexfreundliche MySQL-Abfragen, SQLite-Fallback und Kontaktindex-Migration. Neuer Index fehlt in der hier erreichbaren Datenbank noch.
F23 Im Code umgesetzt put(false) wird erkannt; Logo erst schreiben, dann Referenz ändern, dann alte Datei löschen. Fehlerfälle getestet.
F24 Im Code umgesetzt CI verwendet pint --test, Schreibpermission entfernt.
F25 Gezielte Tests erweitert; breite Matrix bewusst offen Bessere Regressionen vorhanden. MySQL-/Worker-Matrix bleibt vereinbarungsgemäß verschoben; aktuelle Restfehler zeigen konkrete Ergänzungen auf.
F26 Fokusmechanik im Code vorhanden Öffnen, Tab-Grenzen und Fokusrückgabe implementiert; Browser-/Tastaturnachweis fehlt.
F27 Im Code umgesetzt Composer-Entwicklungsstart verwendet npm run dev:all.
F28 Geplante Dokuänderung umgesetzt Historischer Stand kenntlich, Einstieg und Archivpfad korrigiert. Kleinere Widersprüche im neuen Umsetzungsprotokoll siehe Abschnitt 7.

4. Priorisierte Rest- und Folgebefunde

U01 — P1: Rückstufung auf member entzieht nicht alle Schreibrechte

Bezug: F10, Plan 1.5, Entscheidung E2.

Belege: app/Services/Auth/ContactAccessService.php:148, app/Models/User.php:588, app/Policies/PressReleasePolicy.php:36, :101; resources/views/livewire/customer/press-releases/show.blade.php:379.

Der Sync senkt automatische responsible-Pivots korrekt auf member. CompanyPolicy::update() verweigert diesem Mitglied danach die Änderung der Firma. PressReleasePolicy::canManage() fragt jedoch nur nach Autorenschaft oder canAccessCompany(). Letzteres berücksichtigt jede Mitgliedschaft ohne Rollenfilter.

Damit darf der herabgestufte Kontakt weiterhin fremde Firmen-PMs im Status Draft, Rejected oder Review bearbeiten; je nach Status auch einreichen oder löschen. Bei veröffentlichten PMs gibt es einen weiteren konkreten Schreibweg: applyCorrection() autorisiert nur view, das ebenfalls jedem Firmenmitglied erlaubt ist, und übergibt die Änderung anschließend an den Korrekturservice.

Nachvollziehbarer Ablauf: Automatische Verantwortlichkeit vergeben → Firmen-/Kontaktadresse wechseln → erneut anmelden → Pivot ist member → eine PM eines anderen Autors derselben Firma bearbeiten beziehungsweise eine zulässige kleine Korrektur einer veröffentlichten PM anwenden. Der Firmenstammdatentest ist dabei weiterhin grün.

Testlücke: ContactAccessTest prüft nach dem Entzug nur update/view auf der Company, nicht auf ihren PMs und nicht auf Korrektur-Actions.

Empfehlung: E2 beibehalten, aber Lese- und Verwaltungsberechtigungen über alle PM-/Medien-/Korrekturaktionen trennen. Schreibrechte müssen auf einer ausdrücklichen Verwaltungsrolle oder einer bewusst definierten Autorberechtigung beruhen. Für den Entzugstest einen Nutzer wählen, der nicht PM-Autor ist.

Abnahme: Nach Rückstufung bleibt das gewünschte Lesen möglich; PM-Änderung, Einreichung, Löschung und Korrektur einer fremden Firmen-PM werden serverseitig verweigert.

U02 — P1: KI-Hashprüfung ist nicht mit Ergebnisspeicherung und Veröffentlichung atomar verbunden

Bezug: F07, Plan 4.1/4.2.

Belege: app/Jobs/ClassifyPressRelease.php:75–123, app/Jobs/ScorePressRelease.php:56–95, app/Services/PressRelease/PressReleaseService.php:275–305, app/Console/Commands/PublishScheduledPressReleases.php:103, :144.

Beide Jobs lesen den aktuellen Hash separat und speichern danach ohne gemeinsame Sperre ihr Ergebnis. publish() lädt zwar die PM mit lockForUpdate(), prüft dort jedoch nur Status, Blacklist und Kontingent. Der im Plan ausdrücklich geforderte Vergleich des freigebenden KI-Ergebnisses mit dem gesperrten aktuellen Inhalt fehlt.

Ablauf: Job bewertet Version A → separater Hash-SELECT sieht A → ein Editor speichert Version B → Job speichert die grüne Klassifikation von A → publish() lädt B unter Sperre und veröffentlicht B. Die Blacklist ersetzt keine Klassifikationsprüfung. Auch zwischen Scheduler-Prüfung und publish() besteht dieses Fenster. Beim Score kann entsprechend eine alte Bewertung auf einer neueren Version landen.

Ein zweiter Grenzfall liegt im Scheduler: Fehlt ein Audit-Hash, gilt die Klassifikation ausdrücklich als verwendbar. Der neue Test sichert dieses Legacy-Verhalten sogar ab. Eine später geänderte alte PM ist damit nicht über diesen Schutz abgesichert, solange noch kein neuer gültiger Audit vorliegt. Eine dokumentierte Übergangsregel muss unterscheiden zwischen unverändertem Altbestand und seitdem geändertem Inhalt.

Testlücke: Die neuen Tests ändern den Inhalt während des gemockten Provider-Aufrufs. Sie beweisen den Vergleich vor dem späteren Schreibfenster, nicht das atomare Verhalten zwischen Hashvergleich und Commit beziehungsweise zwischen Scheduler und Publish-Lock.

Empfehlung: Den tatsächlichen Analyse-Snapshot eindeutig versionieren. Ergebnisübernahme und gültige Versionszuordnung mit bedingtem Update oder kurzer Transaktion absichern. Vor automatischer Veröffentlichung dieselbe Version unter dem bestehenden PM-Lock zwingend prüfen. Eine bewusste manuelle Übersteuerung separat und nachvollziehbar modellieren. Den langsamen KI-Aufruf nicht innerhalb der Datenbanksperre ausführen.

Abnahme: Eine Änderung unmittelbar nach dem bisherigen Hash-SELECT verhindert die Speicherung eines gültigen alten Ergebnisses und die automatische Veröffentlichung der neuen Version. Score und Audit bleiben konsistent. Derselbe Fall muss über den Scheduler abgesichert sein.

U03 — P2: Roter Re-Check einer terminierten PM kann weiterhin still in review hängen bleiben

Bezug: F07, Plan 4.2.

Belege: app/Services/PressRelease/PressReleaseService.php:136–147, :169–178; app/Jobs/ClassifyPressRelease.php:118–123; app/Console/Commands/PublishScheduledPressReleases.php:59–65.

Inhaltsänderungen einer bereits klassifizierten PM planen über reclassifyIfClassified() einen Job mit route: false ein. Wird eine zukünftige grüne PM bearbeitet und dieser Re-Check rot, speichert der Job Rot und kehrt ohne Statusrouting oder Benachrichtigung zurück. Der Scheduler wählt später ausschließlich Grün/Gelb. Die PM bleibt deshalb als rote terminierte Meldung in Review stehen.

Der neue Scheduler-Recheck mit route: true hilft nur, wenn die PM noch als Grün/Gelb ausgewählt wird und ihr gespeicherter Audit-Hash nicht passt. Ein bereits vollständig ausgeführter roter Editor-Recheck erreicht diesen Zweig nicht.

Empfehlung: Für noch nicht veröffentlichte terminierte PMs eine ausdrücklich definierte Recheck-Folge einführen: Ablehnung oder manuelle Prüfung mit nachvollziehbarer Information an Kunde und Admin. Bei bereits veröffentlichten PMs kann eine andere Regel gelten.

Abnahme: Grün und terminiert → reguläre Editoränderung → rotes Ergebnis des regulären route:false-Rechecks → erwarteter Status und Benachrichtigungen. Nicht nur einen isoliert vom Scheduler gestarteten Job testen.

U04 — P2: Neue Kontingentperiode behandelt NULL-Altbestand widersprüchlich

Bezug: F18, Plan 4.3.

Belege: database/migrations/2026_09_18_170100_add_quota_period_to_users_table.php:23–28, app/Models/User.php:293–299, app/Services/PressRelease/PressReleaseService.php:365–374, app/Console/Commands/ResetMonthlyPressReleaseQuota.php:40–48.

Die Migration ergänzt eine nullable Monatsspalte ohne Bestandsinitialisierung. Beim Lesen gilt ein positiver Zähler mit period = NULL weiterhin als verbraucht. Beim nächsten erfolgreichen Planverbrauch wird derselbe Zähler dagegen auf 1 gesetzt, weil NULL nicht dem aktuellen Monat entspricht.

Konkretes Beispiel: Tarif mit 3 Slots, bisher 2 verbraucht, neue Spalte NULL. Anzeige zunächst 1 verbleibend. Nach einer weiteren Veröffentlichung stehen Verbrauch 1 und Rest 2 statt Verbrauch 3 und Rest 0. Umgekehrt bleibt ein ausgeschöpfter NULL-Zähler nach einem Monatswechsel ohne Cron ausgeschöpft, weil der Leseweg NULL nicht als Vorperiode erkennt.

Das ist kein bloß hypothetischer Datentyp: In der über Boost erreichbaren Datenbank bestehen zwei Nutzer mit positivem Verbrauch, maximal 2; die neue Monatsspalte fehlt dort noch. Ob diese Konten aktuell einen begrenzten Tarif haben, wurde nicht festgestellt.

Testlücke: Die neuen Monatswechseltests verwenden explizit 2026-08 und 2026-09. Ein positiver NULL-Altzähler mit noch freiem Slot wird nicht bis zum nächsten Publish durchgespielt.

Empfehlung: Eine einmalige, fachlich begründete Bestandszuordnung zum Migrationsmonat festlegen und Lese-, Verbrauchs- und Resetlogik auf dieselbe NULL-Semantik bringen. Falls unbekannter Altverbrauch bewusst verworfen werden soll, muss auch Anzeige und Entscheidung vor dem Verbrauch dazu passen.

Abnahme: Migration mit positivem Altverbrauch, erste Veröffentlichung im selben Monat sowie erster Monatswechsel ohne Reset erhalten das vereinbarte Kontingent korrekt.

U05 — P2: Vollständig fehlgeschlagene Newsletter-Kampagne lässt sich nicht mit --failed wiederaufnehmen

Bezug: F15, Plan 2.6.

Belege: app/Jobs/SendNewsletterCampaignMail.php:180–191, app/Console/Commands/ResumeNewsletterCampaign.php:42–45; tests/Feature/Admin/NewsletterCampaignResilienceTest.php:268, :338.

Sobald kein Empfänger mehr Pending oder Sending ist, setzt conclude() die Kampagne auf Sent. Das gilt auch, wenn alle Empfänger Failed sind. newsletter:resume --failed akzeptiert dagegen ausschließlich Kampagnen im Status Sending.

Ablauf: Kampagne läuft während einer Relay-Störung oder Circuit-Breaker-Pause → alle Jobs scheitern endgültig → Empfänger Failed, Kampagne Sent → Störung behoben → der angekündigte Wiederaufnahmebefehl verweigert die Arbeit.

Testlücke: Der Transportfehlertest erwartet ausdrücklich Sent nach endgültigem Fehlschlag. Der Resume-Test legt separat eine noch Sending genannte Kampagne an. Die Verkettung beider regulärer Abläufe fehlt.

Empfehlung: Kampagnen mit wiederholbaren Fehlzuständen kontrolliert und atomar wieder öffnen oder Abschlusszustände differenzieren. Bereits gesendete Empfänger müssen unverändert bleiben; failed_count, Abschlusszeit und Kampagnenstatus gemeinsam konsistent behandeln.

Abnahme: Alle Empfänger zunächst endgültig fehlschlagen lassen, anschließend den dokumentierten Befehl ausführen und ausschließlich die fehlgeschlagenen Empfänger nachholen.

U06 — P2: Harter Abbruch während des Empfängeraufbaus lässt die Kampagne unvollständig zurück

Bezug: F14, Plan 2.3/2.6.

Belege: app/Services/Newsletter/CampaignSender.php:38–62, :99–113, :134–150; app/Console/Commands/ResumeNewsletterCampaign.php:82.

Die Kampagne wird vor dem batchweisen Einfrieren auf Sending gesetzt. Nur ein abgefangener Throwable setzt sie zurück auf Draft. Ein Prozessabbruch, etwa SIGKILL oder ein harter FPM-Neustart, überspringt diesen Catch.

Nach einem Abbruch hinter dem ersten 500er-Batch existieren nur diese Empfänger. recipients_total kann noch 0 sein. Resume dispatcht ausschließlich vorhandene Zeilen; es vervollständigt die Auswahl nicht. So kann später eine formal abgeschlossene Kampagne entstehen, deren übrige Zielgruppe nie im Versandprotokoll stand.

Testlücke: Der Test für einen abgebrochenen Start erzeugt einen Draft mit Teilbestand. Der reale harte Abbruch hinterlässt Sending. Das sind unterschiedliche Zustände.

Empfehlung: Einen persistenten Vorbereitungszustand mit wiederaufnehmbarem Auswahlfortschritt beziehungsweise eindeutiger Vollständigkeitsmarke einführen. Versand erst nach vollständig eingefrorener Zielgruppe freigeben. Dabei festlegen, welcher Auswahlzeitpunkt für später hinzugekommene Nutzer gilt.

Abnahme: Prozess nach dem ersten Batch hart beenden, Wiederaufnahme durchführen, anschließend exakt die vollständige ursprüngliche Zielgruppe einmal im Empfängerprotokoll und korrekten Gesamtzähler nachweisen.

U07 — P2: Fehler nach erfolgreicher SMTP-Übergabe kann weiterhin automatischen Doppelversand auslösen

Bezug: F15, Plan 2.4.

Belege: app/Jobs/SendNewsletterCampaignMail.php:82–87, :137–145; app/Listeners/RecordMailActivity.php:38; vendor/laravel/framework/src/Illuminate/Mail/Mailer.php:330–338.

Der Catch um Mail::send() gibt den Empfänger bei jedem Throwable als Pending frei. Mail::send() umfasst aber nicht nur die Transportübergabe, sondern auch anschließende MessageSent-Listener. Im vorhandenen RecordMailActivity findet nach erfolgreicher Übergabe eine nicht abgefangene User-Datenbankabfrage statt.

Ablauf: SMTP nimmt die Nachricht an → User-Abfrage im Eventlistener scheitert → äußerer Catch setzt Pending → Queue-Retry sendet die bereits übergebene Nachricht nochmals. Dass ein späterer Datenbankzugriff wieder gelingt, ist bei einer vorübergehenden Verbindungsstörung möglich; der Callbackfehler beweist keine Nichtzustellung.

Der neue Claim verhindert parallele Übernahme. Er beseitigt diese zeitliche Lücke nach der externen Wirkung nicht. Auch die Kommentaraussage, ein Fehler nach Mailserverübergabe verbleibe als unklarer Sending-Zustand, trifft auf diesen vorhandenen Pfad nicht zu.

Empfehlung: Nachgelagerte Protokollierung darf eine erfolgreiche Übergabe nicht in einen automatisch wiederholbaren Transportfehler umdeuten. Zustellfehler und unklare Übergaben getrennt behandeln. SMTP und Datenbank können durch eine lokale Transaktion allein keine allgemeine Exactly-once-Garantie erhalten.

Abnahme: Echter Mailer-Eventfluss mit kontrolliert erfolgreichem Testtransport und anschließend scheiterndem MessageSent-Listener. Der Retry darf keine zweite Übergabe erzeugen. Mail::fake() allein ist dafür ungeeignet.

U08 — P2: Newsletter-Deduplizierung kann den einzigen Zustellnachweis löschen

Bezug: F05, Plan 2.2; Migration vor Deployment prüfen.

Beleg: database/migrations/2026_09_18_110000_add_unique_user_to_newsletter_campaign_recipients_table.php:25–35.

Die Migration behält für jedes Kampagne/User-Paar ausschließlich MIN(id), unabhängig von Status und sent_at. Bei ältester Zeile Pending/Failed und späterer Zeile Sent wird der Zustellnachweis entfernt. Die erhaltene Zeile kann anschließend erneut versendet werden; Kampagnenzähler werden nicht mit den verbleibenden Protokollzeilen abgeglichen.

Einordnung anhand der lesenden Datenprüfung: In der hier erreichbaren Datenbank bestehen 470 Dublettengruppen mit 470 zusätzlichen Zeilen. Für die konkrete Kombination „eine Zeile Sent, älteste Zeile nicht Sent“ ergab die Abfrage 0 Gruppen. Der gefährliche Zweig ist dort somit nicht nachgewiesen; die Migration ist für andere Zustandsmischungen dennoch nicht robust. Eine Produktionsdatenprüfung wurde damit nicht ersetzt.

Empfehlung: Status und Zustellzeit fachlich zusammenführen, vorhandene Sent-Nachweise erhalten und die Interpretation historischer Versandzähler festlegen. Die Bereinigung nicht gleichzeitig mit weiterlaufenden alten Versandjobs ausführen. Bei bereits versandten Dubletten zwischen historischen SMTP-Übergaben und eindeutigen Empfängern unterscheiden.

Abnahme: Mindestens Pending→Sent, Failed→Sent und Sent→Sent als Dublettenbestand migrieren. Keine anschließend versendbare Zeile für einen bereits nachweislich versandten Empfänger erzeugen.

U09 — P2: Alternative Fortify-Endpunkte umgehen die neue TOTP-/Recovery-Pflicht

Bezug: F03, Plan 1.7, Entscheidung E3.

Belege: config/fortify.php:151, config/auth.php:115; vendor/laravel/fortify/routes/routes.php:147–176; vendor/laravel/framework/src/Illuminate/Auth/Middleware/RequirePassword.php:94–98.

Die neuen Volt-Actions verlangen bei bestätigter 2FA den zweiten Faktor. Fortify registriert daneben weiterhin unter anderem DELETE /user/two-factor-authentication, POST /user/two-factor-recovery-codes und GET /user/two-factor-recovery-codes. Diese Wege verwenden auth plus password.confirm, nicht die neue TOTP-/Recovery-Prüfung.

Eine Session mit noch gültigem auth.password_confirmed_at kann daher ohne aktuellen zweiten Faktor 2FA abschalten beziehungsweise Recovery-Codes abrufen oder erneuern. Der konfigurierte Standard für die Passwortbestätigung beträgt 10.800 Sekunden. views = false schaltet diese Aktionsrouten nicht ab. Eine eigene Abschaltung der Fortify-Routen ist im Projekt nicht vorhanden.

Wichtige Grenze: Das ist kein Zugang ohne Session und auch kein pauschaler Bypass von password.confirm. Voraussetzung ist eine noch gültige Passwortbestätigung oder die Möglichkeit, sie mit dem Passwort herzustellen. Es ist aber ein konkreter Nebenweg zur im Plan beschlossenen E3-Regel und zur Beschränkung der Recovery-Code-Ausgabe.

Empfehlung: Alle erreichbaren 2FA-Verwaltungswege auf dieselbe Sicherheitsregel bringen oder ungenutzte Alternativrouten abschalten. Soll die Passwortbestätigung alternativ genügen, muss die Entscheidung E3 entsprechend präzisiert werden.

Abnahme: Native Fortify-Routen und Volt-Actions mit unbestätigter sowie noch passwortbestätigter Session testen. Eine gültige alte Passwortbestätigung allein darf bei vereinbarter E3-Regel keinen aktuellen TOTP-/Recovery-Nachweis ersetzen.

U10 — P2: API-Portalwechsel prüft die bestehende Kategorie nicht erneut

Bezug: F11, Plan 3.6.

Belege: app/Http/Controllers/Api/V1/PressReleaseController.php:130–145, :259–274.

Beim Anlegen wird die Kategorie gegen das Zielportal geprüft. Beim Update geschieht das nur, wenn category_id im Request steht. portal kann separat geändert werden; die bisherige Kategorie bleibt dann ohne Prüfung bestehen.

Ablauf: Draft auf Presseecho mit einer nur dort gültigen Kategorie anlegen → Update ausschließlich mit portal = businessportal24 → Kategorie und Zielportal passen anschließend nicht mehr zusammen. Eine inzwischen deaktivierte Kategorie kann bei einem Update ohne Kategorieparameter ebenfalls erhalten bleiben.

Empfehlung: Den resultierenden Zustand validieren: neue Kategorie oder vorhandene Kategorie gegen neues oder vorhandenes Portal. Zumindest ein Portalwechsel muss die aktuelle Kategorie zwingend neu prüfen.

Abnahme: Portal-only-Update mit portalfremder bestehender Kategorie ergibt 422; ein Wechsel mit gültiger neuer Kategorie gelingt; gemeinsame aktive Kategorien bleiben zulässig.

U11 — P2: Vertragsablauf kann zwischenzeitliche Verlängerung übergehen und doppelte Ablaufmails erzeugen

Bezug: neuer Planpunkt 5.2.

Belege: app/Console/Commands/ExpireGrandfathering.php:37–43, :65–91.

Der Command selektiert abgelaufene Grandfathered-Verträge. Beim späteren Update prüft er nur IDs und cancelled_at IS NULL, nicht erneut Status und Enddatum. Die Transaktion allein macht die zuvor gelesene Auswahl nicht aktuell.

Fall A: Nach der Auswahl verlängert ein Admin das Vertragsende. Der Ablauf-Command beendet die inzwischen verlängerte Vereinbarung trotzdem.

Fall B: Scheduler und manueller Aufruf lesen dieselben Kandidaten. Einer beendet sie, der andere ändert null Zeilen, schreibt aber trotzdem das Ablaufereignis und plant erneut eine Mail ein, weil die Benachrichtigung nicht an den tatsächlich gewonnenen Zustandswechsel gebunden ist.

Testlücke: Zwei vollständig nacheinander ausgeführte Commands belegen Idempotenz nach Abschluss, nicht überlappende Auswahlsnapshots.

Empfehlung: Bedingungen im Schreibzugriff beziehungsweise unter Zeilensperre erneut prüfen. Nur tatsächlich von diesem Lauf beendete Posten für Ereignis und Kundeninformation verwenden.

Abnahme: Verlängerung zwischen Auswahl und Update bleibt erhalten. Zwei überlappende Läufe erzeugen genau einen fachlichen Ablauf und einen Benachrichtigungsauftrag je betroffener Kundengruppe.

U12 — P2: Ablaufbenachrichtigung kann nach erfolgreicher Vertragsbeendigung dauerhaft ausfallen

Bezug: neuer Planpunkt 5.2.

Belege: app/Console/Commands/ExpireGrandfathering.php:65–73, :90–91.

Die Statusänderung wird committed, bevor die Mail eingeplant wird. Scheitert die Queue-Verbindung oder stirbt der Prozess dazwischen, ist der Vertrag bereits Cancelled. Der nächste Lauf nimmt ihn nicht mehr auf und versucht die Kundeninformation nicht erneut.

Die fachliche Beendigung selbst bleibt erhalten. Betroffen ist die ausdrücklich zugesagte Benachrichtigung; ein tatsächlicher Verlust wurde nicht beobachtet.

Empfehlung: Den Benachrichtigungsauftrag zusammen mit dem Zustandswechsel dauerhaft speichern und unabhängig wiederholbar zustellen, etwa über eine Outbox oder einen separaten Versandmarker. Ein bloßes erneutes Ausführen des bestehenden Commands schließt die Lücke nicht.

Abnahme: Fehler beim Enqueue nach Vertragsbeendigung erzwingen, Prozess wieder aufnehmen und genau einen nachholbaren Benachrichtigungsauftrag nachweisen, ohne den Vertrag erneut zu ändern.

U13 — P2, bedingt: Aggregationslock läuft vor einem langen Command ab

Bezug: F19, Plan 4.6.

Belege: app/Console/Commands/AggregatePressReleaseViews.php:51–65, :93–139.

Der gemeinsame Lock ist eine sinnvolle Verbesserung: Er liegt direkt im Command und schützt dadurch auch manuelle Aufrufe. Seine TTL beträgt jedoch fest 600 Sekunden, während die Batchschleife weder Laufzeitgrenze noch Lock-Erneuerung besitzt.

Bei großem Rückstand oder langsamer Datenbank kann die erste Instanz nach Ablauf der TTL noch arbeiten. Eine zweite Instanz kann dann denselben Lock erwerben und denselben noch nicht fortgeschriebenen Wasserstand lesen. Das ursprüngliche Risiko doppelter Aggregation besteht in diesem Zeitfenster erneut.

Evidenzgrenze: Ein tatsächlicher Lauf über zehn Minuten wurde nicht gemessen. Der Befund betrifft die nachweislich fehlende Absicherung dieser Betriebsbedingung.

Empfehlung: Verarbeitung mit Zeitbudget sicher vor Lockablauf beenden oder einen erneuerbaren Lock samt Verlustprüfung verwenden. Alternativ den Wasserstandsübergang zusätzlich datenbankseitig serialisieren. Nur die TTL ohne Laufzeitannahme weiter zu erhöhen verschiebt die Grenze.

Abnahme: Zeit über die Lock-TTL vorziehen, während ein Lauf noch einen Batch verarbeitet; ein zweiter Lauf darf keine bereits beanspruchten Ereignisse nochmals verbuchen.

5. Was sich an der Umsetzung bewährt

Die Restbefunde erfordern gezielte Ergänzungen, keine Rücknahme der gesamten Arbeit.

  • Identität: Der ursprüngliche ungeprüfte Fortify-E-Mail-Wechsel ist abgeschaltet. Die Verifikations-/Aktivitätsprüfung sitzt im Sync-Service und wirkt damit unabhängig vom konkreten Login-Einstieg. Google setzt bei 2FA login.id/login.remember, regeneriert die Session und bleibt bis zur Challenge Gast.
  • Admin-Actions: Neue Permission-Prüfungen stehen direkt in beiden Grant-Actions. Der Schutz hängt damit nicht mehr allein an mount() und einem früher geöffneten Formular.
  • Newsletter-Auswahl: reorder('id') behebt die Ursache des Cursorfehlers. Der Vergleich aller Empfänger-IDs über 1.001 unsortiert angelegte Nutzer ist deutlich aussagekräftiger als ein reiner Count-Test unterhalb der Batchgrenze.
  • Newsletter-Normalbetrieb: Start-CAS, Unique-Constraint, insertOrIgnore und Pending→Sending-Claim sichern unterschiedliche Ebenen sinnvoll ab. Abschluss und Zähler werden gemeinsam geschrieben; der Kampagnenlock serialisiert konkurrierende Abschlüsse. Der Suppression-Test nutzt bewusst den realen Mail-Gate-Pfad.
  • Sichtbarkeit: Die geänderten öffentlichen, Kunden- und API-Abfragen entfernen gezielt den PortalScope. Der bisher irreführende Vorschautest verwendet jetzt delete() statt forceDelete(). Gelöschte Kontakte fallen aus dem öffentlichen Firmenkontakt-Fallback und der Pressefachliste heraus.
  • Cache: Die Newsroom-Sichtbarkeitsprüfung liegt vor dem Seitencache. Der Observer deckt normale Firmenlöschung, Wiederherstellung und Aktivitätswechsel ab; der gesonderte 410-Ablauf vermeidet dabei bewusst einen redundanten globalen Flush.
  • URL-Auflösung: Host, Sprache und Portal werden zentral ausgewertet; fremde Hosts können nicht mehr allein über einen passenden Slug einer PM zugeordnet werden. Tests decken identische Slugs in mehreren Portalen und Ausgaben ab.
  • Storage: Das explizite Prüfen von put() passt zu Disks mit throw=false. Die neue Reihenfolge beim Logoersatz schützt das bisherige Logo vor einem fehlgeschlagenen Upload; bei einem Datenbankfehler wird die neu gespeicherte Datei aufgeräumt.
  • Abrechnung: Vertragsende wird auch unter der bestehenden Rechnungssperre geprüft. Nicht abrechenbare Fälle verbrauchen nicht mehr das erfolgreiche Rechnungslimit. Hold-Gründe sind sichtbar und werden bei erfolgreicher Abrechnung entfernt.
  • Entwicklungswerkzeuge: pint --test kann einen unformatierten Commit jetzt tatsächlich ablehnen. Der Composer-Start ruft die vorgesehenen Vite-Prozesse auf. Die Doku unterscheidet den historischen Konzeptstand vom aktuellen Betriebseinstieg.

6. Testbewertung und konkrete nächste Regressionen

Vorhandene Tests mit brauchbarem Aussagewert

Bereich Gelesene Testdateien, Auswahl Was sie belegen sollen Verbleibende Grenze
Rechte/Identität ContactAccessTest, CompanyMembershipAuditTest, GoogleLoginTest, UserAccessTest, AdminGrantTest Verifikation, Pivot-Herkunft, Rückstufung der Firmenrechte, Google-Challenge, Panelsperre, erneute Grant-Autorisierung PM-Rechte nach Rückstufung und zusammenhängender Google-Challenge-Abschluss fehlen.
Konto-Sicherheit CustomerProfileSecurityTest Locked-Property, Nachweise in den Volt-Actions, bestätigter E-Mail-Wechsel Native Fortify-Alternativrouten nicht mit derselben Regel geprüft.
Newsletter NewsletterCampaignResilienceTest Batchgrenze, Stale-Model-Doppelstart, Unique-Index, Claim, Suppression, ausgewählte Resume-Zustände Kein harter Abbruch bei Sending mit Teilbestand; kein Sent+Failed-Wiederanlauf; kein Fehler nach Transportübergabe.
API/Sichtbarkeit PressReleaseApiTest, PressReleasePreviewTest, ReleaseDetailTest, CustomerCompanyContextTest Validierung und Soft-Delete-Ausschluss Portal-only-Update nicht bis zum resultierenden Kategoriezustand geprüft.
Öffentliche Seiten CompanyNewsroomCacheObserverTest, NewsroomPageTest, GoneTombstoneTest, ReportedUrlResolverTest Cacheinvalidierung, vorgelagerte Sichtbarkeit, Host-/Sprachauflösung Kein neuer Browserlauf; Kollisionen von both und portalbezogenen Einträgen bleiben gesonderter Randfall.
KI PressReleaseClassificationJobTest, PressReleaseContentScoreTest, PressReleaseSchedulingTest Änderung während Provideraufruf, Audit-Hash, Requeue und Scheduler-Fehlercode Kein Edit zwischen Hashvergleich und Commit; kein roter regulärer Editor-Recheck einer terminierten PM.
Kontingent PressReleaseQuotaTest Expliziter Periodenwechsel, Resetwiederholung, gesperrter Verbrauch Positiver NULL-Zähler nach Migration fehlt.
Abrechnung ManualInvoiceGenerationTest, ExpireGrandfatheringTest Vertragsgrenze, 50 Blocker, sequentieller Wiederanlauf Parallelität, Verlängerung im Verarbeitungsfenster und Fehler vor Mail-Enqueue fehlen.
Architektur/Betrieb GlobalScopeGuardTest, PressReleaseTrackingTest Bekannte Scope-Ausnahmen und bestehender Aggregationslock Keine semantische Analyse innerhalb erlaubter Dateien; kein Lockablauf während Verarbeitung.

Wichtig: „Test vorhanden“ und „Test in diesem Audit bestanden“ sind unterschiedliche Aussagen. Für Letzteres liegt hier kein Nachweis vor.

Kleine, abarbeitbare Nacharbeitsfolge

Reihenfolge Paket Minimaler zusätzlicher Nachweis Umfang
1 U01: Rechteentzug vollständig Firmenfremder Autor; nach Downgrade PM-Edit und Live-Korrektur verweigert M
2 U02/U03: KI-Version und Routing Deterministischer Eingriff vor Commit; roter Recheck über regulären Editorpfad M
3 U04: Kontingentübergang Positiver NULL-Altbestand mit freiem Slot; Monatswechsel ohne Cron S–M
4 U05/U06: Newsletter-Wiederanlauf Vollständig Failed→Resume; harter Abbruch nach erster Auswahlseite M
5 U07/U08: Versandnachweis erhalten Fehler im MessageSent-Listener; Migration mit späterem Sent-Datensatz M
6 U09/U10: Nebenwege schließen Native 2FA-Routen; API-Portal-only-Update S–M
7 U11/U12: Vertragsablauf robust Überlappender Ablauf/Verlängerung; Zustellauftrag nach Enqueue-Fehler M
8 U13 und Betriebsnachweise Lock-TTL überschreiten; wirksame Migrationen/Workerwerte nachweisen S–M

S entspricht hier ungefähr einer Stunde bis einem halben Tag, M ungefähr einem bis drei Tagen, jeweils einschließlich sinnvoller Regressionen. Die Angaben sind Planungshilfen und keine Aufwandsgarantie.

Für einfache Logikfehler reichen deterministische Featuretests. Für U02, U06, U11 und U13 sollte mindestens ein gezielter MySQL-/Mehrprozessnachweis hinzukommen. Ein Test mit zwei nacheinander verwendeten veralteten Modelinstanzen bleibt wertvoll, darf aber nicht als tatsächlich parallel ausgeführter Worker-Test bezeichnet werden.

7. Weitere Verbesserungen und Abweichungen vom Plan

Diese Punkte sind von den 13 priorisierten Fehlern getrennt. Sie enthalten teilweise bewusste Produktentscheidungen und kleinere Betriebs- oder Wartbarkeitsgrenzen.

7.1 Herkunftsklassifizierung ist eine Heuristik, kein Herkunftsnachweis

AuditCompanyMemberships.php:80–85 markiert jede erklärbare Responsible-Zuordnung mit granted_via != auto als Auto. Das umfasst auch eine bewusst manuell vergebene Zuordnung, deren E-Mail zufällig passt. Die fehlende historische Herkunft lässt sich allein aus einer heutigen Übereinstimmung nicht rekonstruieren.

Der Plan sieht eine einmalige Klassifizierung vor; diese ist daher nicht pauschal als Implementierungsverstoß zu bewerten. Vor Ausführung sollte die Kandidatenmenge aber nachvollziehbar geprüft werden. Der aktuelle Bericht listet hauptsächlich nicht erklärbare Zeilen, während --classify die erklärbaren umschreibt. Ein wiederholter Lauf nach neuen manuellen Vergaben kann deren Herkunft nachträglich verlieren. Eine Vorschau der tatsächlich umzustufenden Zeilen und eine begrenzte Bestandsmenge wären besser.

Die Rückstufung findet vereinbarungsgemäß erst beim nächsten Login statt. Bestehende Sessions erhalten dadurch keine sofortige Neuberechnung. Auch ein bestätigter E-Mail-Wechsel stößt in ConfirmEmailChangeController nicht selbst den Sync an. Das ist eine Grenze der gewählten Regel, kein entgegen der Entscheidung verlangter sofortiger Komplettentzug.

7.2 Inaktive Superadmins werden nicht in jedem Policy-Kurzschluss ausgeschlossen

Der neue globale Gate::before liefert für inaktive Superadmins null. PressReleasePolicy::before() und CompanyPolicy::before() liefern weiterhin allein aufgrund von is_super_admin true. Die regulären Panel-Gates und API-Aktivitätsmiddleware schützen die geprüften Einstiege; ein vollständiger globaler Deny ist das trotzdem nicht.

Empfehlung: Inaktivität als durchgängige Berechtigungsinvariante formulieren und direkte Policy-Prüfungen zusätzlich testen. Dieser Bericht behauptet daraus keinen bereits nachgewiesenen Weg an sämtlicher Routenmiddleware vorbei. Das Zusammenspiel von globalen Gates und Policy-Filtern ist in der Laravel-12-Autorisierungsdokumentation beschrieben.

7.3 Score-Fingerprint bildet nicht alle bewerteten Eingaben ab

ContentFingerprint.php:15–25 normalisiert Titel und Text für die Duplikaterkennung. Der Score verwendet daneben unter anderem Quellenlink, Untertitel, Bilder, Keywords und Kontakte; siehe ContentScore/Drivers/DeterministicContentScoreDriver.php:35–57 sowie OpenAiContentScoreDriver.php:121–127. Solche Änderungen verändern den aktuellen Hash nicht. Auch Editor-Reprocessing wird teilweise nur bei Titel-/Textänderung ausgelöst.

Die neue Bindung schützt daher nur den definierten Textfingerprint, nicht jede Einflussgröße des Scores. Ein eigener Bewertungsfingerprint beziehungsweise eine Revisionsnummer sollte die tatsächlichen Eingaben umfassen. Duplikatgleichheit und exakte Analyseversion erfüllen unterschiedliche Zwecke. Diese Erweiterung ist bei der Nacharbeit zu U02 sinnvoll.

Außerdem wird bei einem schon vor Jobstart geänderten Text zunächst die neue Version analysiert, danach aber gegen den alten Dispatch-Hash verglichen und erneut eingeplant. Ein früher Versionsvergleich kann diesen unnötigen Provideraufruf vermeiden. Entscheidend bleibt, den Audit-Hash an die tatsächlich analysierte Fassung zu binden.

7.4 Architekturtest friert Dateien ein, nicht sichere Verwendungszwecke

Die Whitelist in GlobalScopeGuardTest.php enthält aktuell 40 Dateipfade; das Protokoll nennt 39. Relevanter als diese Zähldifferenz: Ein neuer unsicherer Aufruf innerhalb einer bereits erlaubten Datei bleibt unsichtbar. Auch eine anders formatierte Schreibweise kann die einfache Textsuche umgehen.

Unter den Ausnahmen befinden sich neben Admin-/Importpfaden auch öffentliche Legacy-Medienauflösung und weitere Services. Die Abnahmeaussage „nur Admin und Importer“ ist deshalb enger als die tatsächliche Regel. Die Whitelist ist ein brauchbarer Warnmechanismus, aber kein Beweis, dass sämtliche öffentlichen Soft-Delete-Pfade sicher sind. Ausnahmen sollten je Fundstelle fachlich begründet werden.

7.5 Resolver und tatsächliche Seitenauswahl sollten dieselbe Priorität verwenden

Der neue ReportedUrlResolver priorisiert kanonische Einträge und ID. ReleaseDetailPageData lädt passende Portal-/Both-Treffer dagegen mit first() ohne entsprechende Sortierung. Die normalen Tests mit eindeutigem Portal/Sprachpaar sind schlüssig. Bei gleichzeitig passendem both- und portalspezifischem Datensatz sollte zusätzlich geprüft werden, dass sichtbare Seite und gemeldeter Datensatz dieselbe PM meinen. Ohne einen solchen Kollisionsnachweis wird dies hier als Prüfergänzung und nicht als sicher eingetretener Fehlbefund geführt.

7.6 Fokusführung braucht noch einen Browsernachweis

Der neue Alpine-Code setzt den Fokus, behandelt Tab/Shift+Tab an den Grenzen und gibt den Fokus zurück. Die globale Cmd/Ctrl+K-Suche kann aber weiterhin bei offenem Login-Dialog den Fokus zur Suche verschieben; der Tab-Handler hängt am Dialog. Zudem ersetzt die eigene Tab-Schleife keine vollständige Prüfung von Hintergrundfokus und Screenreader-Navigation.

Gezielt prüfen: Öffnen per Tastatur, Vorwärts-/Rückwärtstabs, Escape, Fokus nach Schließen, Cmd/Ctrl+K während des Dialogs und schmale Ansicht. Aus dem statischen Code allein wird keine vollständige Barrierefreiheitsabnahme abgeleitet.

7.7 Dokumentierte Umsetzung ist stellenweise weiter gefasst als der Code

Plan-/Protokollaussage Tatsächlicher Stand Bewertung
1.4: Befehl mit --dry-run Standardaufruf ändert keine Pivots, kann aber eine CSV schreiben; es gibt --classify, kein --dry-run. Sinnvolle sichere Voreinstellung, CLI-Abweichung dokumentieren.
1.8: drei Login-Abschlüsse in einen Service ziehen Google-Zweig ergänzt; Abschlusslogik weiterhin auf mehrere Wege verteilt. Funktionaler Fix vorhanden, Refactoringteil offen.
4.2: publish() und Scheduler prüfen Inhaltsbindung Schedulerprüfung vorhanden; publish() fehlt die Versionsprüfung. Wesentliche unerfüllte Anforderung, U02.
4.6: Isolatable und --isolated Gemeinsamer Cache-Lock direkt im Command. Grundsätzlich gute gleichwertige beziehungsweise breiter greifende Lösung; TTL-Grenze U13.
5.2: GrandfatheringTerminated senden Eigene GrandfatheringExpired-Mail eingeführt. Fachlich nachvollziehbare Trennung, keine Beanstandung allein wegen anderer Klasse.
Rechnungslauf „ohne Tageskappe“ / gesamter Bestand Default --limit=500 erstellte Rechnungen pro Lauf. Starvation behoben, Durchsatz weiterhin begrenzt; Beschreibung präzisieren.
E6: keine letzte MAN-Periode mehr Vergleich erlaubt Rechnung, wenn current_period_end == grandfathered_until. Entspricht der detaillierten </>=-Regel, Kurzentscheidung bleibt auslegungsbedürftig. Gleichheitsfall fachlich ausdrücklich bestätigen.
Nur 6.6 bleibt offen Weitere Abnahmefälle und Teile von 1.8/4.2 fehlen. Abschlussformulierung nach den Restbefunden korrigieren.

Die E-Mail-Änderung ohne 2FA verwendet für Passwortkonten das Passwort, bei reinen Magic-Link-Konten keinen zusätzlichen Nachweis. Diese Ausnahme ist im Umsetzungsprotokoll ausdrücklich erläutert. Sie wird hier nicht als versehentlich fehlende TOTP-Prüfung gewertet; ohne eingerichtete 2FA gibt es keinen solchen Faktor. Ein gewünschter frischer Postfachnachweis wäre eine zusätzliche Produktentscheidung.

8. Betriebs- und Migrationsnachweise

8.1 Lesend festgestellter Stand der angebundenen Datenbank

Boost konnte Datenbankabfragen ausführen, obwohl die lokale Sail-CLI nicht erreichbar war. Diese Zugänge sind daher als unterschiedliche Nachweisquellen zu behandeln. Die hier erreichbare Datenbank ist nicht ohne weiteren Nachweis mit Produktion gleichzusetzen.

Die Abfrage der Migrationshistorie für den 18.09.2026 lieferte nur die früheren Migrationen für Crawler-Hits, Index-Checks und Content-Hash/Canonical. Die sechs im Entwicklungsplan neu genannten Audit-Migrationen fehlten. Eine zusätzliche Prüfung von information_schema bestätigte:

Erwartete Änderung Beobachtung
company_user.granted_via Spalte fehlt.
ki_audits.content_hash und discarded_at Beide Spalten fehlen.
users.press_release_quota_period Spalte fehlt.
user_payment_options.billing_hold_reason und billing_hold_at Beide Spalten fehlen.
B-Tree-Index auf contacts.email Neuer Index fehlt; vorhandener Mehrspalten-Fulltextindex ersetzt ihn nicht.
Unique (newsletter_campaign_id, user_id) Neuer Unique-Index fehlt.

Zusätzlich wurden ausschließlich aggregierte Werte gelesen: 470 Newsletter-Dublettengruppen, 470 zusätzliche Zeilen, 0 Gruppen mit späterem Sent-Nachweis bei nicht gesendeter ältester Zeile; 2 Nutzer mit positivem bisherigem Monatsverbrauch. Es wurden keine E-Mail-Adressen, Kundendaten oder Zugangsdaten in diesen Bericht übernommen.

Folgerung: Für diese Datenbank ist der neue Schemaumfang noch nicht nachgewiesen, sondern konkret nicht vorhanden. Wird der neue Code dagegen aktiviert, bevor die Migrationen erfolgen, sind unter anderem fehlende Spalten bei Firmenrechten, KI-Audits und Rechnungsauswahl zu erwarten. Aus den Commitnamen „Deploy A–E“ folgt kein Nachweis über einen tatsächlich erfolgten Produktivdeploy.

8.2 Vor einer Betriebsabnahme erforderliche Nachweise

  1. Zielumgebung und veröffentlichte Codeversion eindeutig benennen; Migrationsergebnis derselben Umgebung dokumentieren.
  2. U04 und U08 vor der Übernahme entsprechender Bestandsdaten klären. Newsletter-Altjobs während der Bereinigung kontrolliert berücksichtigen.
  3. company-memberships:audit dort auswerten. Ungeklärte Bestandsrechte menschlich entscheiden; sie bleiben durch das Default manual zunächst wirksam. Eine abgelegte und gesichtete Ergebnisliste ist im Entwicklungsplan nicht nachgewiesen.
  4. Erklärbare Herkunftskandidaten vor --classify prüfen und die Begrenzung aus Abschnitt 7.1 beachten.
  5. Konfigurationscache aktualisieren, laufende Worker auf neuen Code bringen und die effektiven Queuewerte prüfen. ENV-Overrides können den Default 150 überschreiben. Jobtimeout muss unter retry_after liegen; siehe Laravel-12-Queue-Dokumentation.
  6. Scheduler-Eintrag für billing:expire-grandfathering und den eigentlichen Schedulerbetrieb prüfen. Ein Eintrag in routes/console.php allein belegt keinen laufenden Cron.
  7. Gezielt Rechteentzug, Google-2FA, Soft-Delete-Sichtbarkeit, Newsletterfehler/Wiederaufnahme und Terminveröffentlichung in einer isolierten Umgebung abnehmen.
  8. Erst danach die entsprechenden fachlichen Blöcke als abgeschlossen markieren. Große Newsletter-Kampagnen insbesondere erst nach U05/U06 erneut freigeben.

Diese Schritte wurden als Prüfbedarf dokumentiert, nicht ausgeführt. Es wurden weder Migrationen gestartet noch Pivots klassifiziert, Newsletter versandt, Rechnungen erzeugt oder Verträge beendet.

9. Nachweisübersicht und Schutz bestehender Arbeit

Nachweis Ergebnis
Git-Diff und Commitzuordnung 124 geänderte Dateien im Vergleich 24c1bbf..fb03fb1; diese Zahl umfasst auch Dokumentation.
Plan-/Code-/Testabgleich Alle sechs Blöcke und alle 28 Altbefunde eingeordnet; F21 und die ausdrücklich ausgeschlossenen Themen nicht erneut als Pflichtumfang eingeführt.
Sail-Testversuch Vor Teststart gescheitert; kein eigener grüner Testlauf.
PHPStan / Build / Browser In diesem Audit nicht ausgeführt. Dokumentierter früherer PHPStan-Erfolg bleibt Fremdnachweis aus dem Plan.
Datenbank Nur SELECT-Abfragen, einschließlich Metadaten und aggregierter Bestandszahlen. Keine Änderungen.
Framework-Dokumentation Boost-Suche lieferte „Invalid Request“; ergänzend offizielle Laravel-12-Dokumentation und lokale Vendor-Implementierung verwendet.
Lieferumfang Ausschließlich diese neue Markdown-Datei.

Zu Beginn waren bereits folgende fremde Arbeitsverzeichnisänderungen vorhanden: die Löschung von docs/weiteres/Decision-Update Duplicate-Content & Duplicate-Checking.md sowie die neue Datei docs/weiteres/Decision-Update Verlinkung & Backlinks in Pressemitteilungen.md. Beide gehören nicht zu diesem Audit und wurden unverändert belassen.

Die Priorität der weiteren Entwicklung sollte auf wirksamem Rechteentzug, atomarer KI-Freigabe, korrektem Bestandsübergang und nachweisbarer Wiederaufnahme nach Fehlern liegen. Der bisherige Plan hat die richtigen Problemfelder erfasst; die nächste Iteration sollte die beschriebenen Übergänge schließen und ihre Abnahmefälle ausdrücklich mitführen.