Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/docs/ops/platform-leads-cutover.md
T

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-inbox Postgres: 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

  1. 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.
  2. Source-Credentials einmalig erzeugen und ausschliesslich in den jeweiligen Server-Runtime-Secrets speichern.
  3. Staging-Canary je Source ausfuehren: Create, identischer Replay, Hash-Konflikt, Auth-Fehler, Rate-Limit, Delivery/Retry und Admin-Bearbeitung.
  4. Alle Altquellen dry-run importieren. Zaehler und Hashes gegen das Safety- Inventar pruefen.
  5. Apply und anschliessenden Replay ausfuehren. Der Replay muss created=0 liefern.
  6. Direkt vor jedem Website-Deploy den jeweiligen aktuellen Store erneut sichern und nur das Finaldelta importieren.
  7. Producer einzeln umstellen; nach jeder Quelle einen synthetisch markierten Produktions-Canary und den Admin-/Delivery-Nachweis abwarten.
  8. Legacy-Endpoint read-only/drain-faehig belassen und mindestens sieben volle Tage auf eingehenden Traffic, Fehler, Queue-Failures und Source-Lag beobachten.
  9. 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.