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>
This commit is contained in:
Kevin Adametz 2026-07-20 10:50:59 +00:00
parent 9be473162a
commit 22b7bd38b7
2 changed files with 100 additions and 6 deletions

View file

@ -27,7 +27,16 @@ Alle Commands laufen über Sail: `vendor/bin/sail artisan …` (bzw. ohne Sail
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.
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.
---
@ -59,9 +68,12 @@ Reihenfolge ist in `legacy:import --step=all` hart verdrahtet
(idempotent — beliebig oft wiederholbar).
- [ ] `artisan legacy:migrate-media --portal=all`
(Datei-Bytes aus `dev/migration/...` → **public**-Disk + Varianten).
- [ ] **`artisan press-releases:privatize-media`**
- [ ] **`artisan press-releases:privatize-media --prune-missing`**
(verschiebt PM-Medien public → **private** Disk — Security-Hardening;
MUSS nach `legacy:migrate-media` laufen).
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):
@ -70,6 +82,9 @@ Reihenfolge ist in `legacy:import --step=all` hart verdrahtet
- [ ] `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.
---
@ -81,13 +96,18 @@ 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`.
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.
@ -121,6 +141,12 @@ nachziehen. Businessportal24 braucht hier nichts (Dump ist statisch).
## 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).

View file

@ -68,6 +68,12 @@ php artisan db:seed --class=RolesAndPermissionsSeeder --force
php artisan db:seed --class=AdminPresetSeeder --force
php artisan db:seed --class=PlanSeeder --force
php artisan db:seed --class=PaymentOptionSeeder --force
php artisan db:seed --class=LegalDocumentSeeder --force
php artisan db:seed --class=RejectionCategorySeeder --force
# 5a) Nach frischem Seeding (Erst-Einrichtung oder DB-Reset): Stripe-Produkte/
# Preise neu verknüpfen — die Price-IDs stehen in der plans-Tabelle:
# php artisan billing:sync-stripe-plans
# 6) Caches bauen (bei JEDEM Deploy neu)
php artisan config:cache
@ -179,6 +185,24 @@ 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#
rsync -avz --partial ssh-w01eb4f6@w008b36f.kasserver.com:/www/htdocs/w01eb4f6/presseecho.de/web/thumbnails/ \
dev/migration/presseecho/thumbnails/
rsync -avz --partial ssh-w01eb4f6@w008b36f.kasserver.com:/www/htdocs/w01eb4f6/presseecho.de/web/uploads/ \
dev/migration/presseecho/uploads/
rsync -avz --partial ssh-w01eb4f6@w008b36f.kasserver.com:/www/htdocs/w01eb4f6/businessportal24.com/web/thumbnails/ \
dev/migration/businessportal24/thumbnails/
rsync -avz --partial ssh-w01eb4f6@w008b36f.kasserver.com:/www/htdocs/w01eb4f6/businessportal24.com/web/uploads/ \
dev/migration/businessportal24/uploads/
1. Legacy-Datenimport (Reihenfolge beachten, read-only-Quellen konfiguriert):
`legacy:import` → `legacy:fix-timestamps` → `legacy:migrate-media`
@ -191,11 +215,55 @@ Events: die Cashier-Defaults (Subscriptions, Invoices, Payment-Intents).
Owner-lose Agentur-Firmen und geschützte Fälle), dann scharf.
Namen werden nie überschrieben; gelöscht wird nur soft und nur ohne
PMs/Kontakte/Dritt-Mitgliedschaften/Zahlungsoptions-Verknüpfung.
3. Ersten Admin-User anlegen (Prod hat keine Demo-User):
Registrierung + Rolle via Tinker/DB oder dedizierter Befehl.
3. Ersten Admin-User anlegen (Prod hat keine Demo-User): normal registrieren
(inkl. E-Mail-Verifizierung), dann Rolle via Tinker setzen —
`syncRoles` ersetzt dabei alle bisherigen Rollen des Users:
```bash
php artisan tinker --execute 'App\Models\User::where("email", "mail@example.com")->firstOrFail()->syncRoles(["admin"]);'
```
4. Favicons je Domain deployen (Platzhalter-Generator reicht nicht) [EXTERN R5.4].
5. Smoke-Test: siehe Abschnitt 8.
1. Medien migrieren
# Erst prüfen, ob die Ordnerstruktur stimmt (zählt "Datei fehlt"/"Ziel fehlt")
php artisan legacy:migrate-media --portal=all --dry-run
# Dann scharf (kopiert + erzeugt Bildvarianten — dauert bei der Menge eine Weile)
php artisan legacy:migrate-media --portal=all
Kleine „Datei fehlt"-Restmengen sind bei Altbeständen normal (verwaiste DB-Einträge); große Zahlen heißen Ordnerproblem — dann melde dich mit der Ausgabe.
2. PM-Medien privatisieren (Pflicht, direkt danach)
php artisan press-releases:privatize-media
Verschiebt die PM-Bilder von der public- auf die private-Disk (Security-Hardening). Muss nach migrate-media laufen — und nach jedem späteren Medien-Lauf erneut.
3. Timestamps korrigieren (bewusst NACH den Medien)
php artisan legacy:fix-timestamps
Setzt created_at/updated_at/published_at authoritativ aus den Legacy-DBs. Nach den Medien deshalb, weil die Medien-Migration die Datensätze anfasst und updated_at neu stempelt — der Fix räumt das wieder auf. Wichtig fürs Frontend: Die Sortierung hängt an published_at.
4. Satelliten-Migrationen (jeweils erst --dry-run, dann scharf)
php artisan legacy:import-api-keys --dry-run # lokal waren es 406 Keys — Zahl vergleichen
php artisan legacy:archive-invoices --dry-run
php artisan legacy:grandfather-subscriptions --dry-run # MUSS nach archive-invoices (leitet sich aus dem Rechnungsarchiv ab)
php artisan legacy:import-billing-addresses --dry-run # Report: Kunden mit leeren/kaputten Alt-Adressen → manuell nachpflegen
php artisan legacy:migrate-agency-names --dry-run
php artisan companies:retire-agency-type --dry-run # NACH migrate-agency-names
Die Reports landen unter storage/app/private/migration/ — besonders bei den letzten beiden reinschauen (Mehrfach-Namen, Owner-lose Agentur-Firmen), bevor du sie scharf laufen lässt.
5. Das Gate: Gesamtverifikation
php artisan legacy:verify
JSON-Report prüfen → das ist deine Go/No-Go-Entscheidung für den Cutover. Wenn du magst, reich mir den Report hier rein, ich gehe ihn mit dir durch.
Danach: Stichproben im Browser
Newsroom-Übersicht, eine alte PM-Detailseite (lädt Bild + Anhang?), Firmenlogo, Archiv — und ein Blick in Admin ▸ Reports ▸ Betriebsüberwachung (alle Kacheln grün, besonders „Medien-Privatisierung").
## 8. Smoke-Test nach jedem Deploy
- [ ] Alle drei Domains laden (Hub-Landing, 2× Spoke-Home) über HTTPS