docs: Cutover-Runbook für den Go-Live (Legacy-Import & Umschaltung)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-15 16:07:13 +00:00
parent 3e855375b1
commit 9c67fc6c14

View file

@ -0,0 +1,128 @@
# Cutover-Runbook — Go-Live Pressekonto
Stand: 2026-07-14 · Branch `fix/zeitzone-europe-berlin`
Abhakbare Checkliste für die Umschaltung vom Alt-System auf Pressekonto.
Alle Commands laufen über Sail: `vendor/bin/sail artisan …` (bzw. ohne Sail
`php artisan …`, je nach Deployment-Umgebung).
---
## 0. Grundverständnis (einmal lesen)
- **Zwei Legacy-Quellen, unterschiedlicher Charakter:**
- **Presseecho** = echte **Live-DB** (`mysql_presseecho`). Nur hier greift die
„offline nehmen → Delta nachziehen"-Choreografie.
- **Businessportal24** = **statischer Snapshot-Dump** (`mysql_businessportal`).
Ändert sich nicht mehr → einmal importieren reicht, kein Delta nötig.
- **Idempotenz:** Alle Importer schreiben per `updateOrCreate(legacy_portal, legacy_id)`
+ Crosswalk-Tabelle `legacy_import_map`. Mehrfaches Ausführen erzeugt **keine
Duplikate**. Bei Absturz mittendrin: einfach erneut starten (kein Rollback,
Sicherheit kommt aus Idempotenz, nicht aus Transaktionen).
- **Standard-Lauf überspringt bereits importierte Zeilen.** Zwischenzeitliche
**Änderungen** an Altdatensätzen werden nur mit `--force` nachgezogen.
- **Kein Delete-Sync:** Im Alt-System gelöschte Datensätze bleiben im neuen
System bestehen. Für das kurze Cutover-Fenster i. d. R. vernachlässigbar.
- ⚠️ **`migrate:presseecho` NICHT ausführen** — toter, kaputter Stub. Der einzige
gültige Weg ist `legacy:import`.
- **Voraussetzung Timestamp-Sync:** `legacy:fix-timestamps` nutzt Cross-Schema-
JOINs → beide Legacy-Schemas müssen auf **demselben MySQL-Server** liegen wie
die App-DB. Sonst diesen Schritt durch vollen `--force`-Reimport ersetzen.
---
## 1. Vorbereitung (Tage vorher, in Ruhe)
- [ ] Prod-`.env` gesetzt: `APP_ENV=production`, `APP_DEBUG=false`,
`SESSION_SECURE_COOKIE=true`, `QUEUE_CONNECTION=database` (o. Ä. dauerhaft).
- [ ] Legacy-Zugangsdaten in `.env` eingetragen:
`DB_HOST_PRESSEECHO / DB_DATABASE_PRESSEECHO / DB_USERNAME_PRESSEECHO / DB_PASSWORD_PRESSEECHO`
und `DB_*_BUSINESSPORTAL` (Snapshot-Dump auf denselben Server eingespielt).
- [ ] Secrets rotiert (OpenAI / Google / Mail) — nicht die Alt-Keys übernehmen.
- [ ] Legacy-Medien ins lokale Staging kopiert:
`dev/migration/<portal>/uploads/...` (rsync vom Alt-System).
- [ ] DB-Migrationen ausgeführt: `artisan migrate --force`.
Vorher prüfen: keine Duplikate in `invoices.stripe_invoice_id`
(Unique-Migration bricht sonst bewusst ab).
- [ ] Rollen/Permissions ausgerollt: `artisan db:seed --class=RolesAndPermissionsSeeder`.
- [ ] **Verbindungstest / Zählwerte:** `artisan legacy:import --source=all --dry-run`
→ erwartete Zeilenzahlen plausibel?
---
## 2. Bulk-Import (Presseecho noch live, ohne Downtime)
Reihenfolge ist in `legacy:import --step=all` hart verdrahtet
(categories → users → companies → contacts → press-releases → link-associations).
- [ ] `artisan legacy:import --source=all`
(idempotent — beliebig oft wiederholbar).
- [ ] `artisan legacy:migrate-media --portal=all`
(Datei-Bytes aus `dev/migration/...` → **public**-Disk + Varianten).
- [ ] **`artisan press-releases:privatize-media`**
(verschiebt PM-Medien public → **private** Disk — Security-Hardening;
MUSS nach `legacy:migrate-media` laufen).
- [ ] `artisan legacy:fix-timestamps`
(created_at/updated_at + published_at per Cross-DB-JOIN).
- [ ] Satelliten-Migrationen (Reihenfolge unkritisch, jeweils `--dry-run` zuerst):
- [ ] `artisan legacy:import-api-keys`
- [ ] `artisan legacy:archive-invoices`
- [ ] `artisan legacy:grandfather-subscriptions`
- [ ] `artisan legacy:import-billing-addresses`
- [ ] `artisan legacy:migrate-agency-names`
- [ ] **Gate:** `artisan legacy:verify` → JSON-Report prüfen. Go / No-Go.
---
## 3. Cutover-Fenster (kurze Downtime)
Ziel: nur die seit dem Bulk-Import geänderten/neuen **Presseecho**-Daten
nachziehen. Businessportal24 braucht hier nichts (Dump ist statisch).
- [ ] **Presseecho** in **Read-Only / Wartung** schalten — **Lesezugriff behalten**,
der Delta-Import muss noch aus der Legacy-DB lesen können.
- [ ] Neues System in Wartung: `artisan down`.
- [ ] **Delta-Import mit `--force`** (zieht auch geänderte Bestandsdatensätze nach;
uuid + slug von PMs bleiben stabil → öffentliche URLs brechen nicht):
`artisan legacy:import --source=presseecho --force`
- [ ] Medien-Delta: `artisan legacy:migrate-media --portal=presseecho --force`
(`--force` erzwingt Neukopie + Varianten geänderter Bilder) →
danach erneut `artisan press-releases:privatize-media`.
- [ ] `artisan legacy:fix-timestamps --portal=presseecho`
- [ ] **Gate:** `artisan legacy:verify --portal=presseecho`.
- [ ] **DNS umstellen** auf das neue System.
- [ ] Queue-Worker läuft & wird überwacht → Admin ▸ **Reports ▸ Betriebsüberwachung**
(alle Kacheln grün, besonders „Queue-Worker" und „Medien-Privatisierung").
- [ ] `artisan up` — neues System live.
- [ ] Alt-System **komplett offline**.
---
## 4. Go-Live-Kommunikation
- [ ] Passwort-Reset-Mails versenden (Importer setzt **bewusst keine Passwörter**):
erst `artisan auth:send-go-live-mails --dry-run` (Anzahl prüfen),
dann `artisan auth:send-go-live-mails`.
- ⚠️ **Massen-Mailversand über die Queue** → vorher Worker-Status grün prüfen,
während des Versands Betriebsüberwachung im Blick behalten
(pending/failed jobs).
---
## 5. Nachlauf / Verifikation
- [ ] Stichproben: Login mit Reset-Link, PM-Detailseite (Bild + Anhang laden),
Newsroom-Übersicht, Archiv-Sichtbarkeit.
- [ ] Betriebsüberwachung: keine `failed_jobs`, kein „gestoppter Worker".
- [ ] `storage/logs` auf Fehler prüfen.
- [ ] Backup der neuen Prod-DB direkt nach erfolgreichem Cutover.
---
## Offene Punkte / bewusste Entscheidungen
- **Gelöschte Legacy-Datensätze** werden nicht synchronisiert (kein Delete-Sync).
Falls im Parallelbetrieb im Alt-System gelöscht wird → im neuen System nachher
manuell prüfen (i. d. R. vernachlässigbar bei kurzem Fenster).
- **`legacy:fix-timestamps`** setzt voraus, dass beide Legacy-Schemas auf demselben
MySQL-Server liegen. Andernfalls voller `--force`-Reimport statt Timestamp-Fix.