OA Sync
tour: bereitpoi: bereitgastro: bereit

Regeln & Logiken

Wie der Sync zwischen Outdooractive und Directus tatsächlich funktioniert — die fest verdrahteten Invarianten, der Ablauf eines Laufs, die Ausnahmen, und was bewusst draußen bleibt. Damit auch in vier Wochen noch nachvollziehbar ist, was hier gebaut wurde.

Stand: 2026-05-08Letzter Phasen-Abschluss: Cleanup — Landlord end-to-end entfernt

Sync-Ablauf auf einen Blick

┌─────────────────────────┐
  │  Outdooractive Data API │
  └────────────┬────────────┘
               │
       ┌───────▼────────┐  Schritt 1: Delta-Listing
       │  geänderte IDs │  (lastModifiedAfter = Cursor;
       │                │   bei trigger='manual_full': cursor=null
       │                │   → Vollständiges Listing aller OA-Records)
       └───────┬────────┘
               │
       ┌───────▼────────┐  Schritt 2: Inventory-Probe (Phase 7)
       │  alle aktuellen│  (cursor=null + typeFields=id)
       │  IDs der Quelle│
       └───────┬────────┘
               │
   ┌───────────┴───────────┐
   │  Schritt 3: 3 Pools   │
   │  nacheinander         │
   └───────────┬───────────┘
               │
   ┌───────────▼───────────┐
   │ Pool A: Upsert        │  geänderte Records → Directus
   │   (Pro Record:        │  Parent-Upsert → Bild-Reconciliation →
   │    Parent → Bilder    │  Translations → Cache-Write
   │    → Trans. → Cache)  │  (Bild-Step ab Phase 9)
   │   ↓                   │
   │ Pool B: Soft-Delete   │  Records, die aus der Quelle
   │   ↓                   │  verschwunden sind, werden zu
   │   ↓                   │  status='entwurf'
   │ Pool C: Restore       │  Records, die wieder aufgetaucht
   │                       │  sind, werden zu 'veröffentlicht'
   └───────────┬───────────┘
               │
       ┌───────▼────────┐  Schritt 4: Two-Phase Commit
       │  Cursor + Run- │  (Cursor advanced erst wenn Run
       │  Counts speich.│   erfolgreich abgeschlossen wurde)
       └────────────────┘

Wer bestimmt was — Datenhoheit

Outdooractive ist die Quelle der Wahrheit für alle Felder, die im Field-Mapping als OA-Feld markiert sind. Eigene redaktionelle Felder bleiben unangetastet.

OA-Felder werden bei jedem Sync überschrieben

Felder wie Bezeichnung, Beschreibung, GPS-Tracks und Bilder kommen aus Outdooractive. Bei jedem Sync-Lauf werden sie mit den aktuellen OA-Daten aktualisiert.

Was passiert

Manuelle Änderungen an OA-Feldern in Directus werden beim nächsten Sync verworfen. Eigene redaktionelle Felder (alle, die nicht im Mapping als OA-Feld stehen) bleiben unverändert.

Beispiel

Jemand ändert in Directus die Tour-Bezeichnung von "Säntis-Rundwanderung" auf "Säntis Tour" — beim nächsten Sync wird wieder "Säntis-Rundwanderung" eingetragen, weil das Feld in OA so steht.

Quelle:Hard Constraint: Single Source of TruthPhase 3

Bilder bleiben URL-Referenzen, sie werden nicht heruntergeladen

Bilder werden als Outdooractive-CDN-URL in der dedizierten oa_images Collection gespeichert (seit Phase 9), nicht als hochgeladene Files. URL ist eine Vorlage mit {variant}-Platzhalter — der Frontend setzt eine konkrete Variante (large/thumb/...) bei der Render-Zeit ein. Identität ist die OA-Image-ID, NICHT die URL — wenn OA das CDN rotiert, PATCHt der Sync nur das url-Feld, der Datensatz bleibt erhalten.

Was passiert

Wenn das OA-CDN ausfällt, sind die Bilder im Frontend nicht erreichbar — eine lokale Kopie existiert nicht. URL-Drift bei gleicher OA-Image-ID erzeugt eine PATCH-Operation auf oa_images.url, keine neue Zeile.

Quelle:Hard Constraint: Images URL-onlyPhase 1Phase 9 IMG-08

Neue Records werden direkt veröffentlicht

Es gibt keine Draft- oder Review-Queue. Neu von OA gesyncte Records erscheinen sofort mit status="veröffentlicht" in Directus.

Was passiert

Wenn ein neuer Record in OA fehlerhaft ist (z.B. Beschreibung leer), ist er trotzdem unmittelbar live. Ausnahme: Records aus dem Soft-Delete-Lifecycle (siehe unten) bekommen status="entwurf".

Quelle:Hard Constraint: Auto-publishPhase 3

Der Sync-Service darf nichts in Directus löschen

Das Service-Account hat nur Create + Update auf den synchronisierten Collections — kein Delete. Das ist eine harte Berechtigungs-Grenze in Directus, nicht nur eine Code-Regel.

Was passiert

Selbst wenn der Sync-Code fehlerhaft DELETE versuchen würde, würde Directus mit 403 antworten und der Lauf würde mit Fehler enden.

Quelle:Hard Constraint: No DeletePhase 3Pre-Launch Gate

Wie ein Sync abläuft (in 4 Schritten)

Jeder Lauf folgt diesem festen Schema. Schritt 2 (Inventory-Probe) wurde in Phase 7 ergänzt, alles andere stammt aus Phase 1–4.

Schritt 1: Geänderte Records seit letztem Lauf holen

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

Schritt 2: Vollständige ID-Liste prüfen

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

Inventory-Probe verifiziert numFound + führt eine zweite Probe zur Union-Bildung durch

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

Schritt 3: Drei Pools nacheinander abarbeiten

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

Schritt 4: Cursor erst am Ende speichern (Two-Phase Commit)

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

Pro Content-Type läuft nur ein Sync gleichzeitig

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

Einzelne Record-Fehler stoppen den Lauf nicht

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

Vollständiger Resync ignoriert den Cursor (Phase 8)

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

Bilder werden über OA-Image-ID abgeglichen, nicht über Reihenfolge

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

Bilder-Schema wird vor dem Deploy einmal manuell angelegt

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

Source-Filter — welche Inhalte synchronisiert werden

Outdooractive liefert auch Records von Drittquellen. Mit der Whitelist wird festgelegt, welche Quellen-IDs in Directus landen.

Optionale Whitelist pro Content-Type

Tour, POI und Gastro haben jeweils eine eigene Whitelist im Mapping. Eine leere Whitelist bedeutet "kein Filter" (= alle Quellen werden synchronisiert).

Was passiert

Wenn die Tour-Whitelist auf [1010, 2050] gesetzt ist, werden nur Touren synchronisiert deren OA-Quellen-ID 1010 oder 2050 ist. Alle anderen Touren werden bei Schritt 3 Pool A übersprungen.

Beispiel

In Directus → Mappings → Tour → Quellen-Whitelist: ID "1010" hinzufügen. Beim nächsten Sync nur noch Touren von Quelle 1010 in Directus.

Quelle:Phase 7FILT-01

Whitelist wird nach dem Detail-Fetch ausgewertet

Die Quellen-ID eines Records steht im Detail-Response (meta.source.id), nicht im Listing. Deshalb wird der Filter erst angewendet, nachdem der vollständige Record-Detail von OA geholt wurde.

Was passiert

Records, die durch den Filter fallen, werden NICHT in Directus geschrieben. Wenn ein Record schon in Directus existiert und nachträglich aus der Whitelist fällt, wird er beim nächsten Sync soft-deleted (er ist dann nicht mehr in der "aktiven" ID-Menge).

Quelle:Phase 7FILT-02

Whitelist-Änderungen wirken ohne Service-Neustart

Hinzufügen oder Entfernen von Quellen-IDs in der Dashboard-Mapping-Seite gilt sofort beim nächsten Sync-Lauf — der Service hält den Mapping-Stand nicht im Speicher.

Was passiert

Manueller Sync direkt nach einer Whitelist-Änderung verwendet schon den neuen Filter. Ein laufender Sync arbeitet allerdings mit dem Stand vom Lauf-Start zu Ende (siehe Mappings-Seite-Hinweis).

Quelle:Phase 7FILT-03

Verschwundene Records — Soft-Delete + Restore-Lifecycle

Records, die aus der OA-Quelle verschwinden, werden NICHT aus Directus gelöscht (das darf der Service ohnehin nicht). Stattdessen werden sie auf "Entwurf" gesetzt — und wenn sie wieder auftauchen, automatisch reaktiviert.

Verschwundene Records werden zu "Entwurf", nicht gelöscht

Wenn ein Record aus der OA-Quelle verschwindet (z.B. weil er in OA archiviert oder aus der Whitelist entfernt wurde), setzt der Sync seinen Directus-Status auf "entwurf" und vermerkt einen Zeitstempel im lokalen Cache.

Was passiert

Der Record verschwindet damit aus der Frontend-Auslieferung (Directus-typische Filterung auf status="veröffentlicht"). Inhalt + Bilder bleiben aber erhalten — kein Datenverlust.

Quelle:Phase 7SOFT-06

Wieder aufgetauchte Records werden automatisch reaktiviert

Wenn ein soft-deleted Record später wieder im OA-Delta-Listing auftaucht, setzt der Sync ihn auf status="veröffentlicht", löscht den Soft-Delete-Zeitstempel und überschreibt alle OA-Felder mit dem aktuellen OA-Stand.

Was passiert

Operator muss nichts manuell machen — der Record ist nach dem Restore-Sync wieder live. Im Run-Detail erscheint er als "restored" (sky-blau).

Quelle:Phase 7SOFT-12

Inventory-Probe-Fehler stoppt den Lauf — keine falschen Soft-Deletes

Wenn die ID-Liste-Abfrage (Schritt 2) fehlschlägt — z.B. 5xx-Antwort von OA, Netzwerk-Timeout — bricht der Lauf mit status="failed" ab und der Cursor wird NICHT weitergeschoben.

Was passiert

Ein transienter OA-Ausfall führt damit nicht dazu, dass der gesamte Bestand fälschlicherweise als verschwunden gilt. Der nächste Sync wiederholt die Probe und macht erst dann mit Soft-Delete + Restore weiter.

Quelle:Phase 7SOFT-01

Run-Counts unterscheiden created/updated/soft-deleted/restored

Jeder Run zeigt im Dashboard separate Counts für jeden Pool. Soft-Delete-Counts > 0 sind amber-gefärbt, Restore-Counts sky-blau.

Was passiert

Auf einen Blick erkennbar, ob ein Lauf "normal" war (nur created/updated > 0) oder ob es zu Lifecycle-Übergängen kam. Im Detail-Header steht das ganze 6-spaltig.

Quelle:Phase 7SOFT-07SOFT-08

Bilder-Counter zählen Schreibvorgänge, nicht Bilder im Bestand

Bilder erstellt / Bilder aktualisiert zählen Schreibvorgänge auf der oa_images Collection (CREATEs bzw. PATCHes wegen URL-Drift) UND Junction-Erst-/Updateoperationen. Junctions entfernt zählt entfernte Verknüpfungen Tour↔Bild bzw. POI↔Bild.

Was passiert

oa_images Einträge selbst werden nie gelöscht (siehe Out-of-Scope: bilder-orphan-gc). Auf einen Run mit hohem "Bilder aktualisiert"-Wert deutet OA-seitige URL-Drift hin (selbe OA-ID, neue CDN-URL) oder geänderte Reihenfolge / is_primary-Verteilung.

Quelle:Phase 9IMG-14D9-10D9-11

Mapping-Änderungen — wann wirken sie auf bestehende Records?

Wichtig zu verstehen: Der Sync ist Delta-basiert. Eine Mapping-Änderung (neues Feld, geänderte Übersetzungs-Mapping, neue Whitelist-Quelle) wirkt nicht rückwirkend auf alle Directus-Records — sondern nur auf Records, die der nächste Lauf ohnehin anfasst, weil sie sich in OA geändert haben.

Neue Mapping-Felder werden nur auf Records angewendet, die der nächste Sync anfasst

Wenn du im Mapping ein neues OA-Feld hinzufügst, schreibt der Writer dieses Feld ab dem nächsten Sync-Lauf mit. Aber nur für Records, die im Delta-Listing zurückkommen — also Records, deren `lastModifiedAt` in OA seit dem letzten Cursor weitergewandert ist.

Was passiert

Ein OA-Record, der seit Monaten in OA unverändert ist, erscheint nicht im Delta. Der Sync fasst ihn nicht an. Das neue Feld bleibt für diesen Record in Directus leer — bis irgendwann etwas in OA an dem Record geändert wird (Tippfehler-Korrektur, Bild-Update, neue Beschreibung, …) oder bis ein Cursor-Reset einen Full-Historical-Pull erzwingt.

Beispiel

Tag X: Du fügst das Mapping „Schwierigkeitsgrad" für Touren hinzu. Tag X+1: Tour A wird in OA von einem Redakteur editiert → kommt im nächsten Delta zurück → bekommt den Schwierigkeitsgrad in Directus. Tour B liegt seit 8 Monaten unverändert in OA → wird nicht angefasst, der Schwierigkeitsgrad bleibt leer.

Quelle:Phase 2Phase 3engine.ts:215

Mapping-Cache-Invalidierung ≠ Record-Re-Sync

Wenn du ein Mapping über die Dashboard-UI änderst (PATCH /api/v1/mappings/:type), wird der In-Memory-Mapping-Cache des Sync-Service sofort invalidiert. Das bedeutet: der Writer kennt ab sofort das neue Feld. Es bedeutet NICHT: der Service zieht alle Records nochmal neu.

Was passiert

Die Bestätigung „Feld-Mapping gespeichert" garantiert nur, dass der nächste Sync-Lauf das neue Feld kennt — nicht, dass bestehende Records in Directus damit nachgepflegt werden. Der Cursor-basierte Delta-Pfad gilt unverändert weiter.

Quelle:Phase 4mapping-loader.ts: invalidateMappingCache

Cursor-Reset pro Content-Type erzwingt einen Full-Historical-Pull

Der Cursor wird aus `MAX(oa_id_cache.last_modified_at)` abgeleitet — es gibt keine separate Cursor-Spalte. Wenn du den `oa_id_cache` für einen Content-Type leerst, ist der Cursor automatisch zurückgesetzt, und der nächste Lauf zieht alle Records dieses Typs aus OA.

Was passiert

Der Writer findet existierende Directus-Einträge via `GET ?filter[oa_id][_eq]=…` und PATCHed sie mit dem aktuellen Mapping (inkl. der neuen Felder). Es wird KEINE Massen-CREATE-Welle geben — Directus-Inhalte bleiben erhalten, nur die OA-Felder werden mit dem aktuellen Mapping-Stand überschrieben.

Beispiel

Touren-Mapping erweitert, Backfill der bestehenden ~600 Touren gewünscht: `DELETE FROM oa_id_cache WHERE content_type = 'tour'; DELETE FROM sync_records WHERE run_id IN (SELECT id FROM sync_runs WHERE content_type = 'tour'); DELETE FROM sync_runs WHERE content_type = 'tour';` → Sync-Service neu starten → manueller Tour-Sync. Heavy-Operation, einige Minuten Laufzeit, aber der einzige Weg zum Backfill ohne Force-Full-Feature.

Quelle:Phase 2cursor.ts:23 (selectMaxLastModifiedAt)D-43D-44

„Vollständiger Resync"-Knopf pro Content-Type (v1.2)

Phase 8 (v1.2) liefert pro Content-Type einen sekundären „Vollständiger Resync"-Button auf der Dashboard-Status-Karte. Ein Klick + Bestätigung re-prozessiert alle aktuell in OA gelisteten Records über die normale Pipeline — ohne Cursor, ohne Differ. Der frühere SQL-Reset-Workaround wird damit überflüssig.

Was passiert

Operator klickt „Vollständiger Resync" auf der Tour-Status-Karte → Modal mit Record-Anzahl + Beschreibung erscheint → Bestätigung → 202 mit run_id → Run läuft im Hintergrund durch Pool A (alle Records erneut) + Pool B (verschwundene Records → Soft-Delete) + Pool C (zurückgekehrte Records → Restore). Operator sieht den Lauf in der Run-Historie als „Manual (Voll)".

Beispiel

Tag X: Mapping „Schwierigkeitsgrad" wird hinzugefügt. Tag X: Operator klickt „Vollständiger Resync" auf der Tour-Status-Karte → bestätigt im Modal („Dies verarbeitet 612 tour-Records erneut") → ~3 Minuten später haben alle 612 Touren in Directus den Schwierigkeitsgrad. Kein SQL-Reset, kein Service-Restart.

Quelle:Phase 8D8-04D8-10FORCE-02FORCE-08FORCE-10

Fehler-Handling

Der Sync ist auf transiente Fehler ausgelegt — ein einzelner OA-Hänger, ein einzelner Directus-Timeout darf nicht den ganzen Lauf killen.

Leere oois-Antwort bei vollem Cache = potenzieller API-Fehler

Wenn OA bei einem Delta-Listing eine leere oois-Liste zurückgibt, obwohl der lokale Cache schon Records dieses Typs kennt, wird das als verdächtig behandelt — der Lauf bricht ab und der Cursor bleibt stehen.

Was passiert

Das ist ein Schutz gegen die OA-Quirk, dass die API gelegentlich 200 OK + leere Liste zurückgibt obwohl Records existieren. Ohne diesen Schutz würde der Cursor weitergeschoben und Records, die zwischenzeitlich geändert wurden, wären nicht mehr im nächsten Delta.

Quelle:Hard Constraint: Empty oois GuardPhase 2

InventoryProbeFailedError signalisiert nur noch transiente API-Fehler (kein numFound-Mismatch mehr)

Seit Phase 12 Plan 07 wirft die Inventory-Probe `InventoryProbeFailedError` NUR noch bei harten Transport-Fehlern (HTTP 5xx-Retry-Exhaustion → `ListingFailedError` wird auf Engine-Ebene umgewandelt) — der frühere numFound-Mismatch-Throw (`reason.kind="numfound_mismatch"`) wurde durch einen strukturierten Warn-Log ersetzt (`event="inventory_probe_numfound_drift"`, `severity="drift"`, `missing_count=…`). Begründung: Defense 2 (Union zweier Proben) ist die empirisch belegte Schutz-Schicht, Defense 1 hatte Full-Syncs gegen Projekte mit Cross-Page-Duplikaten komplett blockiert ohne zusätzlichen Schutz zu liefern (12-05-LIVE-SMOKE.md, 12-07-PLAN.md).

Was passiert

Bei HTTP 5xx-Failures während der Inventory-Probe sieht der Operator weiterhin `InventoryProbeFailedError` im Run-Detail, der Cursor wird NICHT vorgeschoben, der nächste Run wiederholt automatisch das volle Window. Bei numFound-Drift (häufiger Fall in der Praxis: cross-page Duplikate) erscheint nur noch ein Warn-Log mit Drift-Diagnose; der Run läuft auf `status="succeeded"` durch und Defense 2 garantiert per Union, dass keine echten IDs verloren gehen können.

Quelle:Phase 7.1Phase 12 Plan 07INV-01oa/errors.ts InventoryProbeFailedErroroa/fetcher.ts logger.warn severity=drift

OA-Rate-Limits werden respektiert (429 + Retry-After)

Wenn OA mit HTTP 429 antwortet, wartet der HTTP-Client (ky) so lange wie der Retry-After-Header sagt, dann probiert er den Request nochmal.

Was passiert

Burst-Sync-Versuche stauen sich auf, statt mit Fehlern zu enden. Cron-Schedules sind bewusst gestaffelt (Touren :00, POIs :20, Gastro :40), um den Service Rate-freundlich zu halten.

Quelle:Phase 2Schedule-Staffelung Empfehlung

Übersetzungs-Junctions werden nie blind beschrieben

Directus-Übersetzungs-Tabellen erlauben Duplikate per blind POST. Der Sync prüft deshalb pro Locale erst ob die Junction-Zeile existiert (GET), und macht dann gezielt PATCH oder POST.

Was passiert

Ohne diesen Check würden bei jedem Sync neue Junction-Zeilen für dieselbe Locale entstehen. Der Sync vermeidet das durch das GET-then-PATCH-Pattern.

Quelle:Hard Constraint: Translation De-DupPhase 3

Berechtigungs-Fehler vom Directus-Service-Account stoppt den Lauf

Wenn das Service-Account in Directus eine UPDATE/CREATE-Operation mit 403 quittiert (z.B. weil eine Berechtigung versehentlich entfernt wurde), wird der ganze Lauf mit status="failed" beendet.

Was passiert

Ein Permission-Bug fliegt sofort auf, statt im Hintergrund still alle Records zu skippen. Der Operator sieht den Fehler im Run-Detail.

Quelle:Phase 3D-65

Bild-Sync — oa_images Collection (Phase 9)

Phase 9 ergänzt den Sync um Bild-Verwaltung: OA-Bilder werden als oa_images-Records in Directus gespeichert und per Junction mit Tours/POIs verknüpft. Die oa_images-Collection muss EINMALIG von einem Admin-Operator angelegt werden, bevor der Sync-Service in Produktion startet.

oa_images-Bootstrap muss vor dem ersten Produktions-Deploy laufen

Bevor der Sync-Service mit Phase-9-Code deployed wird, muss ein Admin-Operator das Bootstrap-Script `scripts/bootstrap-oa-images.ts` einmalig gegen die Produktions-Directus-Instanz ausführen. Das Script legt die `oa_images`-Collection + alle benötigten Felder und Junctions an.

Was passiert

Wenn die `oa_images`-Collection fehlt, verweigert der Sync-Service den Start: er gibt auf stderr einen Fehlertext mit dem Bootstrap-Befehl aus und beendet sich mit Exit-Code 1. Ein Redeploy des Containers löst das Problem nicht — erst das Bootstrap-Script legt die Collection an, danach kann der Container normal starten. Reihenfolge: (1) Plan-03-Bootstrap-Script auf Prod-Directus ausführen → (2) Sync-Service-Container neu deployen → (3) Service startet sauber.

Beispiel

Container startet → stderr: "[boot] Required Directus collection 'oa_images' is missing. Run the bootstrap script …" → Exit 1. Coolify zeigt den Container als unhealthy. Fix: `DIRECTUS_ADMIN_TOKEN=<token> DIRECTUS_URL=<url> tsx packages/sync-service/scripts/bootstrap-oa-images.ts` auf dem Operator-Workstation ausführen → Container neu deployen.

Quelle:Phase 9IMG-02REQ-2src/preflight.ts

oa_images-Records werden vom Sync nie gelöscht

Wenn ein Bild aus einem OA-Record entfernt wird, löscht der Sync NUR die Junction-Zeile (tours_images / pois_images) — das oa_images-Record selbst bleibt in Directus erhalten.

Was passiert

Bilder akkumulieren sich in der oa_images-Collection, werden aber nicht weiter referenziert. Manuelles Aufräumen fällt nicht in den Sync-Verantwortungsbereich. Diese Entscheidung vermeidet Datenverlust falls ein Bild von mehreren Records referenziert wird oder redaktionell weiterverwendet wird.

Quelle:Phase 9IMG-11REQ-11Hard Constraint: Images URL-only

Bild-Reconciliation ist Schritt (d.5) in der Pool-A-Pipeline — nach Parent-Upsert, vor Translations

Für jeden Tour- oder POI-Record führt der Writer nach dem Parent-Upsert einen replace-by-oa_id Delta-Abgleich der Bilder durch: neue Bilder werden als oa_images angelegt + gejunctioned, geänderte URL/Autor/Copyright werden gepatcht, entfernte Bilder verlieren nur die Junction-Zeile. Schlägt dieser Schritt fehl, wird der Fehler in sync_records mit dem synthetischen lang-Wert '_image' erfasst und der Lauf läuft weiter.

Was passiert

Ein Bild-Fehler (z.B. Directus antwortet auf POST oa_images mit 422) bricht weder den Record-Upsert noch den gesamten Run ab. Der Operator sieht im Run-Detail eine Zeile mit outcome="error" und lang="_image" für den betroffenen OA-Record. Alle anderen Records und Sprachen laufen unverändert durch.

Quelle:Phase 9IMG-05IMG-06IMG-07IMG-08D9-08D9-09

Run-Detail zeigt images_created / images_updated / image_junctions_deleted

Jeder Sync-Lauf summiert drei neue Bild-Counter: images_created (neue oa_images oder neue Junction), images_updated (URL-Drift oder Junction-Attribut-Drift), image_junctions_deleted (Junction für entferntes Bild wurde gelöscht). Diese Counter sind unabhängig von created_count/updated_count, die den Parent-Record betreffen.

Was passiert

Bei einem normalen Run ohne Bild-Änderungen sind alle drei Counter 0. Wenn OA für eine Tour ein Bild entfernt hat, steigt image_junctions_deleted um 1. Die Counter erscheinen im Run-Detail-Header direkt unter den klassischen 6 Countern.

Quelle:Phase 9D9-10IMG-05IMG-07

Was bewusst NICHT im Sync ist (v1)

Diese Punkte sind explizit aus v1 ausgeschlossen. Die Begründung steht jeweils dabei — wenn sich der Bedarf ändert, kann ein Punkt in eine spätere Milestone-Version aufgenommen werden. Hinweis: Force-Full-Resync war ursprünglich ebenfalls out-of-scope und wurde mit Phase 8 (v1.2) eingeführt — siehe Abschnitt „Wie ein Sync abläuft".

Events (Veranstaltungen) werden nicht synchronisiert

Nur Tour, POI und Gastro sind in v1 abgedeckt. Events haben zusätzliche Komplexität (Termine, Wiederholungen, Storno).

Was passiert

Events in OA bleiben dort. Eine Aufnahme in den Sync würde eine eigene Phase mit erweitertem Datenmodell erfordern.

Quelle:Out of Scope: Events

Kein Two-way-Sync (Directus → OA)

Der Sync läuft strikt einbahnstraßig: OA ist Quelle, Directus ist Senke.

Was passiert

Änderungen an OA-Feldern in Directus haben keinerlei Effekt auf OA — und sie überleben den nächsten Sync nicht (siehe Datenhoheit).

Quelle:Out of Scope: Two-way Sync

Bilder werden nicht heruntergeladen

Bilder bleiben URL-Referenzen aufs OA-CDN. Lokale Files sparen wir uns.

Was passiert

Wenn das OA-CDN für eine längere Zeit ausfällt, sind die Bilder im Frontend nicht erreichbar — keine lokale Fallback-Kopie existiert.

Quelle:Out of Scope: Image DownloadHard Constraint: Images URL-only

Kein Hard-Delete in Directus

Verschwundene Records werden soft-deleted (siehe Phase 7), nie endgültig aus Directus entfernt.

Was passiert

Selbst wenn ein Record in OA dauerhaft archiviert wird, bleibt er als status="entwurf" in Directus liegen. Manuelles Aufräumen fällt nicht in den Sync-Verantwortungsbereich.

Quelle:Out of Scope: Hard DeletionPhase 7

Verwaiste oa_images-Einträge werden nicht aufgeräumt

Wenn die letzte Junction auf eine oa_images-Zeile wegfällt (z.B. weil OA das Bild aus allen Touren entfernt hat), bleibt die oa_images-Zeile stehen. Der Sync löscht NIE oa_images-Zeilen — nur die Junction-Tabellen tours_images und pois_images werden gepflegt.

Was passiert

Der Sync-Service-Token hat 403 auf DELETE /items/oa_images/:id (Pre-Launch-Gate-Audit). Orphans akkumulieren by-design — bei spürbarem Bestand: manuelle SQL-Bereinigung als Wartungsfenster. Eine separate Cleanup-Routine kann in einer späteren Milestone nachgerüstet werden.

Quelle:Phase 9IMG-11Out of Scope: Orphan-GC

Kein Application-Level-Login fürs Dashboard

Das Dashboard ist via Traefik Basic Auth (Coolify-Label) geschützt — keine Session, kein NextAuth, kein User-Mgmt.

Was passiert

Operator-Authentifizierung passiert auf Infrastruktur-Ebene, nicht in der App. Das hält das Dashboard schlank und vermeidet Auth-State-Komplexität.

Quelle:Hard Constraint: Dashboard Auth (Traefik only)Phase 5

Kein aktives Alerting (E-Mail / Push / Pager)

In v1 wird auf Dashboard-Check vertraut — Operator schaut auf /runs und sieht Failures dort.

Was passiert

Wenn ein Cron-Lauf scheitert und niemand das Dashboard öffnet, fällt der Failure erst beim nächsten Hingucken auf. Bei Skalierung auf mehr Projekte muss aktives Alerting nachgezogen werden (vermerkt im Backlog).

Quelle:Out of Scope: Active Alerting

POI-Mapping-Übersicht — welche OA-Felder landen wo

Phase 10 schließt die Tour↔POI-Asymmetrie. Tour hatte 36 field_mappings, POI nur 12 — POI-Frontend war ohne Beschreibung, Teaser, Öffnungszeiten und Kontakt unterwegs. Diese Sektion dokumentiert, welche OA-Felder Phase 10 für POI hinzufügt.

POI-Texte werden pro Sprache in poi_<group>_translation geschrieben

Pro Locale (de/en/it) füllt der Sync sieben POI-Übersetzungs-Tabellen: poi_stammdaten_translation (OBJECT_TEXT_NAME = title), poi_allgemein_texte_so_translation (OBJECT_TEXT_BESCHREIBUNG_SOMMER_HTML = texts.long, OBJECT_TEXT_TEASER_SOMMER = texts.short, OBJECT_TEXT_TEASER_SOMMER_HTML = teaserText), poi_oeffnungszeiten_translation (OBJECT_OEFFNUNGSZEITEN = texts.businessHours), poi_preis_translation (PRICE_INFO = texts.fee), poi_anreise_translation (OBJECT_ANREISE = texts.gettingThere).

Was passiert

Fehlt ein Locale-Wert in OA, wird die jeweilige Spalte übersprungen — bestehende Directus-Werte bleiben unverändert. Eine Locale ohne JEDEN gemappten Wert in einer Section bekommt gar keine Junction-Zeile (Skip-on-missing per Phase 3 D-56). Leere Strings werden hingegen geschrieben — Single-Source-of-Truth überschreibt veraltete Directus-Werte.

Quelle:Phase 10D10-01POIM-01..07payload-builder.ts:222-228

POI-Titel wird in zwei Spalten geschrieben (Top-Level + Translation)

Wie bei Tour (Phase 3 D-66) wird der OA-Titel sowohl in poi.Bezeichnung (Top-Level, einsprachig) als auch in poi_stammdaten_translation.OBJECT_TEXT_NAME (per Locale) gespiegelt. Damit hat das Frontend sowohl einen sprachunabhängigen Such-Index als auch lokalisierte Titel.

Was passiert

Editorial überschreibt einen Titel manuell? Beim nächsten Sync werden beide Spalten überschrieben — das ist Single-Source-of-Truth-Verhalten (Hard Constraint). Die D-72 sibling-row-Erhaltung im mapping-loader sorgt dafür, dass beide field_mappings-Zeilen mit oa_path="title" überleben.

Quelle:Phase 10D10-01D-66POIM-07mapping-loader.ts:182-191

Kontakt-Felder werden top-level identity gemappt

contact.email → OBJECT_CONTACT_EMAIL, contact.phone → OBJECT_CONTACT_TEL, contact.fax → OBJECT_CONTACT_FAX, meta.author.firstname → AUTOR_VORNAME. Alle vier sind sprachunabhängige Top-Level-Felder.

Was passiert

Liefert OA für einen Record kein contact.email, bleibt OBJECT_CONTACT_EMAIL in Directus auf seinem alten Wert (oder NULL bei Neuanlage). Skip-on-missing gilt hier wie überall (payload-builder.ts:159).

Quelle:Phase 10D10-03D10-04POIM-09POIM-10

POI-Properties landen als Liste in merkmal

OA properties[] (Array von {id, name, title}) wird auf das Directus-JSON-Feld merkmal gemappt — nur die title-Strings, in OA-Reihenfolge. Analog zu category.title → kategorie (das aber einen Skalar wrappt, nicht eine Object-Liste). Der Transform heißt wrap-array-property-titles und ergänzt das bestehende wrap-array.

Was passiert

Ein POI ohne properties[]-Liste lässt merkmal unverändert. Eine leere properties[]-Liste schreibt [] in merkmal (überschreibt alte Werte). Editoriell gepflegte merkmal-Werte werden bei jedem Sync überschrieben — das Feld ist OA-besessen. Wenn OA properties[].title pro Locale lokalisiert (was beim Plan-01-Probe bestätigt wurde), reflektiert merkmal die Werte des zuletzt verarbeiteten Locales — derselbe Mechanismus wie bei category.title → kategorie.

Quelle:Phase 10D10-05POIM-11transforms.ts:wrapArrayPropertyTitles

Öffnungszeiten werden als 7 OBJECT_CLOSED_<TAG>-Booleans abgeleitet

Aus openTimes.weekdays[N].isOpen (true = offen) leitet der Sync sieben Booleans ab — semantisch invertiert: OBJECT_CLOSED_MO, OBJECT_CLOSED_DI, OBJECT_CLOSED_MI, OBJECT_CLOSED_DO, OBJECT_CLOSED_FR, OBJECT_CLOSED_SA, OBJECT_CLOSED_SO. N=0 ist Sonntag, N=6 Samstag (US-Konvention, abweichend von ISO-8601 — gegen die Plan-01-Fixture verifiziert).

Was passiert

Ein POI ohne openTimes.weekdays[]-Array lässt alle sieben Spalten unverändert (Transform liefert null → applyTopLevelMapping überspringt). Ein Wochentag-Eintrag ohne isOpen-Feld wird als „nicht geschlossen" (false) interpretiert. Time-Bereiche pro Tag werden NICHT gemappt — Editorial pflegt OBJECT_OEFFNUNGSZEITEN als Free-Text aus texts.businessHours.

Quelle:Phase 10D10-06POIM-12POIM-13transforms.ts:weekdayIsOpenInverse

Bewusst NICHT gemappt: parking, publicTransit, regions, coordinates, openTimes-Detail

texts.parking + texts.publicTransit (kein passendes Directus-Feld), regions[] / primaryRegion (eigene Phase ≥11 mit M2M-regions-Collection), coordinates[] in DD/DMS/UTM/w3w (point[0,1] reicht), openTimes.additionalInfo + openTimes.weekdays[].times[] (Time-Bereiche werden über OBJECT_OEFFNUNGSZEITEN aus texts.businessHours gepflegt), properties[].id und properties[].name (nur .title wird übernommen).

Was passiert

Diese Felder werden bei jedem Sync in sync_drift protokolliert — kein Fehler, kein Abbruch (Phase 3 D-56). Operator sieht die Drift-Volumes in der Dashboard-Mappings-Seite und kann später entscheiden, ob ein Pre-Launch-Schema-Change (z.B. zwei neue Spalten in poi_anreise_translation) gerechtfertigt ist. Vorgeschlagene Folgephase: Phase 11+ für regions, Phase 11+ für parking/publicTransit.

Quelle:Phase 10D10-02D10-07D10-08POIM-14POIM-15

Backfill bestehender POIs: „Vollständiger Resync"-Knopf nutzen

Phase-10-Mappings wirken nur auf POIs, die der nächste Delta-Sync anfasst (Cursor-basiert). Für Backfill der gesamten POI-Population: auf der Dashboard-POI-Status-Karte „Vollständiger Resync" klicken (Phase 8 FORCE-04). Der frühere SQL-Reset-Workaround ist nicht mehr nötig.

Was passiert

Beim Klick + Bestätigung läuft EIN Full-Resync-Run mit cursor=null durch die normale Pipeline (Pool A→B→C). Inventory-Probe und Soft-Delete bleiben aktiv — verschwundene POIs gehen wie gewohnt auf status="entwurf". Nach dem Lauf sind alle POIs mit den neuen 19 Mappings befüllt (oder skip-on-missing für absente OA-Felder).

Quelle:Phase 8Phase 10FORCE-04D8-04POIM-18

POI-Kategorie-Allowlist — welche category.keys synchronisiert werden

Phase 11 ergänzt zur Quellen-Whitelist eine zweite Filter-Dimension: pro POI-Record entscheidet category.keys[0], ob er überhaupt synchronisiert wird. Beide Filter sind UND-verknüpft — beide müssen passen, sonst wird der Record beim nächsten Sync auf status="entwurf" gesetzt (Soft-Delete, kein Datenverlust).

Welche category.keys[0]-Werte synchronisiert werden

Pro POI prüft der Sync den ersten Eintrag von category.keys[]. Steht der Wert in der Allowlist (collection_mappings.category_keys_allowlist), läuft der POI normal durch Pool A. Steht er NICHT drin, wird er soft-deleted (status="entwurf") — analog zur Quellen-Whitelist aus Phase 7.

Was passiert

Standardmäßig sind 80 Schlüssel in 5 Gruppen freigegeben (Architektur, Kultur & Sehenswürdigkeit, Sport & Freizeit, Verkehr & Mobilität, Natur & Outdoor — siehe Mappings → POI → Tab "Kategorien-Allowlist"). Pro Content-Type unterschiedliche Semantik: Tour hat die Spalte zwar, sie ist aber NULL = Filter aus; Gastro hat eine eigene live-derived 24-Schlüssel-Allowlist (Phase 12, siehe `gastro-source-allowlist`).

Quelle:Phase 11D11-12D11-14POIA-01POIA-04

Aus der Allowlist entfernte Kategorien werden soft-deleted

Entfernt der Operator einen Schlüssel aus der Allowlist, werden alle aktiven POIs in dieser Kategorie beim nächsten Sync auf status="entwurf" gesetzt — gleicher Lifecycle wie Phase 7 Soft-Delete (kein Datenverlust, Inhalt bleibt in Directus). Die zwei Filter (Quelle + Kategorie) sind UND-verknüpft: ein POI muss beide Filter passieren, um synchronisiert zu werden.

Was passiert

Vor dem Speichern zeigt der Editor eine Diff-Preview: „Δ +N / -M → X POIs archivieren, Y restoren". Bei mehr als 50 betroffenen POIs öffnet sich beim Save-Klick ein Bestätigungs-Modal.

Quelle:Phase 11D11-09D11-11D11-13POIA-06

Wieder hinzugefügte Kategorien restoren archivierte POIs

Fügt der Operator einen vorher entfernten Schlüssel wieder in die Allowlist ein, werden archivierte POIs dieser Kategorie automatisch beim nächsten Sync restored — Phase 7 Pool C bleibt unverändert. Status flippt von "entwurf" zurück zu "veröffentlicht", soft_deleted_at wird geleert, OA-Felder werden frisch geschrieben.

Was passiert

Im Editor sieht man im Doppel-Badge pro Schlüssel die Anzahl aktiver vs. archivierter POIs ([56 ✓] [3 ×]) — die archivierten Zahlen kennzeichnen den Restore-Impact bei erneutem Hinzufügen. Hover-Tooltip zeigt die exakten Zahlen.

Quelle:Phase 11D11-04Phase 7 D7-11POIA-09POIA-11

Editor speichert per Draft + Save-Button (kein Auto-PATCH)

Im Tab "Kategorien-Allowlist" werden Schlüssel und Gruppen über Checkboxen in lokalem React-Draft umgeschaltet. Erst beim Klick auf "Speichern" wird ein einziger PATCH /api/v1/mappings/poi gefeuert. "Verwerfen" stellt den Server-Stand wieder her.

Was passiert

Tabwechsel innerhalb der Mapping-Seite verwirft den Draft NICHT. Beim Verlassen der Seite ohne Speichern geht der Draft still verloren (kein Confirm-Dialog in v1).

Quelle:Phase 11D11-09D11-10POIA-10POIA-11

Bei mehr als 50 betroffenen POIs erscheint ein Bestätigungs-Modal

Wenn die Diff-Preview mehr als BULK_CHANGE_CONFIRM_THRESHOLD = 50 POIs (Summe aus Archivieren + Restoren) ergibt, öffnet der Save-Button erst ein Bestätigungs-Modal mit den exakten Zahlen. Unterhalb der Schwelle wird der PATCH direkt abgesetzt.

Was passiert

Die Schwelle ist als Konstante in @oa-sync/shared exportiert (BULK_CHANGE_CONFIRM_THRESHOLD = 50) — anpassbar im Code ohne Migration. Operative Erfahrung: bei einem typischen Phase-11-Erstdeploy archiviert das initiale Save eines konservativ kuratierten 80-Key-Default ca. 739 von ~1195 POIs, das Modal greift.

Quelle:Phase 11D11-11POIA-03POIA-12

Unbekannte category.keys aus OA bleiben sichtbar

Liefert OA einen category.keys[0]-Wert, der NICHT in der TS-Konstante POI_CATEGORIES steht (z.B. ein neuer OA-Schlüssel nach einem OA-seitigen Taxonomy-Update), erscheint er im Editor in einer separaten Sektion "Unbekannte Kategorien" am unteren Rand mit dem Roh-Schlüssel als Label. Operator kann ihn normal in die Allowlist aufnehmen.

Was passiert

Drift wird nicht zurückgehalten — der Editor verkraftet jede OA-Drift. Phase 3 D-56 sync_drift erfasst zudem die Roh-Strings für die Drift-Triage. Filter-seitig: Records mit unbekanntem category.keys[0], die NICHT in der Allowlist stehen, werden soft-deleted (das ist gewünscht — der Operator entscheidet bewusst, ob er den neuen Schlüssel aufnimmt).

Quelle:Phase 11D11-07POIA-11

Gastro: Source-Whitelist 1479261 + Kategorien-Allowlist (AND-kombiniert)

Gastro-Records werden aus OA POIs gefiltert: meta.source.id muss exakt 1479261 sein UND category.keys[0] muss in der Gastro-Allowlist stehen (live-derived in Phase 12 plan 12-01, 24 Schlüssel). Beide Filter werden AND-kombiniert ausgewertet — Phase-7-Pattern, geerbt vom Source-Filter.

Was passiert

Filter-fail bei einem existierenden Record → soft-delete (Pool C, Phase 7 D7-04). Allowlist-Erweiterung um einen archivierten Schlüssel → Restore beim nächsten Sync (Phase 7 D7-11). Bilder werden via gastros_images-Junction synchronisiert (Phase 9 GAST-05).

Quelle:Phase 7Phase 12GAST-02GAST-05D12-04b

Kategorien-Allowlist pro Content-Type — Übersicht

collection_mappings.category_keys_allowlist existiert für alle Content-Types (tour/poi/gastro). Pro Content-Type unterschiedliche Semantik: - **tour:** NULL — Filter aus per Design (kein OA-Mapping) - **poi:** 80-Schlüssel-Allowlist (Phase 11) — Detail siehe `mapping-poi-kategorie-allowlist` - **gastro:** live-derived 24-Schlüssel-Allowlist aus Source 1479261 (Phase 12) — Detail siehe `gastro-source-allowlist`

Was passiert

Operator hat einen zentralen Anker für die Kategorien-Situation pro Content-Type. Detail-Einträge bleiben für drill-down erhalten.

Quelle:Phase 11Phase 12D11-12D12-04

Diese Seite muss aktuell gehalten werden

Wenn eine Phase eine neue Invariante oder einen neuen Lifecycle-Schritt einführt, wird die Datei rules.ts ergänzt — sonst läuft diese Referenz still aus dem Tritt. Die Konvention steht in CLAUDE.md.

packages/dashboard/src/content/rules.ts