Der Sync fragt OA nach allen Records mit lastModifiedAfter > Cursor. Cursor = der gespeicherte Zeitstempel vom letzten erfolgreichen Lauf.
Was passiert
Beim allerersten Lauf ist der Cursor leer, dann werden alle Records in der Quelle gezogen. Danach nur noch Änderungen seit dem letzten Mal.
Quelle:Phase 2
Nach dem Delta-Listing fragt der Sync zusätzlich alle aktuell existierenden IDs der Quelle ab (cursor=null, typeFields=id). So weiß der Service, welche Records aus seinem lokalen Cache aus OA verschwunden sind.
Was passiert
Diese ID-Liste ist die Grundlage für Soft-Delete (Schritt 3, Pool B). Wenn die Probe fehlschlägt, wird der Lauf abgebrochen und der Cursor nicht weitergeschoben — kein Risiko falscher Soft-Deletes durch transiente API-Fehler.
Quelle:Phase 7SOFT-01
Bevor Soft-Deletes berechnet werden, prüft der Sync zweistufig ob die OA-ID-Liste vollständig ist: (1) Auf jeder Probe wird `numFound == unique IDs collected` geprüft — Mismatch emittiert seit Phase 12 Plan 07 einen strukturierten Warn-Log (`event="inventory_probe_numfound_drift"`, `severity="drift"`, `missing_count=…`) und der Lauf läuft weiter. Frühere Versionen brachen hier mit `InventoryProbeFailedError(reason="numfound_mismatch")` ab; die Live-Smoke 2026-05-08 hat gezeigt, dass dieser Throw jeden Full-Sync gegen api-elbland-dresden mit `created_count=0` abgebrochen hat, ohne dass tatsächlich Daten verloren gingen. (2) Die Probe wird zweimal hintereinander gefahren, und für die Soft-Delete-Berechnung gilt die UNION beider Ergebnisse — eine ID gilt nur dann als verschwunden, wenn sie in beiden Proben fehlt. Hintergrund: OA's Pagination ist nicht stabil zwischen Seiten-Calls (live verifiziert 2026-05-05: 65 von 621 IDs sind zwischen zwei Seiten verloren gegangen; live verifiziert 2026-05-08: 34 cross-page Duplikate bei numFound=1195 — UNION beider Proben rekonstruiert den vollen Bestand exakt).
Was passiert
Wenn OA während eines Laufs Records modifiziert und ihre Reihenfolge zwischen Seiten umsortiert, kompensiert Defense 2 (Union beider Proben) den Datenverlust automatisch — eine ID müsste in BEIDEN Proben fehlen, damit sie fälschlich als verschwunden gilt. Defense 1 (numFound-Check) fungiert seither als reines Audit-Signal: Drift-Warns sammeln sich im Log-Stream auf, der Run läuft aber durch und der Cursor wird normal vorgeschoben. Drift > 0 zwischen Probe A und B emittiert zusätzlich einen `inventory_probe_drift` Warn-Log auf Engine-Ebene.
Beispiel
2026-05-05, Run 019df7f7: ohne diese Schutzmaßnahmen wurden 27 noch in OA publizierte Touren auf status="entwurf" gesetzt — der Bug, den Phase 7.1 schließt. 2026-05-08: Defense 1 wurde von throw auf warn weichgespült, weil A ∪ B = numFound exakt rekonstruiert (12-05-LIVE-SMOKE.md, 12-07-PLAN.md).
Quelle:Hard Constraint: Inventory-Probe CoveragePhase 7.1Phase 12 Plan 07INV-01INV-02INV-03
Pool A: Geänderte Records werden upserted (created oder updated). Pool B: Records, die in Schritt 2 nicht mehr in der ID-Liste waren, werden auf status="entwurf" gesetzt. Pool C: Records, die im Cache als soft-deleted markiert sind aber im Delta wieder auftauchen, werden auf status="veröffentlicht" reaktiviert.
Was passiert
Die Reihenfolge ist wichtig: Pool A → Pool B → Pool C. Ein Record kann pro Lauf nur in einem Pool landen, weil sich die Mengen gegenseitig ausschließen.
Quelle:Phase 7SOFT-06SOFT-12
Der Cursor wird erst nach dem letzten erfolgreichen Pool-Schritt geschrieben. Mid-Run-Crashes lassen den Cursor unverändert.
Was passiert
Wenn der Service mitten im Lauf crasht (Container-Neustart, Stromausfall etc.), wird beim nächsten Sync ab dem Stand des letzten erfolgreichen Laufs neu angefangen — keine Records gehen verloren, die schon verarbeiteten Records werden idempotent neu upserted.
Quelle:Hard Constraint: Two-Phase CommitPhase 2
Tour, POI und Gastro können parallel laufen, aber zwei Tour-Läufe gleichzeitig sind ausgeschlossen. Der Sync-Service hält dafür einen In-Memory-Lock.
Was passiert
Wenn ein Tour-Sync läuft und ein zweiter (z.B. via "Sync starten"-Button) versucht zu starten, wird er sofort mit "läuft bereits" abgewiesen.
Quelle:Hard Constraint: Per-Type MutexPhase 4
Wenn ein Record beim Upsert fehlschlägt (z.B. weil ein Pflichtfeld in OA leer ist), wird der Fehler geloggt, im Run-Detail sichtbar, und der Lauf läuft weiter.
Was passiert
Am Ende des Laufs zeigt das Dashboard z.B. "230 created · 5 errored" — die 5 fehlgeschlagenen Records erscheinen mit Fehlertext im Run-Detail, der Cursor wandert trotzdem weiter.
Quelle:Hard Constraint: Error ResiliencePhase 3
Operator-getriggert über Dashboard mit Bestätigungs-Dialog. Verarbeitet alle aktuell in OA gelisteten Records erneut, ohne den Delta-Cursor zu konsultieren. Inventory-Probe, Soft-Delete, Restore und Source-Filter laufen unverändert weiter.
Was passiert
Beim Klick auf "Vollständiger Resync" auf einer Status-Karte: Bestätigungs-Dialog erscheint mit der aktuellen Record-Anzahl ("Dies verarbeitet N tour-Records erneut"). Nach Bestätigung läuft EIN einzelner Sync-Lauf mit cursor=null durch die normale Pool A → B → C Pipeline. In der Run-Historie erscheint der Lauf als trigger="Manual (Voll)". Pro Content-Type ist nur ein Lauf gleichzeitig (Per-Type-Mutex bleibt geteilt zwischen cron/manual/manual_full).
Beispiel
Nach Hinzufügen eines neuen Field-Mappings will der Operator alle Tour-Records sofort mit dem neuen Feld füllen — Klick auf "Vollständiger Resync" auf der Tour-Status-Karte, Bestätigung im Modal, ~3 Minuten später ist Directus auf neuestem Stand. Records, die seitdem auch nicht mehr in OA sind, werden über die normale Soft-Delete-Logik auf "entwurf" gesetzt.
Quelle:Phase 8D8-04D8-08D8-10FORCE-02FORCE-04FORCE-08
Pro Tour/POI/Gastro gleicht der Sync die OA-Bild-IDs gegen die existierenden Junctions ab: neue OA-IDs erzeugen neue Junctions in tours_images / pois_images / gastros_images, fehlende OA-IDs entfernen Junctions, gemeinsame OA-IDs PATCHen die Junction-Felder (sort/is_primary/is_logo). is_primary und is_logo werden aus der OA relations[] Liste abgeleitet (Werte "isPrimary" / "isLogo"). sort = Position im OA images[] Array.
Was passiert
Innerhalb eines Records läuft die Reihenfolge: Parent-Upsert → Bild-Reconciliation → Translations → Cache-Write. Eine leere images[]-Antwort von OA bedeutet "alle Junctions für diesen Parent löschen" — das ist KEIN Abbruch (anders als bei einer leeren oois-Antwort auf der Listing-Seite).
Quelle:Phase 9Phase 12IMG-05IMG-07IMG-09IMG-10GAST-05
Die vier Phase-9 Collections (oa_images, oa_images_translations, tours_images, pois_images) werden via in-Repo-Script `tsx packages/sync-service/scripts/bootstrap-oa-images.ts` mit einem separaten DIRECTUS_ADMIN_TOKEN angelegt. Das Script ist idempotent — Re-Runs sind no-ops (per-collection GET /collections/{name} probe).
Was passiert
Beim Boot prüft der Sync-Service ob oa_images existiert. Fehlt die Collection, beendet sich der Service mit einem Hinweis auf das Bootstrap-Script (Exit-Code 1, Hinweis auf stderr). Der Runtime-DIRECTUS_TOKEN hat keine Schema-Rechte — Schema-Änderungen sind weiterhin Out-of-Scope für die Sync-Runtime, aber der einmalige Bootstrap ist die schmale Ausnahme.
Quelle:Phase 9IMG-01IMG-02