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 f1c0e2766b
commit 4a7e496d1b
8 changed files with 787 additions and 21 deletions

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)