8.6 KiB
Plattform-Leads: Backup, Backfill und Cutover
Dieses Runbook ist der operative Vertrag fuer R-LEADS (HIVEGRID-73). Es
ersetzt den separaten lead-inbox-Schreibpfad kontrolliert durch
POST /v1/platform/leads, ohne die letzte restorable Kopie der Altdaten zu
loeschen.
Sicherheitsregeln
- Kein Producer-Deploy ohne unmittelbar davor wiederholte Inventur und Sicherung seines tatsaechlich beschriebenen Fallback-Stores.
- Backups enthalten PII und werden deshalb nie unverschluesselt im Repo, in Tickets, Logs oder Import-Manifests abgelegt.
- Der verschluesselte Offsite-Stand liegt im privaten Hetzner-Bucket
hivegrid-prod; Objektpraefixe und Hashes duerfen als PII-freier Proof dokumentiert werden. - Import-Manifests enthalten nur Quelle, Zaehler, Zeitbereich, Hashes und technische Run-IDs.
- Ein Import erzeugt weder Benachrichtigungen noch Marketing-Automationen.
- Das letzte verschluesselte Backup bleibt erhalten, bis ein eigener Retention-/Loeschentscheid bestaetigt wurde.
Erster Safety-Snapshot
Der Snapshot vom 2026-07-16 liegt unter
backups/platform-leads/20260716T033754Z. Sein PII-freies Inventar und die
Restore-Ergebnisse stehen in
docs/ops/evidence/r-leads-safety-inventory-20260716.json.
Ergebnis:
lead-inboxPostgres: 1 Datensatz, Custom-Dump in frisches Postgres 16 restauriert.- HiveGrid-Website Produktion: kein SQLite-File im laufenden Container und kein persistentes Volume; 0 Datensaetze.
- HiveGrid-Website Staging: 1 Datensatz, konsistentes SQLite-Backup,
integrity_check=ok. - HiveGrid-Cloud
Lead/LeadEvent: Produktion 0/0, Staging 0/0.
Zusaetzlich wurde irrtuemlich ein nicht zu HiveGrid gehoerender Website-Dateistore
inventarisiert und verschluesselt gesichert. Das Objekt bleibt zur Vermeidung einer
unautorisierten Datenvernichtung erhalten, ist im Evidence-Inventar jedoch mit
excluded_from_hivegrid_import: true markiert und darf weder als Quelle
konfiguriert noch importiert werden. Aufbewahrung oder Loeschung erfordern einen
separaten, ausdruecklich autorisierten Retention-/Loeschentscheid.
Operator-Tooling
Vor dem Source-Dry-Run muss der Canary /leads mit einer aktiven AdminUser-
Identität und der vorgesehenen Sales-Rolle erreichen. Der API-Schalter
PLATFORM_LEADS_INGRESS_ENABLED schützt unabhängig davon nur den externen Ingress.
Der Routing-Operator erhaelt vor dem Bootstrap eine Plattformrolle mit mindestens
admin.leads.read_own und admin.leads.write. Der ausfuehrende Operator braucht
admin.leads.admin. Das Bootstrap-Tool prueft aktiven AdminUser und diese
Rechte fail-closed, bevor es einen Plan erzeugt oder Quellen mutiert. Fuer den
Haris-Canary wird zusaetzlich verifiziert: Login auf admin.hivegrid.de,
/leads erreichbar, zugewiesener synthetischer Lead sichtbar und eine
Status-/Notizmutation erfolgreich.
Die nativen Quellen werden idempotent ueber den AdminLead-Domain-Service konfiguriert. Haris' Admin-User-ID wird zur Laufzeit uebergeben und ist keine Code-Konstante. Der Dry-Run erzeugt einen PII-freien Plan-Hash; Apply akzeptiert nur genau diesen Stand:
pnpm platform-leads:source-bootstrap -- \
--mode dry-run \
--actor-admin-user-id "$PLATFORM_ADMIN_USER_ID" \
--routing-admin-user-id "$HARIS_ADMIN_USER_ID" \
--manifest "$EVIDENCE_DIR/source-bootstrap.dry-run.json"
pnpm platform-leads:source-bootstrap -- \
--mode apply \
--actor-admin-user-id "$PLATFORM_ADMIN_USER_ID" \
--routing-admin-user-id "$HARIS_ADMIN_USER_ID" \
--expected-plan-sha256 "$PLAN_SHA256" \
--manifest "$EVIDENCE_DIR/source-bootstrap.apply.json"
Die aktuelle Website-Quelle wird aktiviert, Haris wird als Default-Assignee
und mindestens als Notification-Empfaenger konfiguriert. Bereits vorhandene
weitere Empfaenger bleiben erhalten. Die beiden reinen Importquellen bleiben
deaktiviert. Das Bootstrap-Tool stellt bewusst keine Credentials aus; deren
einmalig angezeigter Klartext wird ueber /leads/sources erzeugt und direkt in
den jeweiligen Server-Runtime-Secrets hinterlegt.
Der Backfill liest UTF-8-JSONL oder ein JSON-Array. Die feste Zuordnung ist:
| Legacy-Store | Native Source |
|---|---|
lead-inbox-postgres |
lead-inbox-legacy |
hivegrid-website-production-sqlite |
hivegrid-website |
hivegrid-website-staging-sqlite |
hivegrid-website |
hivegrid-cloud-legacy |
hivegrid-legacy-cloud |
Pro Store wird zuerst ein Dry-Run mit einer neuen Manifest-Datei ausgefuehrt. Apply und Replay verlangen sowohl dessen exakten Input-Hash als auch die akzeptierte Zeilenzahl und schreiben bei Drift oder Rejections keine Zeile:
pnpm platform-leads:backfill -- \
--mode dry-run \
--store "$LEGACY_STORE" \
--input "$RESTORED_EXPORT" \
--manifest "$EVIDENCE_DIR/$LEGACY_STORE.dry-run.json"
pnpm platform-leads:backfill -- \
--mode apply \
--store "$LEGACY_STORE" \
--input "$RESTORED_EXPORT" \
--actor-admin-user-id "$PLATFORM_ADMIN_USER_ID" \
--expected-input-sha256 "$INPUT_SHA256" \
--expected-accepted "$ACCEPTED_COUNT" \
--manifest "$EVIDENCE_DIR/$LEGACY_STORE.apply.json"
pnpm platform-leads:backfill -- \
--mode replay \
--store "$LEGACY_STORE" \
--input "$RESTORED_EXPORT" \
--actor-admin-user-id "$PLATFORM_ADMIN_USER_ID" \
--expected-input-sha256 "$INPUT_SHA256" \
--expected-accepted "$ACCEPTED_COUNT" \
--manifest "$EVIDENCE_DIR/$LEGACY_STORE.replay.json"
Ein erfolgreicher Replay hat created=0, failed=0 und keine Gate-Failures.
Die Manifeste enthalten nur Hashes, Zaehler, technische IDs und Zeitbereiche;
Input-Pfade, Lead-Inhalte und Klartext-Legacy-IDs werden nicht aufgenommen. Jede
Manifest-Datei wird exklusiv mit Modus 0600 angelegt und nicht ueberschrieben.
Importe laufen ausschliesslich ueber importLead, bewahren Originalzeitpunkte,
native Legacy-Status und unterstuetzte Activities und loesen weder M011-
Benachrichtigungen noch Automationen aus.
Cloud-Legacy-Status werden verlustarm auf den nativen M040a-Vertrag abgebildet:
trial und customer werden qualified, churned wird disqualified; der
Originalwert bleibt als metadata.legacyStatus erhalten. Historische
score-changed- und converted-Events werden als legacy_event mit
legacyType im begrenzten Activity-Payload uebernommen. Unbekannte Status- oder
Eventtypen bleiben Rejections und stoppen Apply. Damit wird keine M040b-
Deal-Semantik vorgezogen, aber der historische Verlauf geht nicht verloren.
Cutover-Reihenfolge
- Native Tabellen, RLS/Pre-Tenant-Capability, Domain-Service, API, Worker und Admin-Inbox nach Staging deployen; Operatorrolle wie oben anlegen und Berechtigungs- sowie Direkt-Routen gegen 404 pruefen.
- Source-Credentials einmalig erzeugen und ausschliesslich in den jeweiligen Server-Runtime-Secrets speichern.
- Staging-Canary je Source ausfuehren: Create, identischer Replay, Hash-Konflikt, Auth-Fehler, Rate-Limit, Delivery/Retry und Admin-Bearbeitung.
- Alle Altquellen dry-run importieren. Zaehler und Hashes gegen das Safety- Inventar pruefen.
- Apply und anschliessenden Replay ausfuehren. Der Replay muss
created=0liefern. - Direkt vor jedem Website-Deploy den jeweiligen aktuellen Store erneut sichern und nur das Finaldelta importieren.
- Producer einzeln umstellen; nach jeder Quelle einen synthetisch markierten Produktions-Canary und den Admin-/Delivery-Nachweis abwarten.
- Legacy-Endpoint read-only/drain-faehig belassen und mindestens sieben volle Tage auf eingehenden Traffic, Fehler, Queue-Failures und Source-Lag beobachten.
- Nur bei Nulltraffic: App stoppen, Secrets revoken, DNS entfernen und Repo archivieren. Die Datenbank nicht irreversibel loeschen.
Rollback
Innerhalb des Drain-Fensters werden Producer-Secrets/URLs auf den weiterhin laufenden Legacy-Endpoint zurueckgestellt. Nach einem bereits erfolgten Stop wird das letzte bekannte Image gestartet und Postgres aus dem geprueften Custom-Dump restauriert. Native Importe sind idempotent und loeschen keine Quelle; sie muessen fuer einen Rollback nicht rueckgaengig gemacht werden.
Decommission-Gates
Der externe Dienst darf erst gestoppt werden, wenn alle folgenden Nachweise gruen sind:
- der HiveGrid-Website-Producer produktiv auf dem nativen Endpoint;
- Finaldelta importiert und Replay
created=0; - sieben volle Tage kein Legacy-Write-Traffic;
- Native-API-, Admin- und Notification-Monitoring ohne offene kritische Fehler;
- letzter Dump offsite vorhanden und Restore erneut erfolgreich;
- DNS-/Secret-/Mailjet-Abhaengigkeiten inventarisiert und Rollback-Werte sicher hinterlegt.
DNS-Loeschung, Secret-Revoke und App-Stop werden mit Zeitstempel und externer Run-ID in der Ticket-Doc dokumentiert. Eine endgueltige Backup- oder DB-Loeschung ist nicht Teil dieses Tickets.