# Route-Events

> request-getriggert

---

Ein ergänzender, *sofortiger* Detektionsmechanismus: anstatt Zustand nach Zeitplan zu pollen, beobachtet das Modul die HTTP-Anfrage selbst. Wenn eine Anfrage an eine **abgebildete mutierende Route** erfolgreich bearbeitet wurde (2xx/3xx), queue sie das passende Ereignis in dieselbe `cj_event`-Queue — ein abhörender Event-Job reagiert dann beim nächsten Tick (~60 s), ohne Polling-Latenz.

**Welche Routes abgebildet sind** — zwei Regeln, beide abgeleitet aus der live webtrees-Routentabelle. Gemeinsame Voraussetzungen: Pfad beginnt mit `/tree/{tree}/`, der Handler ist im webtrees-Request-Handler-Namespace (`RequestHandlers` in 2.2.6, `Controllers` in 2.3), und die Anfrage ist eine **POST**-Mutation. In 2.2.6 wird dies bei der Abbildung erzwungen (GET-Seiten-Routes werden nicht abgebildet); in 2.3 trägt die Route keine HTTP-Methode, daher erfolgt die POST-Prüfung zur Auslösezeit und das GET-Formular-Load derselben Route wird ignoriert.

1. **Record-Routes** (regelbasiert — Core-Updates werden automatisch übernommen): der Pfad enthält `{xref}` (den Ziel-Datensatz) *und* der Handler-Klassenname beginnt mit `Edit`, `Delete`, `Add`, `Create`, `Link`, `Paste`, `Reorder` oder `Pending`. Der Ereignisname ist das dritte Pfad-Segment, namespaced `_route:<segment>`; wenn mehrere abgebildete Routes ein Segment teilen (`delete`, `edit-raw`, `accept`, `reject`), werden die folgenden Pfad-Parameter angehängt, Bindestrich-getrennt.
2. **Tree-Level-Routes** (kleine kuratierte Allowlist): die signifikanten baumweiten Mutationen, die kein `{xref}` in der URL tragen (Import, Merge, Renumber, …). Jeder Allowlist-Eintrag hat einen expliziten Ereignisnamen. Neue baumweite Mutationen in einem Core-Update brauchen ein Modul-Update (die Record-Route-Regel bleibt voll automatisch).

| Ereignis | Route (unter `/tree/{tree}/`) | Payload |
| :--- | :--- | :--- |
| `_route:edit-note-object` | POST `edit-note-object/{xref}` | `tree`, `xref` |
| `_route:update-record` | POST `update-record/{xref}` | `tree`, `xref` |
| `_route:update-fact` | POST `update-fact/{xref}{/fact_id}` | `tree`, `xref` (, `fact_id`) |
| `_route:delete-xref` / `_route:delete-xref-fact_id` | POST `delete/{xref}{/fact_id}` | `tree`, `xref` (, `fact_id`) |
| `_route:edit-raw-xref` / `_route:edit-raw-xref-fact_id` | POST `edit-raw/{xref}{/fact_id}` | `tree`, `xref` (, `fact_id`) |
| `_route:add-child-to-family`, `_route:add-spouse-to-family`, `_route:add-media-file` | POST `add-…/{xref}` | `tree`, `xref` |
| `_route:add-child-to-individual`, `_route:add-parent-to-individual`, `_route:add-spouse-to-individual` | POST `add-…/{xref}` | `tree`, `xref` |
| `_route:paste-fact` | POST `paste-fact/{xref}` | `tree`, `xref` |
| `_route:link-media-to-record` | POST `link-media-to-record/{xref}` | `tree`, `xref` — das **Media**-Objekt; der Ziel-Datensatz ist ein Formularfeld, nicht in der URL |
| `_route:link-child-to-family`, `_route:link-spouse-to-individual` | POST `link-…/{xref}` | `tree`, `xref` |
| `_route:reorder-children`, `_route:reorder-spouses`, `_route:reorder-media`, `_route:reorder-media-files`, `_route:reorder-names` | POST `reorder-…/{xref}` | `tree`, `xref` |
| `_route:accept-xref` / `_route:accept-xref-change` | POST `accept/{xref}{/change}` | `tree`, `xref` (, `change`) |
| `_route:reject-xref` / `_route:reject-xref-change` | POST `reject/{xref}{/change}` | `tree`, `xref` (, `change`) |
| `_route:import` | POST `import` | `tree` |
| `_route:load` | POST `load` | `tree` — die chunkweise Import-Fortsetzung feuert **einmal pro Chunk** |
| `_route:merge-step1` / `_route:merge-step2` | POST `merge-step1` / `merge-step2` | `tree` |
| `_route:search-replace` | POST `search-replace` | `tree` |
| `_route:renumber` | POST `renumber` | `tree` |
| `_route:data-fix-update` / `_route:data-fix-update-all` | POST `data-fix/{data_fix}/update[-all]` | `tree`, `data_fix` |
| `_route:accept` / `_route:reject` | POST `accept` / `reject` (Stück) | `tree` |
| `_route:change-family-members` | POST `change-family-members` | `tree` — der Familien-XREF ist ein Formularfeld |

Insgesamt 38 Events (27 Record-Routes + 11 Tree-Level-Routes). Der Payload enthält immer `tree` (ID) + `tree_name` plus alle erkannten Route-Parameter. **Record-Routes** (mit `{xref}`) tragen zusätzlich `change_pending` (bool): ob die Änderung nach dieser Anfrage noch in der `change`-Tabelle aussteht (die Auto-Akzept-Präferenz des Benutzers ist aus) oder bereits angewendet (Auto-Akzept an). Eine ausstehende Änderung wird angewendet, wenn ein Admin sie akzeptiert — was ihre eigenen `_route:accept*`-Events feuert. Tree-Level-Routes schreiben direkt (keine `change`-Zeilen) und tragen kein solches Flag.

> **Breaking Change (diese Version).** Record-Routes *ohne* `{xref}` in der URL — alle `_route:create-*`-Events plus `_route:add-unlinked-individual` — werden nicht mehr abgebildet: sie tragen keinen Ziel-Datensatz. Jobs, die auf diese Events hören, feuern nie wieder; die Admin-Tabelle markiert solche Trigger mit einem **unknown route event**-Badge (eine generische Prüfung aller `_route:*`-Trigger aller Jobs gegen die live Map), sodass das Umrichten sichtbar statt still ist.
>
> `_route:accept*` / `_route:reject*` feuern, wenn eine *ausstehende Änderung* akzeptiert oder abgelehnt wird — die Akzeptierung einer ausstehenden Löschung ist der Moment, in dem der Datensatz tatsächlich entfernt wird.

**So funktioniert es**

- **Doppelt gated, standardmäßig aus.** Ein Event feuert nur, während der Admin-Schalter "Route-Events" (Events-Abschnitt) an ist *und* mindestens ein aktiver ereignisgesteuerter Job auf diesen Ereignisnamen hört. Eine nicht abgebildete Anfrage kostet nichts (eine In-Memory-Suche); nur eine erfolgreiche Anfrage auf einer abgebildeten Route macht zwei winzige indexierte DB-Lesevorgänge.
- **Transaktional.** Der `cj_event`-Insert läuft in der *gleichen DB-Transaktion* wie die Mutation (das Modul-Middleware sitzt innerhalb der webtrees-Request-Transaktion), damit das gequeue Event mit der Änderung zusammen committet oder zurückgerollt wird — kein Drift, kein Doppel-Feuer bei fehlgeschlagenem Speichern.
- **Blockiert nie.** Die Detektion läuft *nach* dem Handler, ist exceptions-umhüllt und kann die Antwort nie verändern.
- **Robust gegenüber Core-Upgrades.** Die Map wird aus der Routentabelle neu aufgebaut (geschlüsselt nach Handler-Klassen-*Strings*, kein `::class`), sodass ein umbenannter/entfernter Handler einfach aufhört zu matchen statt zu fatale. Wenn die Routentabelle nicht verfügbar ist (z.B. CLI), hält eine statische Fallback-Map mit der Note-Route den Piloten am Laufen. Die Record-Route-Regel ist voll automatisch; nur die kleine Tree-Level-Allowlist ist kuratiert (siehe oben), und Jobs, die auf ein nicht mehr existierendes Ereignis zeigen — ein entferntes Route-Event oder ein entferntes eingebautes Pseudo-Event — werden in der Admin-Tabelle markiert (**unknown event**-Badge).

Um darauf zu reagieren, erstellen Sie einen ereignisgesteuerten Job, dessen **Ereignisname** passt (z.B. `_route:update-record`) und aktivieren Sie den "Route-Events"-Schalter. Beachten Sie die Überlappung mit den `cronjob:log-edit-update` / `cronjob:log-edit-delete`-Pollern: jede Record-Edit/Delete wird auch in die Log-Tabelle geschrieben, sodass das passende Log-Event beim nächsten Poll ebenfalls folgt — wählen Sie den Mechanismus, dessen Latenz/Präzision zum Job passt (Route-Events sind quasi-sofort und pro Objekt präzise, und tragen `change_pending`; Log-Poller hängen ~5 min hinterher, fangen aber auch Änderungen außerhalb der Web-UI).
