dev: Umsetzungsaudits 29.09. und 02.10., Entwicklungsplan Blöcke 9 und 10 mit bestätigten Entscheidungen E19–E34 und Deploy-Plan, Runbook ergänzt

- UMSETZUNGSAUDIT-2026-09-29.md (Bestandsaufnahme) und -2026-10-02.md
- ENTWICKLUNGSPLAN.md: Block 9 und 10, Entscheidungen E19–E34 vom Betreiber
  am 02.10.2026 bestätigt (E25 revidiert), Deploy-Plan A–E, Protokoll
- deploy-runbook.md: vollständige Scheduler-Tabelle, Restore-Schritte mit
  --accept-unsigned/--without-export, Zugangsdaten aus Abschnitt 7 entfernt
  (stehen weiter in der Historie — Passwort wird rotiert)
- Bildplan-Protokoll mit Korrektur zu „Altbestand geschützt" (A-05)
- dev/README.md und dev/roadmap.md verweisen auf den aktuellen Stand
- .env.example: neue Schlüssel aus Block 9/10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-02 13:09:01 +00:00
parent 5ec252da12
commit aa47e113fe
8 changed files with 787 additions and 21 deletions

View file

@ -83,6 +83,9 @@ PRESS_RELEASE_EVENTS_RETENTION_DAYS=395
# Aufbewahrung der Konversions-Ereignisse in Tagen (siehe config/conversion.php).
# Bewusst derselbe Horizont wie oben. 395 = 13 Monate.
CONVERSION_EVENTS_RETENTION_DAYS=395
# Mail-Link-Klick (Stufe 1): derselbe Klick je IP und Meldung zählt höchstens
# einmal in diesem Fenster (Minuten). Audit 29.09.2026, R-08.
CONVERSION_MAIL_CLICK_THROTTLE_MINUTES=30
# ---------------------------------------------------------------------------
# Legacy-Datenbanken (Alt-Systeme, read-only für Migration/Abgleich).
@ -140,6 +143,16 @@ BOUNCE_IMAP_PORT=993
BOUNCE_IMAP_ENCRYPTION=ssl
BOUNCE_IMAP_USERNAME=
BOUNCE_IMAP_PASSWORD=
# Signatur der Bounce-Referenz X-Pk-Ref (Audit 29.09.2026, R-04). Leer = aus
# APP_KEY abgeleitet. Ein Wechsel entwertet nur Signaturen bereits versendeter
# Mails (deren Rückläufer zählen dann weich).
BOUNCE_SIGNING_KEY=
# Bounce OHNE Signatur (hart wie weich): policy = nur protokollieren (sicher,
# Standard); soft = zählt auf den Schwellwert und ist damit fälschbar.
BOUNCE_UNSIGNED_KIND=policy
# Aufbewahrung der Rückläufer-Ereignisse (email_bounces) in Tagen; gelöscht von
# conversion:purge-events. Die Sperrliste bleibt.
BOUNCE_RETENTION_DAYS=395
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
@ -212,6 +225,11 @@ NEWSLETTER_SYNC_API_KEY=
# NEWSLETTER_CAMPAIGN_BATCH_SIZE=50
# NEWSLETTER_CAMPAIGN_BATCH_INTERVAL_MINUTES=15
# Kampagnen-Versand freischalten (E9-Schalter, Audit 29.09.2026). Standard
# aus, bis U05–U07 umgesetzt sind; Entwürfe, Test-Mails und Abmeldung gehen
# immer.
NEWSLETTER_SENDING_ENABLED=false
# Google-Login (Laravel Socialite). Die Redirect-URI muss in der Google Cloud
# Console EXAKT der Callback-URL der Portal-Domain entsprechen (Login läuft auf
# dem Portal-Host, nicht zwingend APP_URL). Prod: https://pressekonto.com/...
@ -275,6 +293,14 @@ TRANSPARENCY_FREE_PORTALS_CHECKED_AT=
# press-releases:hash-content → press-releases:assign-canonicals.
CANONICAL_ASSIGNMENT_ENABLED=true
CANONICAL_FALLBACK=first
# Anmeldelinks „Diesen Newsroom verwalten" je Newsroom und Tag (nur versendete
# zählen). Audit 29.09.2026, R-09: 3 → 5.
NEWSROOM_CLAIM_LINKS_PER_DAY=5
# API-Zuordnung neuer Firmen zu einem bestehenden Newsroom (Roadmap 1a.4,
# Audit 29.09.2026, R-01): nur für diese User-IDs (kommagetrennt, z. B. das
# connektar-Konto) — sonst nur, wenn der Nutzer der Firma schon angehört.
# Leer = keine Freischaltung.
NEWSROOM_API_ASSIGNMENT_USER_IDS=
# --- Bildkennzeichnung (Formular v2) und Beweisarchiv (Block F) ---
# IMAGE_RIGHTS_V2: Formular v2 im Bildmanager und in der API; false = Verhalten wie vor der Umstellung.
@ -283,7 +309,10 @@ IMAGE_RIGHTS_V2=false
CONFIRMATION_IP_KEY=
# Beweisarchiv: eigener Schlüssel (php artisan archive:key), getrennt sichern — ohne ihn ist das Archiv unlesbar.
ARCHIVE_KEY=
ARCHIVE_PREVIOUS_KEYS=
ARCHIVE_RETENTION_YEARS=3
ARCHIVE_QUARANTINE_DAYS=30
ARCHIVE_SWEEP_PAUSED=false
# Zustimmungsgate vor jeder Einreichung (Block G) — erst mit AGB v0.7 einschalten.
LEGAL_ACCEPTANCE_GATE=false

View file

@ -1,6 +1,6 @@
# Entwicklungsplan zum Audit vom 11.09.2026
**Stand:** 18.09.2026 (Entscheidungen getroffen, Umsetzung läuft)
**Stand:** 29.09.2026 (Blöcke 1–9 umgesetzt; Block 9 = Umsetzungsaudit 29.09., Deploy offen)
**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.
@ -281,6 +281,111 @@ Grundlage: [`UMSETZUNGSAUDIT-2026-09-25.md`](UMSETZUNGSAUDIT-2026-09-25.md) (22
**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 (`docs/` lokal gelöscht, nicht committet) entscheidet der Betreiber.** |
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).
### 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** — stand bis 29.09. im Runbook und liegt in der Git-Historie und auf `origin`.
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.
@ -361,11 +466,19 @@ Entscheidungen aus Block 8 (25.09.2026, bei der Umsetzung getroffen; **alle acht
| 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) | — (nicht 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. | — (nicht committet) | 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.

View file

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

View file

@ -0,0 +1,201 @@
# Umsetzungsaudit vom 02.10.2026
**Grundlagen:** [Entwicklungsplan](ENTWICKLUNGSPLAN.md), [Umsetzungsaudit vom 29.09.2026](UMSETZUNGSAUDIT-2026-09-29.md). Schwerpunkt ist die als erledigt dokumentierte Nacharbeit in Block 9, einschließlich ihrer Wechselwirkungen mit Block 8.
**Geprüfter Stand:** Arbeitskopie auf `technik/betriebsfixes-2026-09`, HEAD `98c465b691752d15a0e07eceb6f927ddda184e90`, einschließlich der bereits vorhandenen Änderungen und unversionierten Dateien. **HEAD allein enthält die geprüfte Umsetzung von Block 9 nicht.** Die im Plan beschriebenen neuen Services, Middleware, Migrationen und Tests liegen teilweise noch unversioniert vor.
**Auftrag:** Prüfung und Dokumentation. Es wurden keine Anwendungsdateien, Tests, Konfigurationen oder Bestandsdaten geändert, keine Migrationen und keine Betriebsbefehle ausgeführt. Einzige neu angelegte Datei ist dieses Audit. Empfehlungen sind nicht umgesetzt.
## 1. Ergebnis
**Die Umsetzung ist deutlich verbessert, aber noch nicht vollständig sachgerecht abgeschlossen.** Die wesentlichen Korrekturen gegen fremde Firmenzuordnung, Google-Vorabübernahme, automatische Veröffentlichung trotz manueller Prüfsperre und ungefilterte Bilder in Feed/PDF sind im Code vorhanden. Mehrere Fehlerpfade bleiben jedoch offen.
Diese Prüfung ergibt **acht konkrete Restbefunde: zwei P1 und sechs P2**. Die P1 betreffen unterschiedliche Bereiche: eine von außen erreichbare Mail-Sperrlücke und einen bedingten Datenverlust beim Entwicklertest. Hinzu kommen offene Betriebsnachweise; diese sind keine nachgewiesenen Produktionsvorfälle.
| ID | Priorität | Befund | Bezug zum letzten Audit/Plan |
|---|---|---|---|
| O01 | **P1 – Sicherheit** | Unsignierte weiche Rückläufer können fremde Mailadressen sperren | R-04, E25, Block 9.6 |
| O02 | **P1 – Testdaten-Sicherheit** | `DB_URL` kann den Schutz vor Tests gegen eine echte Datenbank umgehen | N22, Qualitätsgates |
| O03 | P2 | Nachträglich freigegebene Bilder können nach Snapshotfehler als „nie veröffentlicht“ gelöscht werden | N09, B-05, Block 9.5 |
| O04 | P2 | Restore-Abgleich kann bei wiederverwendeten IDs einen anderen Archiveintrag löschen | A-03/A-06, Block 9.4/9.5 |
| O05 | P2 | Tombstone-Signatur ist durch den Altformat-Rückfall umgehbar; Wiederregistrierung ignoriert ungültige Exporte | A-03/A-15, E31 |
| O06 | P2 | Blockierte Meldungen können alle späteren Terminveröffentlichungen verdrängen | K-02, Block 9.6 |
| O07 | P2 | Archivauftrag wird von Direktweg und Nachholer nicht exklusiv beansprucht | A-02, Block 9.4 |
| O08 | P2 | Nachgeholte Depublikation kann ohne die zugesagten Bilddateien erfolgreich abgeschlossen werden | A-04, Block 9.5 |
**Bewertungsmaßstab:** P1 vor Nutzung des betroffenen Ablaufs schließen; P2 vor fachlicher Abnahme des jeweiligen Ablaufs beheben. Alle acht Befunde sind durch den gelesenen Kontrollfluss belegt. Keiner wurde in dieser Sitzung durch einen schreibenden Test oder gegen Live ausgenutzt. Konkrete Bestandsbetroffenheit und Häufigkeiten wurden nicht erhoben.
## 2. Prüfumfang und Grenzen
Geprüft wurden insbesondere die Änderungen an Authentifizierung und Firmenrechten, Veröffentlichung und Scheduler, Bildbestätigung und Bildlöschung, Snapshot-Versionierung, Archivaufträgen und Restore, Bounce-Verarbeitung und Newsletter-Sperre. Ergänzend wurden die Änderungen an Canonicals, Sitemap/Änderungsdatum, Merge/Unmerge, Zustimmungsgate, Berichten und die zugehörigen Regressionstests gelesen. Bei den älteren Blöcken erfolgte eine gezielte Rückprüfung der berührten Schutzmechanismen, keine vollständige Wiederholung aller historischen Audits.
Die Laravel-Boost-Paketinformationen und die Dokumentationssuche wurden verwendet; Framework-Verhalten beim Datenbank-URL-Override wurde zusätzlich direkt am installierten Laravel-Code geprüft. Die Testkonfiguration und ihre Schutzmechanismen wurden gesondert gelesen.
| Prüfung | Ergebnis dieser Sitzung |
|---|---|
| Plan/Audit gegen Arbeitskopie, Änderungen und neue Dateien | durchgeführt |
| Quelltextprüfung der genannten Aufrufketten und vorhandenen Tests | durchgeführt |
| Sail-Verfügbarkeit: `vendor/bin/sail php -v` | Abbruch: `Docker or Podman is not running.` |
| Pest, Larastan, PHP-Lint, Browser- und MySQL-Parallelitätstests | **nicht ausgeführt** |
| Datenbankabfragen, Mailversand, Archiv-/Restore-Kommandos | **nicht ausgeführt** |
| Live-Konfiguration, Deployment, Schlüssel-/Passwortrotation | nicht verifiziert |
Die im Entwicklungsplan angegebenen **2.265 erfolgreichen Tests, ein übersprungener Test sowie grüne Larastan-/Pint-Läufe** sind historische Angaben vom 29.09.; sie wurden hier nicht erneut bestätigt. Tests würden zudem Testdaten, Logs, Storage-Fakes und gegebenenfalls kompilierte Views schreiben. Es wurden weder Container gestartet noch Caches gelöscht, um den reinen Auditauftrag einzuhalten.
Die folgenden Zeilenangaben beziehen sich auf die geprüfte Arbeitskopie; nach weiteren Änderungen können sie sich verschieben.
## 3. Was am Code nachvollziehbar umgesetzt ist
„Umgesetzt“ bedeutet in dieser Tabelle: Schutz beziehungsweise Korrektur ist im geprüften Code vorhanden. Es ist keine Bestätigung eines erfolgreichen Live-Deployments oder einer vollständig bestandenen Laufzeitprüfung.
| Bereich / alte IDs | Stand und Beleg |
|---|---|
| Fremde Firmenzuordnung, R-01 | `ApiNewsroomAssignment::assign()` prüft vor Pivot und Portaländerung bestehende Zugehörigkeit beziehungsweise Allowlist. Neue API-Pivots tragen `granted_via = api`; der Bestandsprüfbefehl ist vorhanden. Die Bereinigung bereits entstandener Fremdrechte bleibt ein Betriebsnachweis. |
| Google-Vorabübernahme, K-04 | `SocialAuthService::resolveUser()` verwirft bei Verifikation eines offenen Kontos Passwort, Remember-Token und Sanctum-Tokens; `SessionRevocation` und die Web-Middleware behandeln alte Sessions. Regressionstest vorhanden. Persistenz und Ausfallverhalten des Cache-basierten Widerrufs wurden nicht dynamisch geprüft. |
| Gemeinsamer Loginabschluss, K-08 / Plan 1.8 | `LoginCompletion` ist vorhanden und in Google-, Magic-Link-, Volt-Passwort- und Fortify-Abschluss eingebunden. Die pauschale spätere Plan-Notiz „1.8 nur als Google-Zweig“ ist für diese Arbeitskopie veraltet. |
| Manuelle Prüfung, K-01 | Scheduler filtert `manual_review_required_at`; `publish()` prüft bei automatischen Quellen zusätzlich unter der PM-Sperre. Der Adminweg bleibt die vorgesehene manuelle Entscheidung. |
| Öffentliche Bilder, B-01/B-02 | Startseiten-Feed nutzt `publiclyVisible()`, Card-Helfer und PDF prüfen die Auslieferbarkeit. Auch die Medienroute verwendet den gemeinsamen Freigabezustand. |
| Uploadgrenze, B-03 | Temporäre Livewire-Uploadregel ist auf 16 MiB angehoben. Die PHP-/Proxygrenzen sind damit noch nicht nachgewiesen. |
| Bestätigung, B-04/B-14 | Gesperrte Bild-ID und SHA in der Kundenkomponente; Service prüft Hash und aktuellen Bildzustand unter Bildsperre. |
| Entfernen und Rückhaltegründe, B-05/B-07 | Remover lädt PM und Bild unter Sperren neu. `precheck_withheld_at` und `withheld_at` sind getrennt. Rest: O03. |
| Veröffentlichungssnapshots, N09 | Dateifassung enthält Bytes-/Metadatenbezug, OG-Datei und JSON; Schreibfehler werden erkannt und Fassungen beim Bildarchivieren eingesammelt. Rest: O03 und O08; ein allgemeiner Snapshot-Nachholer fehlt. |
| Archivaufträge, A-01/A-02/A-04 | Auftragstabelle ersetzt den einzelnen Marker, Textstände werden im verschlüsselten Auftrag festgehalten; Nachholer ohne Schlüssel bricht ab. Rest: O07/O08. |
| Restore, A-03/A-06 | Quarantäne, Sweep-Pause, Wiederregistrierung und Bereinigung von Anwendungszeilen sind vorhanden. Rest: O04/O05. |
| Änderungsdatum/Sprache/Canonical, R-02/R-03/N19 | Aggregation und PM-Verschiebung beim Merge vermeiden `updated_at`; `content_changed_at` wird für Inhaltsänderungen verwendet; Sitemap berücksichtigt die Sprache; Archivierung stößt Canonical-Neuzuordnung an. |
| Merge/Unmerge, R-05 | Eingefüllte Feldwerte werden protokolliert; Unmerge prüft vor Rücksetzen, ob der Wert noch dem eingefüllten Wert entspricht. |
| Zustimmungsgate, K-07 | `legalAcceptanceVersions` ist `#[Locked]`; abweichende Fassungen und fehlende Pflichtdokumente verhindern die Einreichung. |
| Newsletter, E9 | Versand standardmäßig aus; Start, Resume-Befehl und Versandjob berücksichtigen die Sperre. U05–U07 sind weiterhin aufgeschoben und dadurch nicht behoben. |
| Tests/Qualitätsgates, Q-01/Q-02 | Gemeinsamer Testhelfer steht in `tests/Pest.php`; die beanstandete Slug-Annotation wurde entfernt. Aktueller Lauf nicht bestätigt; zusätzliche Guard-Lücke O02. |
## 4. Restbefunde mit Abhilfe
### O01 · P1 · Bounce-Signatur schützt nicht vor Sperren über weiche Rückläufer
**Fundstellen:** `app/Services/Mail/BounceMailProcessor.php:58–98, 119–147`; `app/Services/Mail/EmailSuppressionService.php:130–163, 221–257`; `config/bounces.php:45–57`.
**Befund:** Die Signatur wird berechnet, aber ein ungültiger Nachweis wird nur behandelt, wenn `$kind === Hard` ist. `delayed` ergibt sofort `Soft`; auch ein zunächst harter Rückläufer kann vorher aufgrund seines Status in `Soft` umklassifiziert werden. Beide Wege gelangen ungeprüft in die Sperrzählung. Drei weiche Ereignisse innerhalb von sieben Tagen genügen mit der Standardkonfiguration für eine aktive Sperre.
**Nachvollziehbarer Fall:** Drei unsignierte DSNs mit jeweils eigener Message-ID, derselben fremden Empfängeradresse und `Action: delayed` erreichen das verarbeitete Bounce-Postfach. Der Parser liefert weiche Rückläufer; die Signatur hat auf diesen Zweig keinen Einfluss. Die Sperrliste blockiert anschließend auch transaktionale Mails an diese Adresse. Voraussetzung ist, dass solche Nachrichten das konfigurierte Postfach und den Abhollauf erreichen; eine vorgelagerte Absenderauthentisierung ist in dieser Codekette nicht implementiert.
**Zusätzlich:** Im dokumentierten Übergang `BOUNCE_UNSIGNED_HARD_KIND=soft` ist derselbe Effekt auch mit drei unsignierten harten Rückläufern möglich. **Die spätere Umstellung auf `policy` schließt den direkten Soft-Pfad nicht.**
**Abhilfe:** Für jeden Rückläufer, der eine Empfängersperre beeinflussen kann, einen gültigen empfängergebundenen Nachweis verlangen. Ohne Nachweis ausschließlich zur Sichtung protokollieren, ohne Soft-/Hard-Sperrzähler. Eine bewusst unsichere Übergangsregel ist kein vollständiger R-04-Fix.
**Regressionstest:** Mehrere unsignierte beziehungsweise falsch signierte `delayed`- und `failed/4.x`-DSNs mit verschiedenen Message-IDs dürfen auch oberhalb des Soft-Schwellwerts keine Sperre erzeugen. Beide Werte von `unsigned_hard_kind` abdecken. Gültig signierte Soft-Bounces müssen weiterhin regulär zählen.
**Vorhandene Tests:** `BounceMailProcessingTest` prüft den einzelnen unsignierten Hard-Bounce und wiederholte falsch signierte Hard-Bounces, aber nicht diese wiederholten Soft-Wege.
### O02 · P1 · Testdatenbank-Guard ignoriert den URL-Override
**Fundstellen:** `tests/TestCase.php:31–54`; `phpunit.xml:30–33`; `config/database.php:58–61`; `vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php:224–225`; `vendor/laravel/framework/src/Illuminate/Support/ConfigurationUrlParser.php:36–51`; `tests/Pest.php:27–28`.
**Befund:** Der Guard kontrolliert den Verbindungsnamen und die rohen Konfigurationswerte `driver=sqlite`, `database=:memory:`. Die SQLite-Konfiguration erlaubt daneben `url=env('DB_URL')`. Laravel löst diese URL später auf und überschreibt damit unter anderem Treiber und Datenbank. PHPUnit erzwingt `DB_CONNECTION` und `DB_DATABASE`, neutralisiert `DB_URL` jedoch nicht.
**Nachvollziehbarer Fall:** Die Testumgebung erbt eine erreichbare reale Datenbank über `DB_URL`. Die rohen SQLite-/Memory-Werte passieren den Guard; die tatsächliche Verbindung wird anschließend aus der URL gebildet. `RefreshDatabase` kann dann `migrate:fresh` gegen die reale Datenbank starten und Tabellen löschen.
**Einordnung:** Das ist ein bedingtes Entwicklungs-/CI-Risiko, keine behauptete aktuelle Löschung und kein öffentlicher Webangriff. In den geprüften lokalen Env-Dateien wurde kein `DB_URL`-Eintrag festgestellt; die gesamte Prozess-/CI-Umgebung ist damit nicht ausgeschlossen.
**Abhilfe:** `DB_URL` für Tests ausdrücklich neutralisieren. Zusätzlich die vollständig aufgelöste Verbindungskonfiguration prüfen beziehungsweise jede nicht leere URL bereits vor den DB-Traits ablehnen. Der bestehende frühe Zeitpunkt in `setUpTraits()` ist richtig und muss erhalten bleiben.
**Regressionstest:** Nur den Konfigurationswert `database.connections.sqlite.url` auf eine Nicht-Test-URL setzen und verlangen, dass der Guard ohne Verbindungsaufbau abbricht. Keine echte Datenbank für diesen Negativtest verwenden. `TestDatabaseGuardTest` deckt bisher nur einen anderen Default-Verbindungsnamen ab.
### O03 · P2 · Veröffentlichungsmarke hängt bei späterer Bildfreigabe am Snapshot-Erfolg
**Fundstellen:** `app/Services/PressRelease/Images/PressReleaseImageUploader.php:324–364`; `app/Services/Image/Precheck/ImagePrecheckReviewService.php:31–55`; `app/Services/PressRelease/Images/ImageCorrectionService.php:100–145`; `app/Services/PressRelease/Images/PublishedImageSnapshot.php:128–145`; `app/Services/PressRelease/Images/PressReleaseImageRemover.php:86–119`.
**Befund:** Beim normalen PM-Publish wird die Veröffentlichungsmarke in der Transaktion gesetzt. Bei nachträglicher Kundenbestätigung an einer bereits veröffentlichten PM wird dagegen zunächst nur `rights_confirmed_at` gespeichert. Das Bild ist damit öffentlich auslieferbar. `published_ever_at` setzt erst der nachgelagerte Snapshot nach erfolgreichen Dateischreibvorgängen. Ähnlich arbeiten die Freigabe der Vorab-Sichtung und die erstmalige Freigabe im Korrekturweg. Snapshotfehler werden lediglich gemeldet.
**Nachvollziehbarer Fall:** Admin lädt an einer veröffentlichten PM ein neues Bild hoch; Kunde bestätigt; Snapshot-Schreiben scheitert. Die Medienroute kann das Bild ausliefern, `published_ever_at` bleibt aber NULL. Beim späteren Entfernen nimmt der Remover den Zweig „nie veröffentlicht“ und löscht Original, Bestätigung und Bildzeile ohne `image_removed`-Archivauftrag. Ein konkurrierendes Entfernen im Zeitfenster vor dem Snapshot kann denselben Fehler auslösen.
**Abhilfe:** Bei jedem Übergang zur öffentlichen Auslieferbarkeit die Veröffentlichungsmarke innerhalb einer mit dem Remover koordinierten PM-/Bildtransaktion setzen. Snapshot-Erstellung als separat nachholbare Arbeit führen. Ein fehlgeschlagener Nachweis darf aus einem tatsächlich freigegebenen Bild kein „nie veröffentlichtes“ machen.
**Regressionstest:** Nachträgliche Kundenbestätigung, Precheck-Freigabe und Korrektur-Freigabe an veröffentlichter PM jeweils mit fehlgeschlagenem Snapshot-Schreiben prüfen. Danach entfernen: Beweisdateien müssen archiviert oder für einen offenen Archivauftrag erhalten werden. Ergänzend MySQL-Test für Freigabe gegen Entfernen mit zwei Verbindungen.
**Vorhandene Tests:** `ImagePipelineAuditTest:299` deckt den Schreibfehler beim normalen PM-Publish ab; dort wird die Marke bereits separat gesetzt. Dieser Test beweist die nachträglichen Freigabewege nicht.
### O04 · P2 · Restore-Tombstones werden nur über die numerische ID zugeordnet
**Fundstellen:** `app/Services/Archive/EvidenceArchive.php:450–480, 817–834`.
**Befund:** Der Restore-Abgleich löscht zunächst den im Export genannten Dateipfad, sucht die zu tombstonende Datenbankzeile aber ausschließlich mit `find(exportierte ID)`. Es wird nicht geprüft, ob deren `payload_path` zum Export passt. Die neue Wiederregistrierung erzeugt ihrerseits frische Auto-Increment-IDs.
**Nachvollziehbarer Fall:** Nach Rückkehr zu einem älteren DB-Stand wird ID 101 für Payload B verwendet, während der aktuelle Tombstone-Export ID 101 für die früher gelöschte Payload A enthält. Der Abgleich markiert B als gelöscht, bereinigt gegebenenfalls die Anwendungskopien von B und löscht in der anschließenden Tombstone-Schleife auch dessen Payload. Die HMAC kann dabei vollständig gültig sein: Falsch ist die Identitätszuordnung. Wiederregistrierung in anderer Reihenfolge und ein erneuter Abgleich sind ein weiterer Weg zu abweichenden IDs.
**Abhilfe:** Den UUID-haltigen Payload-Pfad beziehungsweise eine dauerhaft gespeicherte Archiv-UUID als Wiederherstellungsidentität verwenden. ID und Pfad müssen vor jeder destruktiven Aktion konsistent sein; bei Konflikten abbrechen und melden. Eine fremde Zeile darf niemals allein aufgrund der alten ID tombstoned werden.
**Regressionstest:** Gültiger Export für ID X/Pfad A, aktuelle DB-Zeile mit ID X/Pfad B. B samt Anwendungskopien bleibt erhalten. Zusätzlich Wiederregistrierung mit geänderten IDs und wiederholten Restore-Abgleich prüfen.
### O05 · P2 · Ungültige und unsignierte Tombstone-Exporte werden nicht sicher abgegrenzt
**Fundstellen:** `app/Services/Archive/EvidenceArchive.php:560–564, 847–888`; `app/Console/Commands/ArchiveReregister.php:29–49`; `app/Console/Commands/ArchiveReconcileAfterRestore.php:27–42`.
**Befund A – Signatur-Rückfall:** Fehlt `hmac` oder ist es kein String, behandelt `readTombstoneExport()` den Export als zulässiges Altformat und gibt die Zeilen ungeprüft zurück. Ein veränderter neuer Export wird somit verwendbar, sobald sein HMAC-Feld entfernt oder auf NULL gesetzt wird. Das Dateiformat selbst weist nicht vertrauenswürdig nach, dass es tatsächlich vor der Signaturumstellung entstand. Betroffen ist der zugesagte Integritätsschutz einer Löschliste; ein anonymer Zugriff auf diese Datei wurde nicht festgestellt.
**Befund B – Wiederregistrierung:** Bei ungültiger Signatur liefert der Leser eine leere Liste plus Status `invalid`. `reregister()` übernimmt nur die Liste und verwirft den Status. Der Befehl kann daraufhin Payloads als neue Einträge registrieren, die laut dem eigentlich maßgeblichen Export gelöscht bleiben müssten, und dennoch Erfolg melden. Das betrifft auch einen Export, dessen früherer Signaturschlüssel versehentlich nicht konfiguriert ist.
**Abhilfe:** Für den Normalbetrieb gültig signierte Exporte verlangen. Altformat-Import nur über einen ausdrücklich ausgewählten, kontrollierten Migrationsweg zulassen. Ungültige Exporte müssen Wiederregistrierung und destruktiven Exportabgleich stoppen. Für fehlende Exporte eine ausdrückliche Restore-Regel vorsehen, statt Unkenntnis still als „nichts gelöscht“ zu behandeln.
**Regressionstest:** Manipulierten Export zusätzlich ohne `hmac` und mit `hmac=null` prüfen. Bei ungültiger Signatur darf `archive:reregister` keine Zeile erzeugen und muss fehlschlagen. Der vorhandene Manipulationstest prüft nur geänderte Einträge bei weiterhin vorhandenem HMAC im Reconcile-Befehl.
### O06 · P2 · Dauerhaft blockierte Kandidaten verstopfen den Veröffentlichungslauf
**Fundstellen:** `app/Console/Commands/PublishScheduledPressReleases.php:68–98, 132–180`.
**Befund:** Jeder Lauf lädt nur die ältesten `--limit` Kandidaten, standardmäßig 200. Meldungen ohne Kontingent oder ohne Bildbestätigung bleiben mit unverändertem Status und Termin in derselben Auswahl. Ein Fortsetzen hinter diesen Kandidaten findet nicht statt. Der Lauf endet bei ausschließlich fachlichen Blockaden erfolgreich.
**Nachvollziehbarer Fall:** 200 ältere fällige Meldungen sind blockiert, eine jüngere ist vollständig veröffentlichungsfähig. Jeder Lauf verarbeitet erneut dieselben 200. Die jüngere Meldung erscheint nie, solange der blockierte Bestand die vordere Auswahl füllt. Zum kleinen Regressionstest genügen `--limit=2`, zwei blockierte und eine zulässige Meldung.
**Abhilfe:** Kandidaten fair und mit stabilem Cursor durchlaufen; fachliche Blockaden dürfen nicht dauerhaft das gesamte Laufbudget belegen. Beispielsweise Seiten nach `(scheduled_at, id)` mit getrenntem Scan-/Zeitbudget verarbeiten oder blockierte Kandidaten mit einer kontrollierten Wiedervorlage versehen. Allein das Limit zu erhöhen verschiebt den Fehler.
**Regressionstest:** Die zulässige Meldung hinter einer vollen Seite blockierter Kandidaten wird im selben oder einem nachweislich fortschreitenden Folgelauf veröffentlicht. Blockaden bleiben sichtbar und werden weiterhin erneut geprüft.
### O07 · P2 · Archivaufträge sind trotz Transaktionen nicht gegen Doppelverarbeitung geschützt
**Fundstellen:** `app/Services/Archive/ArchivePendingQueue.php:88–99, 123–135`; `app/Services/Archive/PendingArchiveRetry.php:58–69, 108–140`; `app/Console/Commands/ArchiveRetryPending.php:43–53`; `app/Services/Archive/EvidenceArchive.php:138–165`.
**Befund:** Sowohl `completeWith()` als auch `retry()` starten die Archivierung mit einer bereits geladenen Auftraginstanz. Keiner der Wege lädt und beansprucht die Auftragszeile exklusiv unter Sperre; auch das zwischenzeitliche Löschen des Auftrags wird vor der Archivierung nicht geprüft. Für die neuen `pm_depublished`-/`gdpr_change`-Aufträge gibt es keine eindeutige, im Archiveintrag gespeicherte Auftragsidentität.
**Nachvollziehbarer Fall:** Der Direktweg hat seinen fachlichen Commit abgeschlossen, aber noch nicht archiviert. Der Nachholer liest den nun sichtbaren Auftrag. Beide erzeugen ein Archiv-ZIP und eine Archiveintrag-Zeile; erst anschließend löschen sie denselben Auftrag. Die zweite Löschung mit null Treffern rollt den zweiten Archiveintrag nicht zurück. Auch zwei manuell gleichzeitig gestartete Nachholer können dies auslösen. `withoutOverlapping()` am Scheduler koordiniert den HTTP-Direktweg nicht.
**Auswirkung:** Ein Ereignis bekommt mehrere Nachweise und gegebenenfalls unterschiedliche Erfassungs-/Aufbewahrungszeitpunkte. Bei Bildereignissen konkurrieren zusätzlich Dateilesen und die Bereinigung lokaler Beweisdateien. Das Versprechen „ein Auftrag je Ereignis, einmaliger Abschluss“ ist damit nicht vollständig umgesetzt.
**Abhilfe:** Beide Wege über denselben atomaren Claim beziehungsweise dieselbe Auftragssperre führen und nach Sperrerwerb das weitere Bestehen prüfen. Eine dauerhafte eindeutige Ereignis-/Auftrags-ID am Archiveintrag verhindert Doppelablage zusätzlich. Externe Dateioperationen und Wiederholung nach Prozessabbruch ausdrücklich einbeziehen.
**Regressionstest:** Zwei Verbindungen/Prozesse bearbeiten denselben Auftrag; genau ein vollständiger Archiveintrag entsteht, der Marker wird erst nach Abschluss entfernt. Zusätzlich eine bereits gelöschte, aber noch im Speicher gehaltene Auftraginstanz erneut verarbeiten: keine weitere Ablage. Keine dynamische MySQL-Reproduktion in dieser Sitzung.
### O08 · P2 · Depublikationsauftrag hält Bildpfade fest, aber nicht deren Verfügbarkeit
**Fundstellen:** `app/Services/Archive/PressReleaseArchiver.php:123–145, 154–193`; `app/Services/PressRelease/Images/PressReleaseImageRemover.php:149–180`; `app/Services/Archive/PendingArchiveRetry.php:108–128`; `app/Services/Archive/ArchivePendingQueue.php:90–96`.
**Befund:** Der festgehaltene Depublikationsstand enthält den Snapshot-Pfad und Bildmetadaten. Die Bytes werden erst beim späteren Archivieren gelesen. Existiert der Pfad dann nicht mehr, wird die Datei ohne Fehler ausgelassen. `recordDepublished()` erzeugt trotzdem einen Eintrag, wodurch der Auftrag als erfolgreich erledigt gilt.
**Nachvollziehbarer Fall:** Depublikation bei Archivstörung erzeugt einen offenen Auftrag. Nach Behebung der Störung wird das Bild vor dem Nachholer entfernt und regulär als `image_removed` archiviert; der Remover bereinigt seine lokalen Snapshots. Der spätere `pm_depublished`-Eintrag enthält nun den Text und Bildmetadaten, aber keine Bilddatei. Der Auftrag verschwindet ohne Hinweis auf einen unvollständigen Nachweis. Die Datei kann noch im getrennten Bildarchiv vorhanden sein, wird hier aber weder übernommen noch verbindlich referenziert. Bei unabhängigem Dateiverlust fehlt auch dieser Rückweg.
**Abhilfe:** Unveränderliche Snapshot-Dateien bis zum Abschluss aller referenzierenden Aufträge erhalten oder den Auftrag auf eine dauerhaft verfügbare archivierte Fassung verweisen lassen. Erwartete, aber fehlende Dateien dürfen nicht still als vollständige Archivierung gelten. Für v2-Bilder Vollständigkeit anhand der im Auftrag festgehaltenen Fassung prüfen; Altbestand ohne Snapshot gesondert kennzeichnen.
**Regressionstest:** Depublikation mit fehlgeschlagener Archivierung, danach erfolgreiche Bildentfernung, danach Retry. Der Depublikationsnachweis enthält die richtige damalige Bildfassung oder eine belastbare Archivverknüpfung. Bei fehlenden Bytes bleibt ein sichtbarer Fehler/offener Auftrag bestehen. Der vorhandene A-04-Test prüft spätere Textänderungen, nicht diese Dateilebensdauer.
## 5. Offene Punkte und Hinweise ohne neuen bestätigten Laufzeitbefund
1. **Deployment und Rechtebereinigung:** Block 9 ist in dieser Arbeitskopie uncommittet. Ob die acht Migrationen, die Allowlist, Worker-Neustarts und die Prüfung alter API-Zuordnungen auf Live durchgeführt wurden, ist nicht belegt. Die vorhandenen neuen Dateien müssen beim späteren Commit/Deploy vollständig erfasst werden. Eine neue Sperre am API-Einstieg entzieht bereits vorhandene Fremdrechte nicht rückwirkend.
2. **Bekanntes Zugangsdatenleck:** Der Entwicklungsplan dokumentiert ein aus der Arbeitskopie entferntes KAS-Passwort, das in der Git-Historie verblieben ist. Der Wert wurde hier weder ausgegeben noch in das Audit übernommen. Die erfolgreiche Rotation ist aus dem geprüften Stand nicht nachgewiesen und bleibt vorrangig zu bestätigen. Das ist ein offener Betriebsnachweis, keine Feststellung einer erfolgten Kompromittierung.
3. **Newsletter:** `NEWSLETTER_SENDING_ENABLED` ausgeschaltet lassen, bis U05–U07 umgesetzt und geprüft sind. Der neue Schalter ersetzt deren fachliche Korrektur nicht.
4. **Restore-Abnahme:** DB-alt/Disk-neu, Disk-alt/DB-neu und beide-alt inklusive Export, wiederverwendeten IDs, Signaturfehlern, Quarantäne und erneuten Abgleichen in einer isolierten Umgebung testen. Bei Wiederregistrierung sind Legal Holds laut bestehendem Code/Runbook zusätzlich zu prüfen. Ein externes Tombstone-Ziel bleibt nach E16 offen.
5. **Bildbetrieb:** Proxy-/PHP-Uploadgrenzen, Speicherbedarf großer Bilder, Zeitbudget des synchronen Vorabchecks und Browser-/CDN-Verhalten nach OG-Korrekturen sind weiterhin nicht durch diese Prüfung belegt. Das sind die bereits dokumentierten Mess-/Abnahmepunkte, keine hier gemessenen neuen Ausfälle.
6. **Qualität:** MySQL-/Worker-Integration und Browser-/Tastaturabnahme bleiben offen. Die gelesenen Tests belegen gute Abdeckung der normalen und vieler einzelner Fehlerwege, aber nicht die oben aufgeführten Kombinationen und Parallelabläufe. Volt-Code ist nicht vollständig durch den bisherigen PHPStan-Scope abgesichert.
7. **Dokumentationsstand:** Die lokalen Löschungen unter `docs/` sind weiterhin vorhanden und wurden nicht verändert. Der Plan enthält neben dem aktuellen Block-9-Stand noch historische „offen“-Aussagen zu bereits zusammengeführten Loginabschlüssen und Snapshot-Versionierung. Bei einer späteren Dokumentationspflege historische Protokolle deutlich von der aktuellen Restliste trennen.
8. **Weitere Fachfreigaben:** Die im letzten Audit benannten Freigaben für Rechtstexte, Vorabcheck und spätere Bildfunktionen werden durch dieses technische Audit nicht ersetzt. E19–E34 sind im Plan ausdrücklich als noch zu bestätigende Entscheidungen gekennzeichnet; eine spätere Bestätigung wurde hier nicht festgestellt.
## 6. Empfohlene nächste Umsetzung
| Reihenfolge | Arbeit | Abnahmebedingung |
|---|---|---|
| 1 | O01: Signaturnachweis für jede sperrwirksame Bounce-Art | Auch wiederholte unsignierte Soft-DSNs sperren keine Adresse. |
| 2 | O02: Testumgebung und tatsächliche DB-Konfiguration absichern | URL-Override wird vor Verbindung und `RefreshDatabase` abgefangen. |
| 3 | O03: Bildfreigabe, Veröffentlichungsmarke und Nachweisauftrag koordinieren | Ein öffentlich freigegebenes Bild gerät trotz Snapshotfehler nicht in die Sofortlöschung. |
| 4 | O04/O05: Restore-Identität und Export-Vertrauen korrigieren | Kein fremder Archiveintrag wird gelöscht, kein gelöschter Nachweis bei ungültigem Export wieder registriert. |
| 5 | O07/O08: Archivaufträge exklusiv und vollständig abschließen | Ein Ereignis ergibt einen vollständigen, reproduzierbaren Nachweis; fehlende Dateien bleiben sichtbar. |
| 6 | O06: Fairer Fortschritt im Scheduler | Blockierte Kandidaten halten andere fällige Veröffentlichungen nicht dauerhaft auf. |
| 7 | Betroffene Regressionstests, dann isolierte Gesamtsuite und Betriebsabnahme | Aktuelle Ergebnisse dokumentiert; Live-Schritte und Zugangsdatenrotation nachgewiesen. |
**Abnahmeurteil:** Die Kernrichtung der Nacharbeit ist sachgerecht. Die pauschale Kennzeichnung von Block 9 als vollständig erledigt ist für Sicherheit, Bildnachweis und Restore jedoch zu weitgehend. Die oben beschriebenen Restfehler sollten vor der jeweiligen Freigabe geschlossen werden. Dieses Audit selbst hat ausschließlich dokumentiert.

View file

@ -329,3 +329,4 @@ Agenda, Prüfannahmen, finale Texte und Risiken stehen im Decision-Update (Absch
| 25.09.2026 | Umsetzungsaudit 25.09. nachgearbeitet | `4e34c3d`, `48129e9` | Block 8 im Audit-Entwicklungsplan: Freigabezustand `isPubliclyDeliverable()`, Ability `confirmImageRights`, Archiv-Sperren/Fehlerauswertung, `archive_pending_at` + Nachholer, Anonymisierung über den Remover, Rejected-Purge, App-Kopien fallen mit dem Tombstone; N09 offen |
| 28.09.2026 | **Deploy 1, 2 und 2b live** | — | Betreiber: 12 Migrationen (längste `archive_pending_at` 12 s), Seeder, Backfill 63 Bestätigungen / 35.459 `published_ever_at`, `purge-inactive-legacy --execute` 10.769 Bilder (~2,98 GB), `purge-drafts` 2 Entwürfe / 0 Abgelehnte (Altbestand korrekt geschützt), Archiv-Nachholer und Orphan-Sweep leer, vier Archiv-Tasks im Scheduler, OG-Varianten für 3.807 Bestandsbilder erzeugt, Caches neu |
| 28.09.2026 | **Deploy 1b live** | — | `IMAGE_RIGHTS_V2=true`; `IMAGE_PRECHECK` und `LEGAL_ACCEPTANCE_GATE` bleiben aus (AVV/DSE bzw. AGB 0.7). Betreiber hat den Bild-Upload mit dem neuen Formular über ein Testkonto auf Live geprüft: funktioniert. Legacy-Bildupload antwortet ab jetzt 410. Vier-Wochen-Frist für Block I (C2PA) läuft ab heute, also ab 26.10.2026 |
| 29.09.2026 | Umsetzungsaudit 29.09. (Block 9 im Audit-Entwicklungsplan) | — (nicht committet) | Feed/Card-Helfer/PDF nur freigegebene Bilder (B-01/B-02), Livewire-Upload 16 MiB (B-03), Snapshot versioniert inkl. OG-Variante (N09), Bestätigung an Bild-ID/SHA gebunden (B-04), Entfernen unter Sperre (B-05), Korrektur über Archivauftrag (B-06/A-09), `precheck_withheld_at` (B-07), Magic-Link-Zweck `image_confirmation` (B-10), Sweep verwaister Originale (B-11). **Korrektur zur Zeile vom 28.09.:** „Altbestand korrekt geschützt" war nicht belegt — `published_ever_at` ist für alle abgelehnten Alt-Meldungen NULL (Backfill kann „offline genommen" nicht erkennen); `purge-drafts` griff nur nicht, weil die Bilder vorher entfernt waren. Jetzt schützt `whereNull('legacy_id')` bzw. `PressRelease::wasEverPublished()` den Altbestand ausdrücklich (A-05) |

View file

@ -1,6 +1,6 @@
# dev/ — Projekt- und Betriebsdokumentation
Stand: 20.07.2026 — **das System ist live** (pressekonto.com, presseecho.de,
Stand: 29.09.2026 — **das System ist live** (pressekonto.com, presseecho.de,
businessportal24.com). Die Struktur trennt aktive Betriebs-/Planungsdoku von
abgeschlossenen Phasen (Archiv).
@ -11,7 +11,9 @@ abgeschlossenen Phasen (Archiv).
| `betrieb/deploy-runbook.md` | Deploy- und Betriebs-Runbook (SFTP-Deploy-Checkliste, Worker, Scheduler, Stripe-Webhook, Rollback) — **erste Anlaufstelle für alles Operative** |
| `betrieb/api-legacy-paritaet.md` | Verhaltens-Referenz der Legacy-API-Kompatibilitätsschicht (`/service/…`) — lebendes Dokument, solange Alt-Integrationen (Connektar) die Pfade nutzen |
| `betrieb/umami.md` | Umami-Analytics-Snippets je Domain |
| `roadmap.md` | Offene Punkte und nächste Entwicklungen/Optimierungen |
| `2026-09-01/` | **Aktuelle Roadmap** (`roadmap-2026.md` + Anhänge 1 und 2), Basiswerte Block 0, Umsetzungsprotokoll Teil 1–11 |
| `2026-09-11-audit/` | **Audits und Entwicklungsplan** (`ENTWICKLUNGSPLAN.md`, Blöcke 1–9), Umsetzungsaudits 18.09./25.09./29.09., Bildkennzeichnung (Plan Fassung 2, Decision-Update) — der jüngste Umsetzungsaudit ist der Einstieg in den aktuellen Stand |
| `roadmap.md` | Nachlauf Go-Live (Stand 20.07.) und Ideen-Backlog — die laufende Planung steht in `2026-09-01/` |
## Lokal / gitignored (bleiben liegen, nicht committen)

View file

@ -168,16 +168,36 @@ Crontab des Web-Users:
* * * * * cd /var/www/pressekonto && php artisan schedule:run >> /dev/null 2>&1
```
Aktive Tasks (`php artisan schedule:list`):
Aktive Tasks (`php artisan schedule:list`, Stand 29.09.2026 — maßgeblich ist
`routes/console.php`; `ScheduleConfigurationTest` prüft Sperrdauer und
Minutenkollisionen):
| Zeitplan | Task | Ohne Cron passiert … |
|---|---|---|
| jede Minute | `press-releases:aggregate-views` | Aufrufzahlen bleiben stehen (Ereignisse sammeln sich) |
| alle 5 Min | `press-releases:publish-scheduled` | **geplante PMs werden nie veröffentlicht** |
| täglich 03:00 | `magic-links:purge --days=30` | Token-Tabelle wächst |
| täglich 04:30 | `billing:generate-manual-invoices` | manuelle Rechnungen fehlen |
| So 04:00 | `press-releases:purge-drafts --days=180` | Alt-Entwürfe bleiben liegen |
| monatl. 1. 00:05 | `press-releases:reset-monthly-quota` | **Kontingente werden nie zurückgesetzt** |
| alle 10 Min | `mail:fetch-bounces` | Rückläufer werden nicht gesperrt |
| alle 15 Min | `market:refresh` | Börsendaten im Ticker veralten |
| täglich 03:00 | `magic-links:purge --days=30` | Token-Tabelle wächst |
| täglich 03:10 | `snapshots:purge` | Schwärzungs-Snapshots bleiben über 30 Tage liegen |
| täglich 03:20 | `archive:purge` | Beweisarchiv hält Einträge über die Frist |
| täglich 03:25 | `conversion:purge-events` | Konversionsereignisse und `email_bounces` über 395 Tage |
| täglich 03:30 | `archive:export-tombstones` | Restore-Abgleich ohne aktuellen Löschstand |
| täglich 03:35 | `archive:retry-pending` | offene Archivaufträge werden nicht nachgeholt |
| täglich 03:40 | `archive:sweep-orphans` | verwaiste Archivdateien bleiben (Quarantäne statt Löschen) |
| täglich 03:50 | `press-releases:purge-events` | Aufruf-, Crawler- und Feed-Protokolle über 395 Tage |
| täglich 04:15 | `billing:expire-grandfathering` | abgelaufene Bestandsschutz-Verträge laufen weiter |
| täglich 04:20 | `mail:check-domains --stale --limit=500` | DNS-Befunde der Kontaktdomains veralten |
| täglich 04:30 | `billing:generate-manual-invoices` | manuelle Rechnungen fehlen |
| täglich 05:10 | `press-releases:check-index-status` | Indexierungsbericht bekommt keine neuen Daten |
| So 04:00 | `press-releases:purge-drafts --days=180` | Alt-Entwürfe bleiben liegen |
| So 04:45 | `press-release-images:sweep-orphan-originals --execute --hours=48` | Originale abgebrochener Uploads bleiben liegen |
| monatl. 1. 00:05 | `press-releases:reset-monthly-quota` | **Kontingente werden nie zurückgesetzt** |
Jeder Task hält seine `withoutOverlapping()`-Sperre nur kurz über der
erwarteten Laufzeit (Audit K-09). Nach einem Absturz mitten im Lauf gibt
spätestens dann der nächste Lauf wieder frei; sofort mit
`php artisan schedule:clear-cache`.
**Monitoring (R5.3):** Jeder Scheduler-Task hat ein `->onFailure()`, der
Queue-Worker einen `Queue::failing()`-Listener. Fehlschläge werden als
@ -193,7 +213,7 @@ Endpoint in Stripe (Live-Mode) registrieren: `https://pressekonto.de/stripe/webh
Events: die Cashier-Defaults (Subscriptions, Invoices, Payment-Intents).
## 7. Erst-Einrichtung (nur beim allerersten Go-Live)
# kopieren der Bilddaten per rync #@jk:8#PN§!4V02wR:rV#
# Bilddaten per rsync kopieren (Zugangsdaten stehen im Passwortmanager, nicht im Repository)
rsync -avz --partial ssh-w01eb4f6@w008b36f.kasserver.com:/www/htdocs/w01eb4f6/presseecho.de/web/thumbnails/ \
dev/migration/presseecho/thumbnails/
@ -288,13 +308,42 @@ Newsroom-Übersicht, eine alte PM-Detailseite (lädt Bild + Anhang?), Firmenlogo
(`config:cache` … `queue:restart`).
- Migrationen sind additiv gehalten; kein automatisches `migrate:rollback` in Prod —
im Fehlerfall DB-Backup einspielen (Backup **vor** jedem Deploy mit Migrationen!).
- **Nach jedem DB-Restore:** `php artisan archive:reconcile-after-restore` ausführen.
Das Beweisarchiv (Bildkennzeichnung Block F) löscht Einträge nach Frist; eine
eingespielte Sicherung würde gelöschte Zeilen und Payloads zurückbringen. Der
Abgleich liest den täglich exportierten Tombstone-Stand
(`storage/app/archive/tombstones/latest.json`) **und** die Tombstone-Zeilen der
Datenbank und entfernt wiederhergestellte Payloads erneut. Exit-Code ≠ 0 heißt:
mindestens eine Datei ließ sich nicht löschen — Log prüfen, Lauf wiederholen.
- **Nach jedem DB-Restore — Beweisarchiv in dieser Reihenfolge** (Umsetzungsaudit
29.09.2026, A-03/A-06/A-15):
1. **Sofort** `php artisan archive:sweep-orphans --pause` (der Sweep läuft
nachts um 03:40; nach einem DB-Restore fehlen jüngeren Einträgen die Zeilen,
ihre Payloads sähen verwaist aus). Seit 29.09. verschiebt der Sweep ohnehin nur
in die Quarantäne (`storage/app/archive/quarantine/JJJJ-MM-TT/…`,
`ARCHIVE_QUARANTINE_DAYS`, Standard 30 Tage) — die Pause verhindert auch das.
Harte Alternative ohne DB: `ARCHIVE_SWEEP_PAUSED=true` + `config:cache`.
2. `php artisan archive:reconcile-after-restore` — liest den täglich exportierten
Tombstone-Stand (`tombstones/latest.json`, seit 29.09. mit HMAC) **und** die
Tombstone-Zeilen der Datenbank, entfernt wiederhergestellte Payloads erneut und
löscht die mit dem Backup zurückgekommenen Kopien in der Anwendung
(Bestätigungen, Korrekturen, weich gelöschte Bildzeile) erneut. Setzt den
Sweep selbst aus. Zuordnung über den Payload-Pfad, nicht die ID: passt eine
Export-Zeile zu einer DB-Zeile mit gleicher ID, aber anderem Payload, wird
nichts angefasst („passen nicht zur Datenbank", Exit ≠ 0, Log prüfen).
„Signatur ungültig" = Export verworfen, nur DB-Tombstones abgeglichen,
Exit ≠ 0 — Export-Herkunft prüfen; fehlt ein Schlüssel in
`ARCHIVE_PREVIOUS_KEYS`? Ein **unsignierter** Export wird seit 02.10.2026
ebenfalls verworfen (ein entferntes `hmac` sähe genauso aus); nur wenn er
nachweislich von vor dem 29.09.2026 stammt, mit `--accept-unsigned`
wiederholen. Exit ≠ 0 sonst: mindestens eine Datei ließ sich nicht löschen
— Log prüfen, Lauf wiederholen.
3. `php artisan archive:reregister --dry-run`, dann `php artisan archive:reregister`
— legt für Payloads ohne Zeile (Einträge nach dem Backup-Zeitpunkt, auch aus der
Quarantäne) die Zeile aus `manifest.json` wieder an. Laut Tombstone-Export
gelöschte Payloads bleiben außen vor. **Bricht ab**, wenn der Export ungültig
oder unsigniert ist (wie Schritt 2, `--accept-unsigned` nur für Altexporte)
oder ganz fehlt — ohne Export lassen sich gelöschte Einträge nicht von
verlorenen unterscheiden; nur bewusst mit `--without-export`. Legal Hold
steht nicht im Manifest: wiederhergestellte Einträge (Zusammenfassung
`reregistered_at`) im Admin prüfen.
4. `php artisan archive:retry-pending` (offene Archivaufträge nachholen; Zähler im
Admin-Systemstatus) und `php artisan archive:sweep-orphans --dry-run`.
5. `php artisan archive:sweep-orphans --resume` — Kachel „Beweisarchiv-Orphan-Sweep"
im Systemstatus wird wieder grün.
- **Achtung bei Restore der Archiv-Disk:** Wird `storage/app/archive` selbst auf
einen älteren Stand zurückgesetzt, ist auch `tombstones/latest.json` alt. Dann
**vor** dem Abgleich den jüngsten Tombstone-Export von außerhalb des Hosts
@ -302,13 +351,23 @@ Newsroom-Übersicht, eine alte PM-Detailseite (lädt Bild + Anhang?), Firmenlogo
oder sicherstellen, dass die Datenbank den aktuellen Stand hat — der Abgleich
nimmt dann die DB-Tombstones. Ein automatisches externes Exportziel ist nicht
gebaut (Betreiberentscheidung offen, Umsetzungsaudit 25.09.2026, 5.1 c).
- **Nach dem Restore ebenfalls:** `php artisan archive:retry-pending` (holt
Archiveinträge nach, deren Ablage beim Vorgang fehlgeschlagen ist; Zähler im
Admin-Systemstatus) und `php artisan archive:sweep-orphans --dry-run`
(Payload-Dateien ohne Zeile).
- `ARCHIVE_KEY` (Beweisarchiv) und `CONFIRMATION_IP_KEY` liegen nur in der
Ploi-Umgebung. `ARCHIVE_KEY` getrennt vom DB-Backup sichern — ohne ihn ist das
Archiv unlesbar; erzeugen mit `php artisan archive:key`.
Archiv unlesbar; erzeugen mit `php artisan archive:key`. Fehlt er (z. B. nach
`config:cache` ohne Schlüssel), meldet der Systemstatus „Beweisarchiv-Schlüssel"
kritisch und `archive:retry-pending` bricht ab, ohne Aufträge anzufassen.
- **Schlüsselrotation `ARCHIVE_KEY`** (A-15): neuen Schlüssel mit
`php artisan archive:key` erzeugen und getrennt sichern; den bisherigen Wert
**vorne** in `ARCHIVE_PREVIOUS_KEYS` (kommagetrennt) eintragen, den neuen als
`ARCHIVE_KEY` setzen, `config:cache`, `queue:restart`. Jeder Eintrag trägt die
ID seines Schlüssels (`archive_entries.key_id`); alte Einträge bleiben mit dem
früheren Schlüssel lesbar, neue werden mit dem neuen verschlüsselt. Einen
früheren Schlüssel nur entfernen, wenn der Systemstatus keine Einträge mit
unbekanntem Schlüssel meldet (praktisch: erst nach Ablauf aller damit
verschlüsselten Einträge). Anwaltsexporte werden mit einem per HKDF abgeleiteten
Schlüssel signiert; `php artisan archive:verify-export <zip>` prüft neue und alte
Exporte (vor dem 29.09. mit dem Archivschlüssel signiert) gegen alle bekannten
Schlüssel.
## 10. Betrieb nach Go-Live (Stand 20.07.2026 — System ist LIVE)

View file

@ -3,6 +3,13 @@
Stand: 20.07.2026 (Go-Live-Tag). Kurzfristiges zuerst; Details zu erledigten
Phasen im Archiv (`dev/archive/`).
> **Hinweis 29.09.2026:** Die laufende Planung steht in
> `dev/2026-09-01/roadmap-2026.md`, der Entwicklungsstand in
> `dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md` und im jüngsten Umsetzungsaudit.
> Die Punkte unten stammen vom Go-Live-Tag; ihr Stand ist dort nicht
> nachgeführt und wird im Umsetzungsaudit vom 29.09. (Abschnitt 6.4) als „auf
> Live zu bestätigen" geführt.
## Nachlauf Go-Live (kurzfristig)
- [ ] **Go-Live-Mails** an Bestandskunden: erst `php artisan