- 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>
494 lines
59 KiB
Markdown
494 lines
59 KiB
Markdown
# Entwicklungsplan zum Audit vom 11.09.2026
|
||
|
||
**Stand:** 05.10.2026 (Blöcke 1–10 umgesetzt und live seit 05.10.2026 als **v1.1.0**, siehe [`deploy-v1.1.0.md`](../betrieb/deploy-v1.1.0.md))
|
||
**Grundlage:** [AUDIT.md](AUDIT.md) (geprüfter Commit `5d0570b`), abgeglichen mit HEAD `24c1bbf` auf `technik/betriebsfixes-2026-09`
|
||
**Zweck:** Nur die Befunde, die am aktuellen Code belegt sind, in eine abarbeitbare Reihenfolge bringen. Jeder Punkt hat einen konkreten Fix, einen Regressionstest und eine Aufwandsklasse.
|
||
|
||
---
|
||
|
||
## 1. Ergebnis des Abgleichs
|
||
|
||
Der Audit hält. Von 28 Befunden sind 25 am aktuellen Stand bestätigt, zwei nur teilweise (F08, F17), einer ist zwar richtig, aber praktisch ohne Relevanz (F21). Kein Befund wurde durch die Commits seit dem 11.09. behoben, obwohl mehrere der betroffenen Dateien angefasst wurden (`ContactAccessService`, `ManualInvoiceService`, `PressReleaseService`, `NewsroomDetailPageData`, `press-kits/show`).
|
||
|
||
Was über den Audit hinausgeht und bei der Prüfung aufgefallen ist:
|
||
|
||
- **F05 ist schlimmer als beschrieben.** Auf einer Kopie der Live-Daten liefert der Newsletter-Versand an alle 59.705 berechtigten Nutzer genau 500 eindeutige Empfänger, davon 480 doppelt. Die übrigen 59.205 werden nie erreicht. Der Grund ist `chunkById` hinter einer `created_at`-Sortierung; Laravel entfernt nur eine vorhandene `id`-Sortierung, nicht die führende.
|
||
- **F15 hat einen zweiten Fehler durch die Mail-Sperrliste vom 15.09.** Ist ein Empfänger gesperrt, bricht `GateOutgoingMail` den Versand ab, der Job wertet den Rückgabewert nicht aus und markiert den Empfänger als „gesendet“. Nach einer Pause durch den Circuit Breaker landen Empfänger endgültig auf „Failed“, und `queue:retry` holt sie nicht nach.
|
||
- **F04 hat eine Fundstelle, die der Audit nicht kennt.** Die öffentliche Meldungsseite (`ReleaseDetailPageData.php:76-80`) fällt auf `Contact::withoutGlobalScopes()` zurück und zeigt damit gelöschte Kontakte mit Name, Mail und Telefon öffentlich an.
|
||
- **F04 im Kundenpanel ist ein sichtbarer Bug.** Nach „Kontakt löschen“ im Pressefach bleibt der Kontakt in der Liste und ist weiter editierbar (`press-kits/show.blade.php:273-306`).
|
||
- **F22 ist gemessen.** `LOWER(email)` auf `companies` (114.100 Zeilen) ergibt einen Vollscan mit 108.424 geschätzten Zeilen; der direkte Vergleich nutzt `companies_email_idx` mit 1 Zeile. Die Abfrage läuft bei jedem Login. Die Kollation ist bereits `utf8mb4_unicode_ci`, `LOWER()` bringt semantisch nichts.
|
||
|
||
### Befundtabelle
|
||
|
||
| ID | Audit | Prüfung an HEAD | Eigene Einordnung | Block |
|
||
|---|---|---|---|---|
|
||
| F01 | P1 | bestätigt, Route aktiv, Kette vollständig | **kritisch**, sofort | 1 |
|
||
| F02 | P1 | bestätigt | mittel | 1 |
|
||
| F03 | P1 | bestätigt, `showRecoveryCodes` ohne `#[Locked]` | mittel, Fix klein | 1 |
|
||
| F04 | P1 | bestätigt, plus zwei Fundstellen mehr, eine falsch benannt | mittel (PII öffentlich, Panel-Bug) | 3 |
|
||
| F05 | P1 | bestätigt und auf Live-Kopie bewiesen | **kritisch**, vor der nächsten Kampagne | 2 |
|
||
| F06 | P1 | bestätigt, aber latent: kein Datensatz hat `grandfathered_until` | niedrig heute, Fix klein | 5 |
|
||
| F07 | P1 | bestätigt, `content_hash` wird nur für Duplikate genutzt | mittel, Fenster: Admin-/Korrektur-Bearbeitung im Status Review | 4 |
|
||
| F08 | P2 | teilweise: ohne Snapshot kein Aufruf möglich, nur Stale-Session-Fall | niedrig, Fix klein | 1 |
|
||
| F09 | P2 | bestätigt per Tinker | niedrig bis mittel, Einzeiler | 1 |
|
||
| F10 | P2 | bestätigt, keinerlei Entzugslogik, Pivot ohne Herkunft | mittel, macht F01 dauerhaft | 1 |
|
||
| F11 | P2 | bestätigt, nur negative `per_page` unbegrenzt, `status[]` gibt 500 | niedrig bis mittel | 3 |
|
||
| F12 | P2 | bestätigt, Canonical-Arbeit vom 18.09. hat die Report-Controller nicht erfasst | mittel-niedrig | 3 |
|
||
| F13 | P2 | bestätigt, bis 1 h Stale | mittel | 3 |
|
||
| F14 | P2 | bestätigt | mittel, zusammen mit F05 | 2 |
|
||
| F15 | P2 | bestätigt, plus Sperrlisten-Fehler | mittel | 2 |
|
||
| F16 | P2 | bestätigt, aktuell 4 fällige Fälle, alle nicht abrechenbar | niedrig | 5 |
|
||
| F17 | P2 | teilweise: nur der Score-Job kann 60 s reißen | niedrig bis mittel, Einzeiler | 4 |
|
||
| F18 | P2 | bestätigt | mittel (verpasster Cron = Monat ohne Kontingent) | 4 |
|
||
| F19 | P2 | bestätigt, kein `Isolatable` | niedrig, Einzeiler | 4 |
|
||
| F20 | P2 | bestätigt, Runbook verspricht Alarm, der nie kommt | niedrig bis mittel, Einzeiler | 4 |
|
||
| F21 | P2 | bestätigt, Importer ist stillgelegt | **nicht aufgenommen** | – |
|
||
| F22 | P2 | bestätigt und gemessen | mittel, Einzeiler je Stelle | 3 |
|
||
| F23 | P2 | bestätigt | niedrig bis mittel | 3 |
|
||
| F24 | P2 | bestätigt | klein | 6 |
|
||
| F25 | P2 | bestätigt; die irreführenden Tests werden in den Blöcken 2 und 3 ersetzt | die breite MySQL-/Worker-Matrix ist **aufgeschoben** | 6 |
|
||
| F26 | P2 | bestätigt, kein Fokus-Plugin geladen | niedrig | 6 |
|
||
| F27 | P3 | bestätigt | klein | 6 |
|
||
| F28 | P3 | bestätigt, Link auf `dev/frontend/hub-flux/PROGRESS.md` ist tot | klein | 6 |
|
||
|
||
Abschnitt 10 des Audits (unbelegte Beobachtungen) wurde nicht geprüft und ist nicht im Plan. Die drei übrigen Dateien im Ordner sind keine Audit-Befunde: Bildkennzeichnung ist eine eigene Spezifikation mit Feedback-Audit (noch nicht umgesetzt), Kontakterreichbarkeit ist seit den Commits `ada41cb` ff. umgesetzt.
|
||
|
||
---
|
||
|
||
## 2. Aufwandsskala und Arbeitsregeln
|
||
|
||
| Kürzel | Bedeutung |
|
||
|---|---|
|
||
| S | eine Stunde bis ein halber Tag |
|
||
| M | ein bis drei Tage |
|
||
| L | länger oder abhängig von einer Entscheidung |
|
||
|
||
Für jeden Punkt gilt: zuerst der Regressionstest, der am heutigen Code rot ist, dann der Fix, dann `php artisan test --compact` für die betroffene Datei, am Blockende die gesamte Suite und PHPStan. Kein Block wird ohne Deploy abgeschlossen. Blöcke 1 und 2 sind voneinander unabhängig und können parallel laufen; Block 1 hat Vorrang, weil er ausnutzbar ist.
|
||
|
||
---
|
||
|
||
## Block 1 — Identität und Rechte (F01, F10, F03, F02, F09, F08)
|
||
|
||
Der Kern: Firmenrechte entstehen aus einer E-Mail-Übereinstimmung, die nie auf Verifikation geprüft wird, und werden nie wieder entzogen. Alles andere in diesem Block sind Nebenwege, die dieselbe Regel unterlaufen.
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 1.1 | Fortify-Feature `updateProfileInformation` in `config/fortify.php:150` abschalten. Die Kunden-UI nutzt den Endpunkt nicht; sie hat den `pending_email`-Prozess. Test: `PUT /user/profile-information` liefert 404. | S | E1 |
|
||
| 1.2 | `ContactAccessService::syncMembershipsForEmail()` verlangt `hasVerifiedEmail()` und `is_active`. Gilt für alle Aufrufer, nicht nur den Login-Listener. Test: unverifizierter Nutzer mit passender Firmenadresse erhält keinen Pivot; nach Verifikation schon. | S | keine |
|
||
| 1.3 | Herkunft am Pivot speichern: Spalte `company_user.granted_via` (`auto`, `manual`, `owner`). Migration klein, Tabelle klein. Bestehende Zeilen: `manual`, wenn sie nicht durch den Sync erklärbar sind (siehe 1.4). | S | E2 |
|
||
| 1.4 | Bestandsprüfung als Befehl mit `--dry-run`: alle `responsible`-Pivots, deren Nutzer-Mail heute weder zur Firma noch zu einem Kontakt der Firma passt. Ausgabe als Liste, keine Löschung. Danach Entscheidung je Zeile. | S | 1.3 |
|
||
| 1.5 | Entzug beim Login-Sync: `auto`-Pivots mit `responsible`, die keinen aktuellen E-Mail-Treffer mehr haben, entfernen (E7, revidiert E2: `member` ist keine Leserolle). Hochgestufte manuelle Mitgliedschaften (`auto-upgrade`) fallen auf `member` zurück; `manual` und `owner` bleiben unberührt. Test: Kontakt löschen oder Adresse ändern, erneut anmelden, Schreibrecht auf fremde Firmen-PMs ist weg. | M | 1.3, E7 |
|
||
| 1.6 | `security.blade.php`: `#[Locked]` auf `showRecoveryCodes`; die Codes nur im Render nach einer Action ausgeben, nicht aus einer Property. Test: `set('showRecoveryCodes', true)` scheitert. | S | keine |
|
||
| 1.7 | Erneuter Nachweis vor `disableTwoFactorAuthentication()`, `regenerateRecoveryCodes()` und `updateEmail()`: aktueller TOTP-Code oder Recovery-Code. Das funktioniert auch für Konten ohne Passwort. Test: ohne Code 422, mit Code Erfolg. | M | E3 |
|
||
| 1.8 | `GoogleController.php:50`: 2FA-Zweig aus `MagicLinkConsumeController.php:73-81` übernehmen; danach die drei Login-Abschlüsse in einen Service ziehen. Test in `GoogleLoginTest`: Konto mit TOTP endet als Gast auf der Challenge. | S | keine |
|
||
| 1.9 | `AppServiceProvider.php:125`: `Gate::before` nur für `is_super_admin && is_active`. Test: deaktivierter Superadmin mit Session bekommt 403 auf Admin- und Kundenroute. | S | keine |
|
||
| 1.10 | `routes/admin.php:101-104`: Nutzer-Detailroute unter `can:users:manage` stellen, zusätzlich `authorize()` in beiden Grant-Actions. Test: Rechteentzug bei offener Komponente, danach `grantCredits` scheitert. | S | keine |
|
||
|
||
**Entscheidungen**
|
||
|
||
- **E1** Fortify-Profiländerung abschalten (empfohlen) oder auf den `pending_email`-Prozess umbiegen. Abschalten ist ein Einzeiler und schließt den Einstieg vollständig; niemand nutzt den Endpunkt.
|
||
- **E2** Entzugsregel: auto-vergebene `responsible` ohne aktuellen Treffer werden beim nächsten Login auf `member` gesenkt (empfohlen), oder komplett entfernt. Absenken ist reversibel und verliert keine manuellen Rechte. *Revidiert durch E7 am 18.09.2026: `member` ist die Teamrolle mit Schreibrecht auf alle Firmen-PMs; die Zuordnung wird entfernt.*
|
||
- **E3** Nachweisform für Sicherheitsaktionen: TOTP-/Recovery-Code (empfohlen, funktioniert für alle Kontotypen) oder Passwort mit Sonderweg für passwortlose Konten.
|
||
|
||
**Abnahme:** Die Kette aus F01 (fremde Adresse setzen, neu anmelden, zurückwechseln) ist als Test in `ContactAccessTest` abgebildet und hinterlässt keinen Pivot. Alle Login-Wege (Passwort, Magic Link, Google) landen bei aktivem TOTP auf der Challenge. Ergebnis von 1.4 ist gesichtet.
|
||
|
||
---
|
||
|
||
## Block 2 — Newsletter (F05, F14, F15)
|
||
|
||
Vor der nächsten Kampagne. Der Versand erreicht heute nur die ersten 500 und verschickt 480 davon doppelt.
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 2.1 | `CampaignSender.php:39`: Empfängerquery vor `chunkById` mit `reorder()` auf reine ID-Reihenfolge stellen. Die Sortierung ist eine UI-Sortierung der Vorschau, keine Versandreihenfolge. Test: 1.001 Nutzer mit gemischten `created_at`, jede ID genau einmal in den Empfängerzeilen. | S | E4 |
|
||
| 2.2 | Unique-Index `(newsletter_campaign_id, user_id)` auf `newsletter_campaign_recipients`. Tabelle ist klein, kein FULLTEXT, ALTER ist sofort. | S | 2.1 |
|
||
| 2.3 | Kampagnenstart atomar: `UPDATE … SET status='sending' WHERE id=? AND status='draft'`, nur bei einer betroffenen Zeile weitermachen. Empfänger mit `insertOrIgnore`. Test: zwei Instanzen derselben Kampagne, nur eine startet. | S | 2.2 |
|
||
| 2.4 | Versandjob: atomarer Claim (`UPDATE … SET status='sending' WHERE id=? AND status='pending'`), Abschluss von Empfängerstatus und Zähler in einer Transaktion. `$tries` und `$backoff` am Job setzen statt am Worker. | M | keine |
|
||
| 2.5 | Rückgabewert von `Mail::send()` auswerten: Bei Abbruch durch die Sperrliste Empfänger auf `suppressed` statt `sent`, Zähler nicht erhöhen. Neuer Status-Wert im Enum. Test: gesperrte Adresse endet nicht als „gesendet“. | S | 2.4 |
|
||
| 2.6 | Befehl `newsletter:resume {campaign}`: alle `pending`-Empfänger einer `sending`-Kampagne neu einplanen, `failed` optional zurücksetzen. Ersetzt den unwirksamen `queue:retry`-Hinweis des Circuit Breakers. | S | 2.4 |
|
||
|
||
**Entscheidung**
|
||
|
||
- **E4** Versandreihenfolge: keine fachliche Bedeutung (empfohlen, dann reicht 2.1) oder „neueste zuerst“ als Anforderung (dann Cursor über `created_at, id`, Aufwand M).
|
||
|
||
**Abnahme:** Test über die 500er-Grenze in `NewsletterCampaignTest`; Doppelstart erzeugt keine zweite Empfängerzeile; gesperrte Adressen erscheinen in der Statistik als gesperrt.
|
||
|
||
---
|
||
|
||
## Block 3 — Sichtbarkeit, Löschung, Cache, Eingaben (F04, F13, F22, F12, F11, F23)
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 3.1 | `LOWER(email)` entfernen in `ContactAccessService.php:132,146`, `User.php:186`, `ImportNewsletterBounces.php:131,174`. B-Tree-Index auf `contacts.email` (6 MB, FULLTEXT vorhanden, Rebuild dauert Sekunden). Test: Groß-/Kleinschreibung und Leerzeichen bleiben korrekt; EXPLAIN auf dem Dev-Dump zeigt `ref`. | S | keine |
|
||
| 3.2 | `ReleaseDetailPageData.php:76-80` und `press-kits/show.blade.php:273-306`: `withoutGlobalScopes()` durch `withoutGlobalScope(PortalScope::class)` ersetzen. Test: gelöschter Kontakt erscheint weder öffentlich noch im Pressefach. | S | keine |
|
||
| 3.3 | Dasselbe für API v1 (`PressReleaseController.php:36,238,257`, `CompanyController.php:17,34`), Vorschau (`PressReleasePreviewController.php:33-42`), Medien-Controller und `PressRelease.php:194,207,219,430,440,730`. Den Test `PressReleasePreviewTest.php:237-247` auf `delete()` umstellen. Test je Modell: PM, Firma, Kontakt. | M | 3.2 |
|
||
| 3.4 | Firmen-Observer oder Aufruf in `companies/edit.blade.php`: `WebCaches`-Flush bei `delete()` und bei Wechsel von `is_active`. Kontakt-Speichern und -Löschen im Pressefach flushen den Newsroom. `NewsroomDetailPageData.php:41` prüft vor dem Cache-Treffer auch `is_active` und `deleted_at`. Test: Cache warm, Firma deaktivieren, nächster Aufruf 404. `GoneTombstoneTest.php:162` so umbauen, dass der Cache vorher warm ist. | S | keine |
|
||
| 3.5 | Gemeinsamer URL-Resolver für Meldeverfahren (`PressReleaseReportController.php:325`, `ReportFormController.php:102`): Host gegen `config/domains.php` prüfen, Portal und Sprache aus Host und Pfad, dann Slug. Bei Duplikaten die kanonische PM bevorzugen (`canonical_press_release_id is null desc`, wie in `RedirectPortalOnlyPagesFromHub.php:73`). Fremder Host: kein Treffer. Test: gleicher Slug in beiden Portalen und beiden Sprachen. | M | keine |
|
||
| 3.6 | API v1: `per_page` als `integer|min:1|max:100`, `status` als `in:` über das Enum, `language` als `in:de,en`, `category_id` zusätzlich `is_active` und Portal-Match. Test: `per_page=-1`, `status[]=x` liefern 422. | S | keine |
|
||
| 3.7 | `PressReleaseAttachmentStorage.php:66` und `ImageService.php:85`: Rückgabewert von `put()` prüfen, bei `false` Exception. Logo-Tausch in `press-kits/show.blade.php:153` und `companies/edit.blade.php:157`: erst speichern, dann DB, dann altes Logo löschen. Test mit gemocktem Storage, der `false` liefert. | S | keine |
|
||
|
||
**Abnahme:** Kein `withoutGlobalScopes()` ohne Argument mehr außerhalb von Admin-Verwaltung und Importern (Architekturtest in Pest, der die Trefferliste einfriert). Login-Pfad ohne Vollscan.
|
||
|
||
---
|
||
|
||
## Block 4 — KI-Prüfung und Scheduler (F07, F18, F20, F17, F19)
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 4.1 | `ClassifyPressRelease` und `ScorePressRelease` bekommen den `content_hash` zum Zeitpunkt der Einplanung im Konstruktor. Vor dem Speichern Vergleich mit dem aktuellen Hash; bei Abweichung Ergebnis verwerfen, ins `ki_audits` protokollieren, neuen Lauf einplanen. Hash ebenfalls in `ki_audits` speichern. | M | keine |
|
||
| 4.2 | `PressReleaseService::publish()` und `PublishScheduledPressReleases` prüfen, dass `classified_at` nach der letzten Inhaltsänderung liegt oder der Audit-Hash zum aktuellen passt; sonst nicht veröffentlichen, sondern neu prüfen. Wenn der Re-Check einer terminierten PM rot wird, Kunde und Admin benachrichtigen statt still überspringen. Test: Text während der Analyse ändern, altes Ergebnis darf nicht freigeben. | M | 4.1 |
|
||
| 4.3 | Kontingent periodisieren: Spalte `users.press_release_quota_period` (`YYYY-MM`). `consumePublishSlot()` und `pressReleaseQuotaRemaining()` setzen den Zähler auf null, wenn die Periode nicht dem aktuellen Monat entspricht. Der Cron wird zum Aufräumer, nicht zur einzigen Wahrheit. Test: Periode wechselt ohne Cron, doppelter Reset löscht nichts. | M | E5 |
|
||
| 4.4 | `PublishScheduledPressReleases.php:117`: `FAILURE`, wenn `$failed > 0`. Fachliche Ablehnungen (Blacklist, rot) bleiben `SUCCESS`. Test auf Exit-Code. | S | keine |
|
||
| 4.5 | `ScorePressRelease`: `public int $timeout = 120`. `retry_after` in `config/queue.php` auf 150. Runbook ergänzen. | S | keine |
|
||
| 4.6 | `AggregatePressReleaseViews implements Isolatable`, Schedule mit `--isolated`. | S | keine |
|
||
|
||
**Entscheidung**
|
||
|
||
- **E5** Kontingent bleibt an den Kalendermonat gebunden (empfohlen, entspricht dem heutigen Verhalten) oder wechselt auf die Stripe-Abo-Periode. Nur Ersteres ist in diesem Plan.
|
||
|
||
---
|
||
|
||
## Block 5 — MAN-Abrechnung (F06, F16)
|
||
|
||
Beide latent. Aufnehmen, weil die Fixes klein sind und der Fehler beim ersten Datensatz mit Vertragsende ohne Vorwarnung eintritt.
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 5.1 | `ManualInvoiceService::duePaymentOptions()` und die Sperr-Nachprüfung berücksichtigen `grandfathered_until`: keine Rechnung für eine Periode, die nach dem Vertragsende beginnt. Test: gestern beendet, heute endend, unbefristet. | S | E6 |
|
||
| 5.2 | Ablauf-Befehl im Scheduler: Vereinbarungen mit überschrittenem `grandfathered_until` auf beendet setzen und `GrandfatheringTerminated` senden. Heute passiert das nur manuell. | S | 5.1 |
|
||
| 5.3 | `GenerateManualInvoices`: nicht abrechenbare Vereinbarungen aus dem Fenster ausschließen (Zustandsmarke `billing_hold_reason` an der Vereinbarung) und in Seiten weiterlaufen, bis kein abrechenbarer Kandidat mehr da ist. Die Admin-Zahlungsseite zeigt die Hold-Fälle bereits; nur die Marke fehlt. Test: 50 unvollständige vor einer vollständigen. | M | keine |
|
||
|
||
**Entscheidung**
|
||
|
||
- **E6** Verhalten am Vertragsende: letzte Teilperiode wird noch abgerechnet oder nicht. Empfehlung: keine Rechnung mehr, sobald `current_period_end` nach `grandfathered_until` liegt.
|
||
|
||
---
|
||
|
||
## Block 6 — Entwicklung und Qualitätsgates (F24, F27, F28, F26, F25)
|
||
|
||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||
|---|---|---|---|
|
||
| 6.1 | `lint.yml`: `vendor/bin/pint --test` statt Schreibmodus, `contents: write` entfernen, auskommentierten Commit-Schritt löschen. | S | keine |
|
||
| 6.2 | `composer.json` Script `dev`: `npm run dev:all` statt `npm run dev`. | S | keine |
|
||
| 6.3 | `docs/README.md`: Stand-Absatz und toten Link auf `dev/archive/frontend/hub-flux/PROGRESS.md` korrigieren, Hinweis „historisch, aktueller Einstieg ist `dev/README.md`“. Kein Umbau der Doku. | S | keine |
|
||
| 6.4 | Login-Modal in `site-header.blade.php:190`: Fokus beim Öffnen auf das erste Element, Rückgabe beim Schließen, Tab-Begrenzung. Ohne neue Abhängigkeit als kleine Alpine-Direktive; alternativ `@alpinejs/focus` (Abhängigkeit, Freigabe nötig). | S | keine |
|
||
| 6.5 | Architekturtest: `withoutGlobalScopes()` ohne Argument nur in einer Whitelist erlaubt (schließt Block 3 ab). | S | 3.3 |
|
||
| 6.6 | Aufgeschoben: MySQL-/Worker-Integrationstests als zweite CI-Stufe. Sinnvoll erst nach Block 2 und 4, wenn es konkrete Sperr- und Timeout-Fälle zu beweisen gibt. | L | 2, 4 |
|
||
|
||
---
|
||
|
||
## Block 7 — Nacharbeit aus dem Umsetzungsaudit vom 18.09.2026
|
||
|
||
Grundlage: `UMSETZUNGSAUDIT-2026-09-18.md` (13 Restbefunde U01–U13, Abschnitt 7 mit kleineren Punkten, Abschnitt 8 Betriebsnachweise). Am 18.09.2026 gegen `fb03fb1` geprüft.
|
||
|
||
### Abgleich der Restbefunde
|
||
|
||
| ID | Bewertung | Befund im Code | Aufnahme |
|
||
|---|---|---|---|
|
||
| U01 | bestätigt, aber Entscheidungsfrage | `PressReleasePolicy::canManage()` erlaubt jedem Firmenmitglied Bearbeiten, Einreichen, Löschen und Korrektur der Firmen-PMs; `member` ist im System keine Leserolle, sondern die normale Teamrolle. Die Rückstufung nach E2 nimmt deshalb nur die Firmenstammdaten weg. | 7.13, wartet auf E7 |
|
||
| U02 | bestätigt | Beide Jobs vergleichen den Hash per separatem SELECT und speichern danach ohne Bedingung; `publish()` prüft unter der Sperre keine Klassifikation. Fenster zwischen Vergleich und Schreiben sowie zwischen Scheduler-Prüfung und Sperre. | 7.3 |
|
||
| U03 | bestätigt | `reclassifyIfClassified()` läuft mit `route: false`; ein rotes Ergebnis bleibt ohne Statuswirkung, der Scheduler wählt nur Grün/Gelb. Terminierte rote PM hängt still in Review. | 7.4 |
|
||
| U04 | bestätigt | Lesen zählt `period = NULL` als laufenden Monat, `consumePublishSlot()` setzt bei NULL auf 1 statt +1. Dev-Kopie: zwei Nutzer mit Altverbrauch. | 7.2 |
|
||
| U05 | bestätigt, aufgeschoben | Kampagne wird Sent, sobald keine offene Zeile mehr da ist, auch bei nur Failed; `newsletter:resume` verlangt Sending. | E9, nicht jetzt |
|
||
| U06 | bestätigt, aufgeschoben | Harter Abbruch beim Einfrieren lässt Sending mit Teilbestand zurück; Resume ergänzt die Auswahl nicht. | E9, nicht jetzt |
|
||
| U07 | bestätigt, aufgeschoben | `RecordMailActivity` fragt nach der Übergabe die Datenbank; ein Fehler dort landet im Catch um `Mail::send()` und gibt den Claim frei. | E9, nicht jetzt |
|
||
| U08 | bestätigt | Bereinigungsmigration behält `MIN(id)` ohne Blick auf den Status. Dev-Kopie: 470 Gruppen, keine mit späterem Sent-Nachweis, Live nicht geprüft. | 7.1 |
|
||
| U09 | bestätigt | Fortify registriert `two-factor.enable/confirm/disable/qr-code/secret-key/recovery-codes/regenerate-recovery-codes` mit `password.confirm` (drei Stunden gültig). Die Sicherheitsseite ruft die Fortify-Actions direkt auf, nutzt die Routen also nicht. | 7.5 |
|
||
| U10 | bestätigt | Update prüft die Kategorie nur bei mitgesendeter `category_id`; ein reiner Portalwechsel behält eine portalfremde Kategorie. | 7.6 |
|
||
| U11 | bestätigt | Update im Ablauf-Command filtert nur `id` und `cancelled_at`; Ereignis und Mail hängen nicht an der tatsächlich geänderten Zeile. | 7.7 |
|
||
| U12 | bestätigt | Mail wird nach dem Commit eingeplant; ein Fehler dazwischen ist nicht nachholbar. Produktion nutzt die Datenbank-Queue, ein Marker macht es unabhängig vom Treiber. | 7.7 |
|
||
| U13 | bestätigt | Lock 600 s fest, Schleife ohne Zeitbudget; `AppSetting::setValue()` schreibt die Marke unbedingt. | 7.8 |
|
||
| 7.1 | teilweise | `--classify` schreibt jede erklärbare Zeile ohne Vorschau und ohne Datumsgrenze um; `ConfirmEmailChangeController` stößt keinen Sync an. | 7.10 |
|
||
| 7.2 | bestätigt | `PressReleasePolicy::before()` und `CompanyPolicy::before()` fragen nur `is_super_admin`. Über Panel- und API-Middleware ist ein inaktives Konto zwar draußen, die Policy sollte es trotzdem nicht bejahen. | 7.9 |
|
||
| 7.3 | richtig, nicht aufgenommen | Der Fingerprint deckt Titel und Text ab, der Score liest weitere Felder. Der Score hat keine Statuswirkung; eine eigene Bewertungsversion ist Aufwand M ohne Fehlerfolge. Notiz bleibt. | — |
|
||
| 7.4 | teilweise | Whitelist hat 40 Einträge, Protokoll nannte 39. Die Grenze der Textsuche ist bekannt und dokumentiert. | 7.12 |
|
||
| 7.5 | bestätigt | `ReleaseDetailPageData` lädt mit `first()` ohne die Sortierung des Resolvers; bei Doppelveröffentlichung mit gleichem Slug könnten Seite und gemeldeter Datensatz auseinanderfallen. | 7.11 |
|
||
| 7.6 | offen, Nutzer | Browsernachweis für die Fokusführung; Cmd/Ctrl+K bei offenem Dialog. Manuell abnehmen. | — |
|
||
| 7.7 | bestätigt | Protokoll ist an vier Stellen weiter gefasst als der Code (`--dry-run`, 1.8, „ohne Tageskappe“, „nur 6.6 offen“). | 7.12 |
|
||
| 8.x | Betrieb | Die Dev-Kopie war noch nicht migriert; die Migrationen werden hier eingespielt und damit auf MariaDB geprüft. Die Live-Schritte bleiben wie dokumentiert. | Live-Schritte |
|
||
|
||
Nicht aufgenommen aus Block 7: U05–U07, weil das Kampagnenmodul nicht in Betrieb ist (siehe E9); 7.3 als Notiz; 7.6 als manuelle Abnahme.
|
||
|
||
### Arbeitspakete
|
||
|
||
| Nr. | Punkt | Aufwand | Inhalt |
|
||
|---|---|---|---|
|
||
| 7.1 | U08 | S | Bereinigungsmigration behält je Paar zuerst eine `sent`-Zeile, sonst die älteste. Test mit Pending→Sent, Failed→Sent, Sent→Sent. |
|
||
| 7.2 | U04 | S | Migration setzt `press_release_quota_period` auf den laufenden Monat, wo Verbrauch > 0 und Periode NULL. `consumePublishSlot()` behandelt NULL wie den laufenden Monat (+1). Test: Altzähler 2 von 3, nächste Veröffentlichung ergibt 3 von 3. |
|
||
| 7.3 | U02 | M | Jobs speichern Ergebnis und Audit nur per bedingtem UPDATE `where content_hash = analysierter Hash`; trifft es nicht, Audit als verworfen und neuer Lauf. `publish()` prüft bei automatischen Quellen (`ki`, `scheduler`) unter der Zeilensperre, dass die jüngste gültige Klassifikation den aktuellen Hash trägt; sonst kein Publish, neuer Prüfjob mit Routing, `StaleClassificationException`. Admin-Publish bleibt manuelle Übersteuerung. |
|
||
| 7.4 | U03 | S | Rotes Ergebnis eines Re-Checks ohne Routing bei einer PM in Review führt zur Ablehnung mit Autorenmail wie bei der Erstprüfung. Veröffentlichte PMs bleiben unverändert (Admin-Signal). |
|
||
| 7.5 | U09 | S | Middleware sperrt die sieben ungenutzten Fortify-2FA-Verwaltungsrouten (404); Challenge-Routen bleiben. Test je Route mit passwortbestätigter Session. |
|
||
| 7.6 | U10 | S | API-Update prüft die resultierende Kategorie gegen das resultierende Portal beim tatsächlichen Portalwechsel oder bei mitgesendeter `category_id`; ein erneut gesendetes, unverändertes Portal löst allein keine Prüfung aus (Präzisierung 25.09.2026, Umsetzungsaudit 5.3). |
|
||
| 7.7 | U11, U12 | M | Bedingtes UPDATE mit Status, Enddatum und `cancelled_at`; nur getroffene Zeilen erzeugen Ereignis und Mail. Marker `grandfathering_expired_at` und `grandfathering_notified_at`; Mail-Einplanung in zweitem Schritt, nachholbar beim nächsten Lauf. |
|
||
| 7.8 | U13 | S | Zeitbudget 480 s pro Lauf (Lock 600 s), Rest beim nächsten Lauf. Marke nur bedingt fortschreiben (`where value = alte Marke`), sonst Transaktion abbrechen. |
|
||
| 7.9 | 7.2 | S | Policy-`before()` bejaht nur aktive Super-Admins. |
|
||
| 7.10 | 7.1 | S | `--classify` zeigt vorher die betroffenen Zeilen (Anzahl, Stichprobe) und nimmt mit `--before=` nur Pivots vor dem Deploy. `ConfirmEmailChangeController` stößt den Sync an. |
|
||
| 7.11 | 7.5 | S | `ReleaseDetailPageData` sortiert wie der Resolver (Erstquelle zuerst, dann ID). |
|
||
| 7.12 | 7.4, 7.7 | S | Protokoll korrigieren: 40 Ausnahmen, `--classify` statt `--dry-run`, 1.8 teilweise, Rechnungslauf mit Kappe 500 pro Lauf, E6 Gleichheitsfall. |
|
||
| 7.13 | U01 | M | Nach E7: automatische Zuordnung beim Wegfall der Grundlage entfernen statt senken; neuer Pivot-Wert `auto-upgrade` für hochgestufte manuelle Mitgliedschaften. |
|
||
|
||
**E6, Gleichheitsfall:** Endet der Vertrag genau am letzten Tag der Periode (`grandfathered_until = current_period_end`), wird die Periode noch berechnet. Das ist die Regel „Rechnung, wenn die Periode nicht über das Vertragsende hinausreicht“. Bestätigt am 18.09.2026 mit E6.
|
||
|
||
---
|
||
|
||
## Block 8 — Nacharbeit aus dem Umsetzungsaudit vom 25.09.2026
|
||
|
||
Grundlage: [`UMSETZUNGSAUDIT-2026-09-25.md`](UMSETZUNGSAUDIT-2026-09-25.md) (22 Befunde: 4 × P1, 18 × P2, dazu Abschnitt 5 mit Optimierungen). Jeder Befund wurde am 25.09.2026 am Code gegengeprüft, bevor etwas geändert wurde; Plan Fassung 2 und Decision-Update vom 22.09. wurden auf abweichende Entscheidungen durchgesehen. Ergebnis: **alle 22 Befunde bestätigt, keiner war anders entschieden.** Zwei Befunde (N13, 5.1 a) sind Lücken zwischen einer eigenen Entscheidung (O1, E-B10) und ihrer Umsetzung.
|
||
|
||
### Abgleich
|
||
|
||
| Nr. | Ergebnis | Umsetzung |
|
||
|---|---|---|
|
||
| N01 (P1) | bestätigt | Gemeinsamer Freigabezustand `PressReleaseImage::isPubliclyDeliverable()` (nicht zurückgehalten; v2 nur mit Rechtebestätigung; Bestand `rules_version` NULL bleibt sichtbar), Scope gleichlautend; Medienroute wendet ihn an, nicht freigegebene Bilder nur mit Signatur/`view`-Recht und `private, no-store`. Cache-Lebensdauer öffentlicher Medien unverändert (E17). |
|
||
| N02 (P1) | bestätigt | `tombstone()` liest den Eintrag unter `lockForUpdate` neu und prüft Hold, Frist und Tombstone erneut; Hold setzen/aufheben in Transaktion mit Sperre, Hold auf Tombstone wird abgelehnt. |
|
||
| N03 (P1) | bestätigt | Bedingtes UPDATE, `syncOriginal` und Audit-INSERT in einer Transaktion (Klassifikation und Score); Re-Dispatch erst nach Commit. Keine neue Regel „ohne Audit-Hash kein Auto-Publish" (E11). |
|
||
| N04 (P2) | bestätigt | Erwarteter Hash bis in `reject()`/`holdForManualReview()` unter der Sperre; Abweichung → `StaleClassificationException` + Re-Check für den aktuellen Stand (`rescheduleForCurrentContent()`); Vorprüfung in `redRecheckMustReject()`. |
|
||
| N05 (P2) | bestätigt | Eigene Ability `confirmImageRights` (nur Kontoinhaber, auch bei Published; Super-Admin-Kurzschluss in Policy und globalem `Gate::before` ausgenommen); Service prüft Identität und ausstehende Bestätigung selbst (E10). |
|
||
| N06 (P2) | bestätigt | Livewire-Komponenten tragen die gezeigten Fassungen (`legalAcceptanceVersions` per Trait-Hook `rendering`, `legalTextKey`/`imageLegalTextKey` als `#[Locked]`); Abweichung beim Absenden → Ablehnung, neuer Wortlaut wird gezeigt. Fehlendes Pflichtdokument bei aktivem Gate ist ein Konfigurationsfehler. |
|
||
| N07 (P2) | bestätigt | `store(..., allowedStatuses)`: Status-Regel des Aufrufers wird unter der PM-Sperre erneut geprüft; nach Kundenbestätigung an veröffentlichter PM folgt der Veröffentlichungssnapshot. |
|
||
| N08 (P2) | bestätigt | Cache-Invalidierung nach dem Commit außerhalb der Dateibereinigung; Fehler nur `report()`. |
|
||
| N09 (P2) | offen | Snapshot-Deduplizierung nur über Bildbytes, `put()`-Ergebnisse ungeprüft — **noch nicht umgesetzt** (siehe unten). |
|
||
| N10 (P2) | bestätigt | `delete()`/`put()` ausgewertet; Tombstone erst nach bestätigter Löschung; `purgeDue()` liefert `{due, purged, failed}`; Export über `.tmp` + `move`; Commands enden mit FAILURE bei Fehlern (Scheduler-Alarm greift). |
|
||
| N11 (P2) | bestätigt | `record()` schreibt `.tmp` → Zeile → `move`; Marker `press_releases.archive_pending_at` in der fachlichen Transaktion, `archive:retry-pending` (03:35) holt `pm_depublished`/`gdpr_change`/`image_removed` nach, `archive:sweep-orphans` (03:40) räumt Dateien ohne Zeile; Anonymisierung/Korrektur archivieren aus dem Redaction-Snapshot; Systemstatus zeigt offene Marker. `deleteFromAdmin` bleibt blockierend (E15). |
|
||
| N12 (P2) | bestätigt | Anonymisierung entfernt Bilder nach dem Commit über `PressReleaseImageRemover` (`image_removed`, Grund `gdpr`, Antrag referenziert); lokale Beweisdateien danach weg, Pfade an der Zeile geleert. |
|
||
| N13 (P2) | bestätigt (Lücke zu O1) | `press-releases:purge-drafts` erfasst zusätzlich `rejected` mit `published_ever_at` NULL: gleiche Frist ab letzter Änderung, nur Medien, Status bleibt (E12). Altbestand mit früherer Veröffentlichung unangetastet. |
|
||
| N14 (P2) | bestätigt | Phase 2 je Kunde in Transaktion: Posten sperren, Marker per bedingtem UPDATE beanspruchen, Mail in derselben Transaktion einplanen (DB-Queue, E18). |
|
||
| N15 (P2) | bestätigt | Kohorten halboffen `[lower, upper)`, lückenlos; Kohortensumme = Segment. Basiswert 0.1 bleibt mit Fußnote stehen. |
|
||
| N16 (P2) | bestätigt | `byReader()` gruppiert nach Art, Schlüssel, Portal und Sprache wie `topFeeds()`. |
|
||
| N17 (P2) | bestätigt | `sitemap_role` (`source`/`copy`/`none`) und `sitemap_portals` aus derselben Regel wie die Sitemaps; Kundenansicht mit drittem Zweig für Kopien. |
|
||
| N18 (P2) | bestätigt | Nenner und Zustände auf derselben Menge (`visibleOn()`); jüngster Check über `max(id)`. |
|
||
| N19 (P2) | bestätigt | `reassignCopiesOf()` bei Depublikation, Gone, Löschung und Admin-Statuswechsel; Erstquelle nur ohne Soft-Delete-Umgehung aufgelöst; Admin-Republish ordnet zu. |
|
||
| N20 (P2) | bestätigt | Strikte Schemaprüfung im OpenAI-Treiber (Objekt je Kategorie, echtes Bool, endliche Konfidenz 0…1) → Zustand `error`; `normalize()` wertet nur `hit === true`. |
|
||
| N21 (P2) | bestätigt | Marke ungecacht (`AppSetting::getValueFresh()`) in Aggregation und Purge; Cache im Konfliktfall verworfen; `setValue()` löscht den Cache erst nach Commit. |
|
||
| N22 (P1) | bestätigt | Guard in `Tests\TestCase::setUpTraits()`, also vor `RefreshDatabase`; phpunit.xml pinnt die Feature-Flags. |
|
||
| 5.1 a | bestätigt | Beim Tombstone eines `image_removed`-Eintrags werden Bestätigungen, Korrekturen und die weich gelöschte Bildzeile hart gelöscht (E13). |
|
||
| 5.1 b | bestätigt | `label_corrected` nur für veröffentlichte Bilder (E14). |
|
||
| 5.1 c | teilweise | Abgleich nach Restore läuft zusätzlich über alle DB-Tombstones; externes Exportziel nicht gebaut (E16), Runbook-Hinweis. |
|
||
| 5.1 d | bestätigt | `archive_entries.legal_request_id`, Activity-Meta trägt `archive_entry_id`, Archivseite filtert nach Vorgang. |
|
||
| 5.1 e | bestätigt | `App\Support\FileDeletion` (Ergebnis + `exists()`); nie veröffentlicht → Exception in der Transaktion, veröffentlicht → Log + Systemstatus-Zähler. |
|
||
| 5.2 Kategorienfeed | bestätigt | Kategorie-Admin leert die Seiten-Caches bei Änderung von Sichtbarkeit, Portal, Eltern oder Slug. |
|
||
| 5.2 Dry-Run | bestätigt | `purge-events --dry-run` zählt Crawler- und Feed-Zeilen mit. |
|
||
| 5.2 übrige | offen | Synchroner Bildcheck (Zeitbudget), Dekodieraufwand, Report-Skalierung, OG-Cache bei Korrektur: Messung bzw. Betreiberentscheidung, nicht Teil dieser Nacharbeit. |
|
||
| 5.3 | erledigt | Audit, Decision-Update und Plan Fassung 2 versioniert, Antwortkopie entfernt; U10-Formulierung präzisiert. Browser-/Tastaturabnahme und MySQL-/Worker-CI bleiben offen. |
|
||
|
||
### Live-Schritte für Block 8
|
||
|
||
1. `php artisan migrate --force` — zwei nullable Spalten (`archive_entries.legal_request_id`, `press_releases.archive_pending_at`), INSTANT, Sekunden.
|
||
2. `php artisan schedule:list` — neu `archive:retry-pending` 03:35 und `archive:sweep-orphans` 03:40 (beide mit Alarm bei Fehlschlag).
|
||
3. `php artisan press-releases:purge-drafts --dry-run` — vor dem ersten Wochenlauf prüfen, dass nur `rejected`-Meldungen **ohne** `published_ever_at` erfasst werden (10.463 „offline genommen"-Altfälle müssen draußen bleiben).
|
||
4. `php artisan archive:retry-pending --dry-run` und `archive:sweep-orphans --dry-run` einmal ansehen; Systemstatus zeigt offene Marker und lokale Beweisreste.
|
||
5. `view:cache`, `config:cache`, `queue:restart`, FPM-Neustart wie üblich.
|
||
|
||
**Nicht in dieser Nacharbeit:** N09 (Snapshot-Versionierung nach Credit-/KI-Korrektur und `put()`-Prüfung im Snapshot) — eigener Schritt, weil die Snapshot-Datei damit versioniert werden muss (Bildbytes plus Metadaten) und der Neuaufbau bei Korrektur mit der OG-Cache-Frage (5.2) zusammenhängt.
|
||
|
||
## Block 9 — Umsetzung des Umsetzungsaudits vom 29.09.2026
|
||
|
||
Grundlage: [`UMSETZUNGSAUDIT-2026-09-29.md`](UMSETZUNGSAUDIT-2026-09-29.md) (4 × P1, 22 × P2, P3-Liste, OPT-1 bis OPT-9). Auftrag des Betreibers vom 29.09.2026: Block 9 vollständig und die kleinen Punkte (P3, OPT) mit umsetzen. Umgesetzt am 29.09.2026 in vier parallelen Gruppen mit getrennter Dateihoheit (Rechte/KI/Jobs, Bild-Pipeline, Archiv, SEO/Berichte); Integration, Qualitätsgates und Doku im Anschluss. Jeder Befund zuerst als roter Regressionstest, dann der Fix.
|
||
|
||
### Stand je Befund
|
||
|
||
| Schritt | Befunde | Stand | Kern der Umsetzung |
|
||
|---|---|---|---|
|
||
| 9.1 | R-01, K-04 | erledigt | API-Zuordnung zu bestehender Firma nur für Owner/Mitglieder oder Allowlist (`NEWSROOM_API_ASSIGNMENT_USER_IDS`), Pivot `granted_via = api`, Befehl `company-memberships:api-assignments` (Liste, `--remove` mit Vorschau). OAuth-Verifikation eines offenen Kontos: Passwort `null`, `remember_token` neu, Sanctum-Tokens weg, alle Sessions widerrufen (`SessionRevocation` + Middleware `EnsureSessionIsNotRevoked`, weil Redis-Sessions nicht je Nutzer aufzählbar sind) |
|
||
| 9.2 | K-01, B-01, B-02, B-03 | erledigt | Scheduler und `publish()` (Quellen `ki`/`scheduler`) respektieren `manual_review_required_at` (`ManualReviewRequiredException`); Feed, Card-Helfer und Nachweis-PDF nur mit `isPubliclyDeliverable()`; Livewire-Temp-Upload 16 MiB |
|
||
| 9.3 | R-02/K-03, R-03, N19-Rest | erledigt | Aggregation und Merge ohne `updated_at`; neue Spalte `press_releases.content_changed_at`, `PressRelease::contentModifiedAt()` für lastmod, `dateModified`, `article:modified_time`; Sitemap und Legacy-`view_url` mit `/en/` für englische Meldungen; `archive()` gibt Kopien frei |
|
||
| 9.4 | A-01, A-02, A-03 | erledigt | Auftragstabelle `archive_pending_jobs` (`ArchivePendingQueue::push()`/`completeWith()`), `archive_pending_at` nur noch Sammelmarker; Nachholer ohne Schlüssel FAILURE; Sweep verschiebt in Quarantäne (30 Tage), Pause per `--pause`/`--resume` bzw. automatisch beim Restore-Abgleich, `archive:reregister` aus `manifest.json` |
|
||
| 9.5 | N09, B-04 bis B-07, A-04, A-06 | erledigt | Snapshot versioniert (Bytes- plus Metadaten-Hash, OG-Variante enthalten, `put()` geprüft, alle Fassungen ins Archiv); Bestätigung an Bild-ID und SHA gebunden; Entfernen unter Sperre mit frischem Stand; Korrektur archiviert über Auftrag; `precheck_withheld_at` getrennt von `withheld_at`; Depublikationsstand am Auftrag; Restore-Abgleich löscht Anwendungskopien |
|
||
| 9.6 | K-02, R-04, R-05, E9-Schalter | erledigt | Kontingent-/Bildsperre im Scheduler als „blockiert" (Exit 0, einmaliger Verlaufseintrag), KI-Job ohne Retry; nach Kundenbestätigung erneut routen; `X-Pk-Ref` per HMAC signiert, Hard-Sperre nur bei gültiger Signatur; Unmerge setzt nur unveränderte Füllwerte zurück (`company_merge_moves.merged_value`); `NEWSLETTER_SENDING_ENABLED` (Standard aus) |
|
||
| 9.7 | Q-01, Q-02, Q-06, P3, OPT | erledigt bis auf Q-05 | Testhelfer nach `tests/Pest.php` (Suite läuft parallel); Larastan voller Scope ohne Meldung; P3-Liste und OPT-1 bis OPT-9 umgesetzt (OPT-4 teilweise, Integritätshash bleibt bewusst); Q-07 war ein Fehlalarm (`IMAGE_PRECHECK_MODEL` steht auskommentiert in `.env.example`). Q-05 entschieden am 05.10.2026: `docs/` ist der per Devcontainer eingebundene Obsidian-Vault und kommt aus dem Repository (`.gitignore`), die Historie bleibt. |
|
||
|
||
Zusätzlich bei der Integration: `press-release-images:sweep-orphan-originals` wöchentlich (So 04:45) im Scheduler, löscht nichts, solange der Archiv-Sweep pausiert ist; `purge-events` von 03:20 auf 03:50; alle `withoutOverlapping()` mit kurzer Sperrdauer (`ScheduleConfigurationTest`); Scheduler-Tabelle im Runbook vollständig. **Im Runbook stand seit `665c94f` (21.07.2026) ein Passwort neben dem KAS-SSH-Zugang** — aus der Arbeitskopie entfernt; in der Git-Historie und auf `origin` bleibt es, deshalb rotieren (Betreiber). **Erledigt 06.10.2026:** SSH-Zugang des Altsystems geändert und deaktiviert (Betreiber).
|
||
|
||
### Entscheidungen (bei der Umsetzung getroffen, **vom Betreiber am 02.10.2026 bestätigt**, einschließlich der Revision von E25)
|
||
|
||
| Nr. | Befund | Entscheidung |
|
||
|---|---|---|
|
||
| E19 | R-01 | Allowlist per `.env`, Standard leer; ohne Eintrag verhält sich connektar wie vor 1a.4 (neue Firma je Einlieferung). Live: connektar = User 13993 |
|
||
| E20 | K-04 | Passwort bei OAuth-Verifikation eines offenen Kontos verwerfen; Anmeldung danach über Google, Magic Link oder Passwort-Reset |
|
||
| E21 | K-02 | Keine Autorenmail bei Kontingent- oder Bildsperre (keine passende Vorlage; Bildbestätigung wurde schon angefordert), nur Verlaufseintrag und Log |
|
||
| E22 | K-08 | Google-Login ohne erzwungenes „Angemeldet bleiben"; `magic-login` ohne `guest`, fremde Links wie bisher |
|
||
| E23 | E9 | Newsletter-Versand per Schalter aus, bis U05–U07 umgesetzt sind |
|
||
| E24 | R-02 | `content_changed_at` ohne Backfill; Fallback `published_at` |
|
||
| E25 | R-04 | ~~Unsignierte Rückläufer im Übergang `soft`, nach etwa 14 Tagen auf `policy`~~ — **revidiert 02.10.2026 (O01):** jeder nicht verifizierte Rückläufer, hart wie weich, nur zur Sichtung (`BOUNCE_UNSIGNED_KIND=policy`); `soft` nur als bewusste, fälschbare Ausnahme |
|
||
| E26 | R-09 | Claim-Links je Firma und Tag 5 statt 3, gezählt erst nach Versand |
|
||
| E27 | R-10 | `email_bounces` 395 Tage wie DSE 5a; die Sperrliste selbst bleibt. DSE erwähnt Rückläufer noch nicht (Betreiber/Anwalt) |
|
||
| E28 | R-11 | Canonical-Portal über Kategorie-ID (1–14), Slug-Liste als Rückfall |
|
||
| E29 | A-02 | Auftragstabelle statt Zeitstempel-Marker |
|
||
| E30 | A-03 | Quarantäne 30 Tage statt Löschen |
|
||
| E31 | A-15 | Schlüsselrotation über `ARCHIVE_PREVIOUS_KEYS`, Export-HMAC per HKDF; alte Exporte bleiben prüfbar |
|
||
| E32 | B-05 | Entfernen eines veröffentlichten Bildes blockiert bei Archivfehler nicht (E15-Muster), Auftrag bleibt offen, Beweisdateien bleiben liegen |
|
||
| E33 | B-09 | Admin als Eigentümer der Meldung bekommt im Bildmanager die Kundenregel |
|
||
| E34 | B-10 | Eigener Magic-Link-Zweck `image_confirmation`, 72 h, einmalig |
|
||
|
||
### Deploy-Plan für die Blöcke 9 und 10
|
||
|
||
Gilt für beide Blöcke zusammen (Block 10 hat keine eigene Migration). In Klammern die Entscheidung bzw. der Befund, aus dem der Schritt folgt.
|
||
|
||
**A — Vor dem Deploy**
|
||
|
||
1. `.env` auf Live ergänzen:
|
||
- `NEWSROOM_API_ASSIGNMENT_USER_IDS=13993` — connektar, auf Live gegenprüfen (`select id from users where email = 'info@connektar.de'`). Ohne Eintrag legt connektar wieder je Einlieferung eine neue Firma an (E19).
|
||
- `NEWSLETTER_SENDING_ENABLED=false` — bleibt aus, bis U05–U07 umgesetzt sind (E23).
|
||
- `BOUNCE_UNSIGNED_KIND=policy` — unsignierte Rückläufer nur zur Sichtung (E25 revidiert, O01).
|
||
- Optional, Standardwerte greifen: `BOUNCE_SIGNING_KEY` (leer = aus `APP_KEY`), `BOUNCE_RETENTION_DAYS` (395, E27), `ARCHIVE_PREVIOUS_KEYS` (nur bei Schlüsselrotation, E31), `ARCHIVE_QUARANTINE_DAYS` (30, E30), `ARCHIVE_SWEEP_PAUSED`, `CONVERSION_MAIL_CLICK_THROTTLE_MINUTES` (30), `NEWSROOM_CLAIM_LINKS_PER_DAY` (5, E26).
|
||
2. Hauptkategorien prüfen: `select id, slug from category_translations where locale = 'de' and category_id between 1 and 14` muss zu `config/canonical.php` → `portal_by_category_id` passen (E28). Weicht etwas ab, vor dem Deploy die Config anpassen — sonst ändert sich die Canonical-Zuordnung still.
|
||
3. Uploadgrenzen: PHP `upload_max_filesize` und `post_max_size` sowie nginx `client_max_body_size` ≥ 16 MiB mit Puffer, z. B. 20M (B-03).
|
||
4. DB-Backup ziehen; `ARCHIVE_KEY` liegt getrennt gesichert vor.
|
||
|
||
**B — Deploy**
|
||
|
||
5. Dateien hochladen (Branch `technik/betriebsfixes-2026-09`).
|
||
6. `php artisan migrate --force` — acht Migrationen, alle additiv: `press_releases.content_changed_at` (INSTANT, E24), `company_merge_moves.merged_value`, `press_release_images.precheck_withheld_at` + Übernahme offener Sichtungen, Magic-Link-Zweck `image_confirmation` (E34), `archive_pending_jobs` (E29), `archive_entries.key_id` + Index `payload_path` (E31), `archive_access_log.admin_name/-email`. Dev-Kopie (MySQL): 0,7 s gesamt.
|
||
7. `php artisan config:cache`, `php artisan view:clear && php artisan view:cache`, `php artisan schedule:clear-cache` (alte 24-h-Sperren von `withoutOverlapping`), `php artisan queue:restart`, FPM-Neustart.
|
||
|
||
**C — Direkt nach dem Deploy**
|
||
|
||
8. `php artisan archive:retry-pending` einmal von Hand — vorhandene Altmarker werden zu Aufträgen `legacy_marker` und laufen einmal (E29). Danach im Admin-Systemstatus die Kacheln `archive_key`, `archive_pending`, `archive_sweep` grün.
|
||
9. `php artisan company-memberships:api-assignments` sichten; nur bei Treffern ohne Freischaltung `--remove` (zeigt vorher die Zeilen, E19).
|
||
10. `php artisan press-releases:assign-canonicals` — setzt veraltete Verweise zurück und leert die Caches (R-06, E28).
|
||
11. `php artisan press-release-images:sweep-orphan-originals` einmal als Trockenlauf; ab dann läuft er sonntags 04:45 scharf (B-11).
|
||
12. `php artisan archive:sweep-orphans --dry-run` — auf Live war der Sweep am 28.09. leer; Funde wandern jetzt in die Quarantäne statt gelöscht zu werden (E30).
|
||
13. `php artisan schedule:list` — 19 Tasks, darunter `press-release-images:sweep-orphan-originals` So 04:45 und `press-releases:purge-events` 03:50.
|
||
|
||
**D — Abnahme auf Live (Testkonto)**
|
||
|
||
14. Anmelden per Passwort, Magic Link und Google; danach einige Seiten aufrufen — die neue Session-Middleware läuft auf jedem Request (E20, E22, K-08). Ein bereits eingeloggter Nutzer, der einen eigenen Magic Link öffnet, landet auf dem Ziel.
|
||
15. Titelbild mit 13–15 MiB über das Formular hochladen (B-03).
|
||
16. `sitemap.xml` stichprobenartig: englische Meldungen unter `/en/press-release/…` (R-03); Meldungsseite: `article:modified_time` = Veröffentlichung bzw. letzte Textänderung, nicht der letzte Aufruf (E24).
|
||
17. Admin ▸ Newsletter: Kampagne zeigt den Sperrhinweis, „Senden" ist aus (E23).
|
||
18. Admin ▸ Mail-Sperrliste nach dem nächsten `mail:fetch-bounces`: unsignierte Rückläufer erscheinen als Richtlinie mit „(unsigniert)", sperren aber nicht (E25).
|
||
|
||
**E — Danach, außerhalb des Codes**
|
||
|
||
19. ~~**KAS-Passwort rotieren**~~ — **erledigt 06.10.2026:** SSH-Passwort des Altsystems geändert, SSH-Zugang deaktiviert. Das KAS-Hosting bleibt für die E-Mail-Postfächer.
|
||
20. **Datenschutzerklärung:** Rückläufer-Protokoll (`email_bounces`, 395 Tage) in Abschnitt 5a/13 aufnehmen (E27, Betreiber/Anwalt).
|
||
21. Restore-Übung in isolierter Umgebung (DB alt/Disk neu, Disk alt/DB neu, beide alt) mit den neuen Schritten aus Runbook Abschnitt 9 (O04/O05).
|
||
22. Protokoll unten um Deploy-Datum und Ergebnisse von 8–18 ergänzen.
|
||
|
||
Keine Kundenkommunikation nötig: E20 betrifft nur offene, nie verifizierte Selbstregistrierungen; E21 erzeugt bewusst keine Mail; E33 ändert nur die Admin-Sicht.
|
||
|
||
## Block 10 — Abgleich Umsetzungsaudit vom 02.10.2026
|
||
|
||
Grundlage: [`UMSETZUNGSAUDIT-2026-10-02.md`](UMSETZUNGSAUDIT-2026-10-02.md) (2 × P1, 6 × P2) zur noch nicht committeten Arbeitskopie von Block 9. Jeder Befund am 02.10.2026 am Code gegengeprüft: **alle acht valide**, keiner durch eine dem Audit unbekannte Entscheidung gedeckt. Alle als offensichtliche Fehler behoben; einzig O01 ändert eine Entscheidung (E25).
|
||
|
||
| ID | Ergebnis | Umsetzung |
|
||
|---|---|---|
|
||
| O01 (P1) | valide — die Signaturprüfung griff nur bei `hard`; `delayed` und zu `soft` umgestufte 4.x-Berichte zählten ungeprüft, drei gefälschte DSNs sperrten jede Adresse | Jede Art, die auf eine Sperre zählt, braucht die gültige empfängergebundene Signatur. Ohne: `bounces.unsigned_kind` (`BOUNCE_UNSIGNED_KIND`, Standard `policy`, nie hart), falsch signiert: immer `policy`. Folge: auch die Freitextberichte des KAS-Relays (ohne Original-Header) sperren nicht mehr selbst, sie landen zur Sichtung |
|
||
| O02 (P1) | valide — `DB_URL` überschreibt Treiber und Datenbank erst beim Verbindungsaufbau | phpunit.xml leert `DB_URL` (env und server), der Guard in `TestCase::setUpTraits()` bricht bei gesetzter `url` ab |
|
||
| O03 (P2) | valide — `published_ever_at` setzte erst der nachgelagerte Snapshot | `PressReleaseImage::markPublishedIfNowPublic()` in der Freigabetransaktion: Kundenbestätigung, Freigabe der Vorab-Sichtung, Korrektur-Freigabe |
|
||
| O04 (P2) | valide — Restore-Abgleich fand die Zeile per ID | Zuordnung über den Payload-Pfad (UUID); gleiche ID mit anderem Payload = Konflikt, nichts angefasst, Exit ≠ 0 |
|
||
| O05 (P2) | valide — fehlendes `hmac` galt als Altformat; `reregister` ignorierte `invalid` | Unsignierte Exporte nur mit `--accept-unsigned`; `archive:reregister` bricht bei ungültigem/unsigniertem Export ab, bei fehlendem Export nur mit `--without-export` |
|
||
| O06 (P2) | valide — blockierte Meldungen belegten dauerhaft die vordere Seite | Cursor über `(scheduled_at, id)`, Limit zählt bearbeitete Meldungen, Blockaden nur auf ein Scan-Budget (10 × Limit) |
|
||
| O07 (P2) | valide — Direktweg und Nachholer starteten mit geladener Instanz | `ArchivePendingQueue::claim()` (Zeilensperre, neu lesen) in `completeWith()` und `PendingArchiveRetry::retry()`; bereits erledigt = nichts tun, kein Fehlversuch |
|
||
| O08 (P2) | valide — fehlende Snapshot-Datei wurde still ausgelassen | Datei weg: Verweis auf den `image_removed`-/`image_replaced`-Eintrag des Bildes (`archived_in`), sonst Exception, Auftrag bleibt sichtbar offen; Altbestand ohne Snapshot als `snapshot: none` gekennzeichnet |
|
||
|
||
Abschnitt 5 des Audits (Betriebsnachweise, Restore-Übung, Bildbetrieb, Fachfreigaben) bleibt offen wie dort beschrieben; Abschnitt 5.7 (veraltete „offen"-Notizen zu 1.8 und N09) ist unten im Protokoll korrigiert. Echte Parallelität (O03, O07) ist weiter nur logisch geschlossen, nicht mit zwei MySQL-Verbindungen nachgestellt (6.6).
|
||
|
||
**Runbook:** Restore-Schritte 2 und 3 um Pfad-Zuordnung, `--accept-unsigned` und `--without-export` ergänzt.
|
||
|
||
## Nicht aufgenommen
|
||
|
||
- **F21 Legacy-Import.** Befund korrekt (Vollladen der Pivot-Tabellen, kein Transaktionsschutz, Wiederanlauf ändert UUID und Slug). Der Importer läuft seit dem 16.07. nicht mehr und ist nicht im Scheduler. Falls das Altsystem noch einmal einen Delta-Lauf braucht: `importRow()` in `DB::transaction` und vorhandene PM über `(legacy_portal, legacy_id)` statt über die Importmap erkennen. Bis dahin steht nur diese Notiz.
|
||
- **Audit Abschnitt 10** (CSP, Proxy, Bildpixelgrenzen, Retention, PDF, Queue-Priorisierung). Nicht geprüft, nicht belegt. Einzelne Punkte können nach Block 6 als eigene Prüfaufträge folgen.
|
||
- **Große Volt-Dateien zerlegen.** Richtig, aber kein Fehler. Läuft nebenbei, wenn ein Block ohnehin in `press-kits/show` oder `press-releases/show` arbeitet.
|
||
|
||
---
|
||
|
||
## Reihenfolge und Deploys
|
||
|
||
Tatsächlich umgesetzt am 18.09.2026 in fünf Commits auf `technik/betriebsfixes-2026-09`. Block 3 und 4 teilen sich Dateien (User-Modell, PressReleaseService) und wurden deshalb zusammen committet.
|
||
|
||
| Deploy | Inhalt | Commit | Migrationen |
|
||
|---|---|---|---|
|
||
| A | Block 1, Punkte 1.1, 1.2, 1.6, 1.8, 1.9, 1.10 | `0ab53bf` | keine |
|
||
| B | Block 2 komplett | `a7f991f` | Unique-Index `newsletter_campaign_recipients` (räumt Doppelungen vorher weg) |
|
||
| C | Block 1, Punkte 1.3, 1.4, 1.5, 1.7 | `62202a4` | `company_user.granted_via` |
|
||
| D | Block 3 und Block 4 | `f235156` | `contacts_email_idx`; `ki_audits.content_hash/discarded_at`; `users.press_release_quota_period` (COPY, Sekunden) |
|
||
| E | Block 5 und Block 6 | `375f4df` | `user_payment_options.billing_hold_reason/-at` |
|
||
| F | Block 7 | `235d392`, `1cd9fd1`, `ea68362`, 7.13 siehe Protokoll | `user_payment_options.grandfathering_expired_at/-notified_at`; Backfill `users.press_release_quota_period` (DML, Sekunden); Newsletter-Bereinigung angepasst (noch nicht live) |
|
||
|
||
Migrationen berühren `press_releases` und `press_release_search` nicht.
|
||
|
||
### Live-Schritte nach dem Deploy
|
||
|
||
1. `php artisan migrate --force` (acht Migrationen, alle kurz; `users` als einziges COPY, nachts einspielen; die Newsletter-Bereinigung behält je Paar die Zustellzeile). Auf der Dev-Kopie am 18.09.2026 auf MariaDB durchgelaufen: 470 Doppelungen entfernt, `users` in 2 s.
|
||
2. `php artisan company-memberships:audit` – Liste sichten (Tabelle: nicht erklärbare `responsible` seit Go-Live; CSV vollständig in `storage/app`). Danach `--classify --before="<Deploy-Zeitpunkt>"`: der Befehl zeigt vorher Anzahl und Stichprobe der umzustufenden Zeilen; jüngere Zuordnungen tragen ihre Herkunft schon.
|
||
3. `php artisan schedule:list` – neuer Task `billing:expire-grandfathering` um 04:15 muss erscheinen.
|
||
4. Queue-Worker neu starten (Job-Timeouts und `retry_after` 150 greifen erst dann).
|
||
5. Kunden mit aktiver 2FA sehen auf der Sicherheitsseite ein neues Code-Feld vor „Deaktivieren“ und „Neue Codes“; E-Mail-Änderung fragt nach Passwort oder Code.
|
||
6. Nächste Newsletter-Kampagne erst nach diesem Deploy starten – und erst, wenn U05–U07 (E9) umgesetzt sind, falls das Kampagnenmodul in Betrieb geht.
|
||
7. Fortifys 2FA-Verwaltungsrouten (`/user/two-factor-…`) antworten jetzt mit 404; die Sicherheitsseite ist davon unabhängig.
|
||
|
||
## Entscheidungen
|
||
|
||
Am 18.09.2026 alle sechs nach Empfehlung entschieden. Umsetzung ab demselben Tag.
|
||
|
||
| Nr. | Frage | Entscheidung |
|
||
|---|---|---|
|
||
| E1 | Fortify-Profiländerung abschalten oder umbiegen | abschalten |
|
||
| E2 | Auto-Rechte bei Wegfall der Grundlage senken oder entfernen | auf `member` senken – **revidiert durch E7** |
|
||
| E3 | Nachweis vor Sicherheitsaktionen | TOTP- oder Recovery-Code |
|
||
| E4 | Newsletter-Versandreihenfolge fachlich relevant | nein |
|
||
| E5 | Kontingentperiode | Kalendermonat |
|
||
| E6 | Letzte MAN-Periode am Vertragsende | keine Rechnung mehr |
|
||
| E7 | U01: automatische Zuordnung bei Wegfall der Grundlage | Zuordnung entfernen (18.09.2026). `member` ist die normale Teamrolle mit PM-Schreibrechten; eigene PMs bleiben über die Autorenschaft, Eigentum, manuelle Rechte und weiterhin passende Adressen bleiben. Eine automatisch hochgestufte manuelle Mitgliedschaft fällt auf `member` zurück. |
|
||
| E8 | U09: ungenutzte Fortify-2FA-Verwaltungsrouten | abschalten, folgt aus E3 |
|
||
| E9 | U05–U07: Newsletter-Wiederaufnahme und Doppelversand | aufgeschoben, bis das Kampagnenmodul in Betrieb geht; bis dahin nur Abmeldung und Sperrliste live (Nutzer, 18.09.2026) |
|
||
|
||
Entscheidungen aus Block 8 (25.09.2026, bei der Umsetzung getroffen; **alle acht vom Betreiber am 25.09.2026 bestätigt**):
|
||
|
||
| Nr. | Frage | Entscheidung |
|
||
|---|---|---|
|
||
| E10 | N05: Wer darf die Kundenbestätigung eines Admin-Uploads abgeben? | nur der Kontoinhaber der Meldung (`press_releases.user_id`), keine Teammitglieder, kein Super-Admin — er ist der Vertragspartner (DU E1) |
|
||
| E11 | N03: Altbestand an Klassifikationen ohne Audit-Hash | keine Regel „ohne Audit-Hash kein Auto-Publish"; alle 3.387 Alt-Audits haben keinen Hash, die Transaktion schließt das Fenster |
|
||
| E12 | N13: abgelehnte, nie veröffentlichte Einreichungen | Medien nach derselben Frist wie Entwürfe (180 Tage ab letzter Änderung), Status bleibt `rejected` (Vertragsdokumentation nach DU 4.1), früher veröffentlichter Altbestand unangetastet |
|
||
| E13 | 5.1 a: Kopien außerhalb des Archivs | Bestätigungs- und Korrekturzeilen sowie die weich gelöschte Bildzeile werden mit dem Archiv-Tombstone hart gelöscht (E-B10, DSE 9a) |
|
||
| E14 | 5.1 b: Korrektur eines nie veröffentlichten Bildes | kein Archiveintrag; die Korrekturzeile dokumentiert, das Manifest beim späteren `image_removed` enthält alle Korrekturen |
|
||
| E15 | N11: Archivausfall bei Löschung/Anonymisierung | blockiert nicht (Art. 12 Abs. 5 DSGVO), Marker + täglicher Nachholer; nur `deleteFromAdmin` bleibt blockierend |
|
||
| E16 | 5.1 c: Tombstone-Export bei vollständigem Disk-Restore | externes Ziel nicht gebaut; Runbook verlangt, den aktuellen Export vor dem Abgleich einzuspielen — Betreiber entscheidet über Storage Box/S3 |
|
||
| E17 | N01: Cache-Lebensdauer öffentlicher Medien (24 h) | unverändert; ein zurückgehaltenes Bild verschwindet für neue Abrufe sofort, Browser-/CDN-Kopien laufen aus |
|
||
| E18 | N14: Idempotenz der Ablaufmail | Transaktion setzt die DB-Queue voraus (`QUEUE_CONNECTION=database`, kein eigenes `DB_QUEUE_CONNECTION`); bei Wechsel auf Redis Zwei-Spalten-Claim nötig |
|
||
|
||
## Umsetzungsprotokoll
|
||
|
||
| Datum | Punkte | Commit | Bemerkung |
|
||
|---|---|---|---|
|
||
| 18.09.2026 | 1.1, 1.2, 1.6, 1.8 (teilweise), 1.9, 1.10 | `0ab53bf` | Fortify-Profilendpunkt aus, Sync verlangt verifiziertes aktives Konto, Google-2FA (Zusammenführung der drei Login-Abschlüsse in einen Service steht aus), `#[Locked]`, Gate nur für aktive Super-Admins, Route unter `can:users:manage` |
|
||
| 18.09.2026 | 2.1–2.6 | `a7f991f` | Auf der Dev-Kopie 470 doppelte Empfängerzeilen, die Migration räumt sie weg; Zusatzbefund: Sperrlisten-Adressen galten als „gesendet“; `newsletter:resume` |
|
||
| 18.09.2026 | 1.3, 1.4, 1.5, 1.7 | `62202a4` | `granted_via`, `company-memberships:audit` (Standardaufruf ändert nichts, `--classify` schreibt; kein `--dry-run`), Entzug beim Login, TOTP-/Recovery-Nachweis; passwortlose Konten (66.000 im Bestand) können die Adresse weiter ändern |
|
||
| 18.09.2026 | 3.1–3.7, 6.5 | `f235156` | 28 Dateien auf `withoutGlobalScope(PortalScope::class)`, 40 Ausnahmen per Architekturtest eingefroren (Admin, Import, Legacy-Medien, einzelne Services); `EmailQuery` (MySQL direkt, SQLite LOWER); Firmen-Observer; `ReportedUrlResolver` |
|
||
| 18.09.2026 | 4.1–4.6 | `f235156` | Hash-Bindung in beiden Jobs und im Scheduler; Kontingentperiode; Exit-Code; Timeout 120 / retry_after 150; Aggregations-Lock |
|
||
| 18.09.2026 | 5.1–5.3 | `375f4df` | Vertragsende in Auswahl und Sperre; `billing:expire-grandfathering` mit Mail; Sperrvermerk und Seitenlauf (Blocker zählen nicht mehr, Kappe 500 erstellte Rechnungen je Lauf bleibt) |
|
||
| 18.09.2026 | 6.1–6.4 | `375f4df` | `pint --test`, `dev:all`, Doku-Einstieg, Fokusführung ohne neue Abhängigkeit |
|
||
|
||
| 18.09.2026 | 7.1, 7.5, 7.6, 7.8–7.12 | `235d392` | Fortify-2FA-Routen 404, Policy-Kurzschluss nur aktiv, API-Portalwechsel prüft Kategorie, Aggregation mit Zeitbudget und gesperrter Marke, Bereinigung behält `sent`, Sync nach E-Mail-Wechsel, `--classify` mit Vorschau und `--before`, Detailseite sortiert wie Resolver, Protokoll korrigiert |
|
||
| 18.09.2026 | 7.7 | `1cd9fd1` | Vertragsablauf: bedingtes Beenden unter Sperre, Marker, Mail nachholbar mit FAILURE-Exit |
|
||
| 18.09.2026 | 7.2, 7.3, 7.4 | `ea68362` | Bedingtes UPDATE in beiden Jobs, Prüfung im Publish-Lock für `ki`/`scheduler`, `StaleClassificationException`; roter Re-Check einer Review-PM lehnt ab (gilt auch für den Admin-Re-Check, PMs mit `manual_review_required_at` ausgenommen); NULL-Periode = laufender Monat plus Backfill |
|
||
| 25.09.2026 | Block 8: N22, N06 (Gate), 5.3 | `4e34c3d` | Guard in `setUpTraits()`, Flags in phpunit.xml gepinnt, Zustimmung an gezeigte Fassung gebunden, Audit/Decision-Update/Plan versioniert |
|
||
| 25.09.2026 | Block 8: N01–N05, N07, N08, N10–N21, 5.1 a–e, 5.2 | `48129e9` | vier parallel geprüfte und umgesetzte Gruppen (Bilder, Archiv, KI/Hintergrund, Berichte); zwei Migrationen; 12 + 9 + 7 + 8 neue Tests |
|
||
| 29.09.2026 | Block 9: 9.1–9.7 (R-01, K-01–K-09, B-01–B-14, N09, A-01–A-15, R-02–R-11, N17/N18/N19-Rest, Q-01/Q-02/Q-06, OPT-1–OPT-9, E9-Schalter) | `dfb0513`, `1d417f8`, `c2e0b85`, `f0eeef0`, `7cd84b2`, `5ec252d`, `aa47e11` (am 02.10. zusammen mit Block 10 nach Themen committet) | vier parallele Gruppen; acht Migrationen, auf der Dev-Kopie (MySQL) in 0,7 s; neue Befehle `company-memberships:api-assignments`, `archive:reregister`, `archive:verify-export`, `press-release-images:sweep-orphan-originals`; KAS-Passwort aus dem Runbook entfernt |
|
||
| 02.10.2026 | Block 10: O01–O08 aus dem Umsetzungsaudit 02.10. | in denselben Commits (O02 `dfb0513`, O01 `c2e0b85`, O06 `f0eeef0`, O04/O05/O07/O08 `7cd84b2`, O03 `5ec252d`) | alle acht Befunde valide und behoben; keine Migration; E25 revidiert (`BOUNCE_UNSIGNED_KIND=policy`); Runbook-Restore um `--accept-unsigned`/`--without-export` ergänzt |
|
||
|
||
Gesamtsuite nach Block 7: 1.930 Tests grün, einer übersprungen; PHPStan ohne Meldung; Pint sauber.
|
||
|
||
Gesamtsuite nach Block 8 (25.09.2026, HEAD `48129e9`, Befehl `php artisan test --compact` nach `config:clear`): 2.122 Tests grün, einer übersprungen; Larastan Level 4 ohne Meldung auf allen geänderten Dateien; Pint sauber. Offen: N09, Browser-/Tastaturabnahme, MySQL-/Worker-CI, die Messpunkte aus 5.2. Ein Test (`ApiPortalResolutionTest`) brauchte wegen der neuen Kategorieprüfung eine gemeinsame Kategorie statt einer zufälligen.
|
||
|
||
Gesamtsuite nach Block 9 (29.09.2026, Befehl `php artisan test --compact` nach `config:clear`): **2.265 Tests grün, einer übersprungen** — seriell (305 s) und erstmals auch parallel (`--parallel --processes=6`, 57 s); Larastan Level 4 über den vollen Scope ohne Meldung (vorher 1); Pint sauber. Offen: Q-05 (`docs/`), U05–U07 (Newsletter, per Schalter gesperrt), 6.6 MySQL-/Worker-CI, 7.6 Browserabnahme.
|
||
|
||
Gesamtsuite nach Block 10 (02.10.2026, `php artisan test --compact --parallel --processes=6` nach `config:clear`): **2.281 Tests grün, einer übersprungen** (95 s); Larastan Level 4 voller Scope ohne Meldung; Pint sauber.
|
||
|
||
Offen: U05–U07 nach E9 aufgeschoben; 7.6 (Fokusführung) manuell im Browser abnehmen.
|
||
|
||
*Stand 02.10.2026: 1.8 ist mit Block 9 (K-08, `LoginCompletion`) und N09 mit Block 9.5 erledigt; die beiden Absätze unten sind historisch.*
|
||
|
||
Offen aus dem Plan: 6.6 (MySQL-/Worker-Integrationstests) bleibt aufgeschoben; 1.8 nur als Google-Zweig, nicht als Service-Zusammenführung; 4.2 hatte die Prüfung in `publish()` nicht (siehe Block 7, U02). Nachträglich zwei zufallsabhängige Tests festgezurrt (Kategorie-Portal in den API-Tests, Titel in der MAN-PDF-Adresse). Gesamtsuite nach allen Commits: 1.891 Tests grün, einer übersprungen; PHPStan ohne Meldung.
|
||
|
||
**05.10.2026 — Blöcke 9 und 10 live (v1.1.0).** Zusammen mit der
|
||
Betriebsüberwachung Phase 1 und dem Composer-Update ausgeliefert; Ablauf und
|
||
Ergebnisse der Schritte 8–13 des Deploy-Plans in
|
||
[`deploy-v1.1.0.md`](../betrieb/deploy-v1.1.0.md) (Protokoll). Kurz: keine
|
||
offenen Archivaufträge, drei API-Zuordnungen von connektar alle freigeschaltet,
|
||
342 Canonical-Paare neu zugeordnet und 13 veraltete Verweise zurückgesetzt,
|
||
Sweeps leer, Scheduler mit 21 Tasks. Schritte 14–18 (Abnahme) nicht einzeln
|
||
gemeldet; 19 (KAS-Passwort) erledigt 06.10.2026; 20–21 (DSE, Restore-Übung) offen. Q-05 entschieden
|
||
(`docs/` aus dem Repository).
|