presseportale/dev/optimize/cutover-runbook.md
Kevin Adametz 22b7bd38b7 docs: Runbooks auf den echten Migrationsstand nachgezogen (16.07.)
Cutover-Runbook: Legacy-DBs liegen bei KAS und sind als lokale Snapshot-
Kopien eingespielt (Delta-Läufe brauchen vorher frische Dumps); privatize-
media läuft mit --prune-missing; retire-agency-type als Satellit ergänzt;
verlorene Presseecho-PM-Bilder (~22k) als bewusste Entscheidung dokumentiert.
Deploy-Runbook: Seeder-Liste um LegalDocumentSeeder/RejectionCategorySeeder
ergänzt, billing:sync-stripe-plans nach frischem Seeding, Tinker-Befehl für
die Admin-Rolle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:50:59 +00:00

8.1 KiB

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. 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/<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 --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 (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 --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.