# Cutover-Runbook — Go-Live Pressekonto > ✅ **ERLEDIGT — archiviert am 20.07.2026.** Die Migration ist vollständig > durchgelaufen (`legacy:verify`: 0 failures, 0 warnings), die echten Domains > sind live geschaltet, Stripe läuft im Live-Mode. Dieses Runbook bleibt als > Referenz erhalten — z. B. für einen späteren `--since`-Delta-Lauf, falls > das Alt-System doch noch einmal Daten liefern sollte (Abschnitt 3). > Betriebs-Doku ab jetzt: `dev/betrieb/deploy-runbook.md`. 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. **Umsetzung Live (16.07.):** Die KAS-Originale (All-Inkl) sind von außen nicht joinbar → Dumps als lokale Snapshot-Kopien `legacy_presseecho` / `legacy_businessportal` auf dem Hetzner-Server eingespielt (Ploi-DBs, `dbUserPressekonto` hat Zugriff), `DB_*_PRESSEECHO`/`DB_*_BUSINESSPORTAL` zeigen auf die Kopien. Ein voller `--force`-Reimport ist als Ersatz ungeeignet: er setzt die `path`-Spalte bereits migrierter Bildzeilen auf den Legacy- Dateinamen zurück und macht die Medien-Migration kaputt. - ⚠️ **Snapshot veraltet:** Alle Legacy-Commands lesen jetzt die lokalen Kopien. Beim Cutover (Abschnitt 3) und vor jedem Delta-Lauf zuerst frische KAS-Dumps ziehen und in die Kopien einspielen — sonst läuft das Delta gegen alte Daten. --- ## 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//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 --prune-missing`** (verschiebt PM-Medien public → **private** Disk — Security-Hardening; MUSS nach `legacy:migrate-media` laufen). `--prune-missing` löscht Legacy-Bildzeilen, deren Datei im Alt-System verloren ging (Import legt pro Legacy-Bild eine Zeile an; ohne Datei bleibt sie sonst als Leiche liegen und hält die Betriebsüberwachungs-Kachel rot). - [ ] `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` - [ ] `artisan companies:retire-agency-type` (läuft NACH `legacy:migrate-agency-names`; Reports unter `storage/app/private/migration/` prüfen — Mehrfach-Namen, Owner-lose Agentur-Firmen, geschützte Fälle; s. Deploy-Runbook §7) - [ ] **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. - [ ] **Frische KAS-Dumps ziehen und die lokalen Snapshot-Kopien neu einspielen** (`legacy_presseecho`; Businessportal24 nur falls sich dort doch etwas geändert hat) — die Legacy-Connections lesen die Kopien, nicht KAS direkt. - [ ] Neues System in Wartung: `artisan down`. - [ ] **Delta-Import mit `--force` + `--since`** (zieht auch geänderte Bestandsdatensätze nach; uuid + slug von PMs bleiben stabil → öffentliche URLs brechen nicht). `--since` auf das Datum des **Bulk-Imports minus 1 Tag Sicherheitsmarge** setzen — dann scannt der Lauf nur die seither angelegten/geänderten Legacy-Zeilen (Sekunden statt Volldurchlauf): `artisan legacy:import --source=presseecho --force --since="2026-07-19"` Die Importer setzen Timestamps inzwischen selbst korrekt (forceFill) — `legacy:fix-timestamps` danach ist Absicherung, kein Muss mehr. ⚠️ Für neue PM-Bilder vorher auch die **Medien** vom Alt-Server nachsyncen (rsync-Delta nach `dev/migration/presseecho/…`), sonst werden ihre Bildzeilen mangels Datei von `--prune-missing` entsorgt. - [ ] Medien-Delta: `artisan legacy:migrate-media --portal=presseecho --force` (`--force` erzwingt Neukopie + Varianten geänderter Bilder) → danach erneut `artisan press-releases:privatize-media --prune-missing` (der `--force`-Reimport legt die Bildzeilen verlorener Legacy-Dateien erneut an — das Pruning räumt sie wieder ab). - [ ] `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 - **Verlorene Presseecho-PM-Bilder akzeptiert (16.07.):** ~22k von ~32k Legacy- Bilddateien existieren im Alt-System nicht mehr (nur Alt-Thumbnails retteten ~4,5k). Entscheidung: ohne Bild importieren — betrifft alte Archiv-Meldungen; betroffene PMs erscheinen bildlos, kaputte Referenzen entstehen nicht (`press-releases:privatize-media --prune-missing` räumt die Datensatz-Leichen ab). - **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.