# Ereignisse

> Ereignisgesteuerte Jobs, Webhooks, Pseudo-Events und Route-Events

---

<pre class="mermaid">flowchart TD
    subgraph Sources[&#34;📡 Event-Quellen&#34;]
        WH[&#34;`🔗 Webhook&lt;br/&gt;**GET &#43; Token**`&#34;]
        DC[&#34;`📞 Direkter Aufruf&lt;br/&gt;**EventQueue::push**`&#34;]
        PE[&#34;📋 Pseudo-Events&lt;br/&gt;Log-Tabelle-Poller&#34;]
        RE[&#34;🔀 Route-Events&lt;br/&gt;POST auf mutierende Routes&#34;]
    end

    subgraph Queue[&#34;⏳ Queue&#34;]
        CE[(&#34;`📦 cj_event&lt;br/&gt;**DB-Warteschlange**`&#34;)]
    end

    subgraph Tick[&#34;`⏱ Tick *(every 60 s)*`&#34;]
        MATCH[&#34;Events matchen&lt;br/&gt;coalesced: 1 per name&#34;]
        BUDGET[&#34;`⏲ **60 s** Wall-Clock-Budget`&#34;]
    end

    subgraph Execution[&#34;⚙ Ausführung&#34;]
        JOB[&#34;`Job-Skript&lt;br/&gt;child process via **proc_open**`&#34;]
        ENV[&#34;🌐 CRONJOB_EVENT&lt;br/&gt;CRONJOB_EVENT_PAYLOAD&#34;]
    end

    subgraph Storage[&#34;💾 Speicherung&#34;]
        RUN[(&#34;cj_run&lt;br/&gt;Laufverlauf&#34;)]
        ER[(&#34;cj_event_run&lt;br/&gt;Verknüpfung&#34;)]
    end

    WH --&gt; Queue
    DC --&gt; Queue
    PE --&gt; Queue
    RE --&gt; Queue
    CE --&gt; Tick
    MATCH --&gt; BUDGET
    BUDGET --&gt; JOB
    JOB --&gt; ENV
    JOB --&gt; RUN
    JOB --&gt; ER

    classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
    classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
    classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
    classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
    classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px

    class WH,DC,PE,RE sourceStyle
    class CE queueStyle
    class MATCH,BUDGET tickStyle
    class JOB,ENV execStyle
    class RUN,ER storageStyle
</pre>

Neben Zeitplänen kann ein Job durch ein **Ereignis** getriggert werden. Webtrees hat kein Ereignissystem, daher sind Ereignisse einfache DB-Zeilen (eine kleine `cj_event`-Queue), die der [Tick](./tick) einmal pro Minute leert. Ein ereignisgesteuerter Job läuft für ein gequeuedes Ereignis, dessen Namen seinen **Ereignisnamen** entspricht — **koalesziert:** pro Tick wird höchstens das *neueste* Ereignis pro Namen geleert (ein Lauf pro Namen pro Tick), und ältere ausstehende Duplikate des Namens werden verworfen — eine Flut identischer Events queue nie eine Flut von Job-Läufen (Job-Skripte sind per Konvention idempotent/inkrementell).

**Benennungsschema (analog zu Job-Namen).** Ereignisnamen sind Slugs (`a-z 0-9 _ -`, max. 64 Zeichen insgesamt), optional namespaced `<domain>:<slug>`:

| Form | Bedeutung |
| :--- | :--- |
| einfacher Slug (z.B. `index-dirty`) | eigene Events — Webhook oder direkter `EventQueue::push()` |
| `cronjob:<slug>` | eingebaute Pseudo-Events (Log-Tabelle-Poller) |
| `_route:<slug>` | Route-getriggerte Events (siehe unten) |
| `<module>:<slug>` | von einem Modul angekündigte Events (siehe "Events ankündigen") |

Ein Ereignis zu queueen hat vier Quellen:

1. **Webhook (externe Systeme).** GET den auf der Admin-Seite gezeigten Endpunkt (Abschnitt "Event webhook"): `GET /module/_cronjob_/Event?event=<name>&data=<json>` mit dem Token im `X-Cronjob-Token`-Header. `data` ist ein optionales JSON-Objekt, das mit dem Ereignis gespeichert wird. Der Endpunkt erfordert das Token, ist rate-limited (20/min pro Site) und akzeptiert nur Ereignisse, während das Modul aktiviert ist. Es ist ein **GET**, kein POST: webtrees lehnt POSTs ohne Session-CSRF-Token ab, den ein externer Aufrufender nicht senden kann.
2. **Direkter Aufruf (eigene Module).** Aus PHP-Code in einem anderen Modul: `\Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue::push('index-dirty', ['tree' => 'X']);` — dies erfordert, dass das Cronjob-Modul installiert ist; schützen Sie es mit `class_exists('Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue')`.
3. **[Pseudo-Events](./pseudo) (eingebaute Poller).** Webtrees hat keinen Event-Bus, daher werden Core-Aktionen (Logins, fehlgeschlagene Logins, Logouts, Datensatz-Edits, Fehler, Suchen) *durch Polling der webtrees-Log-Tabelle beobachtet*. Aktivieren Sie den angebotenen `cronjob:pseudo-events`-Job und das Modul liest die neuen Log-Einträge höchstens alle 5 Minuten, wobei pro Eintrag ein Event gequeued wird.
4. **[Route-Events](./route) (eingebaut, request-getriggert).** Ein kuratierter Satz mutierender Editor-Routes unter `/tree/{tree}/` (Record-spezifische Routes mit `{xref}` im Pfad, plus eine kleine Tree-Level-Allowlist) queue sein Ereignis sofort, direkt in der Anfrage, die die Änderung vorgenommen hat.

Der Tick gleicht jedes ausstehende Ereignis mit den aktivierten Jobs ab, die einen Ereignis-Trigger für diesen Namen tragen (ein Job kann mehrere Ereignis-Trigger haben oder keiner), führt sie aus und markiert dann das Ereignis als verarbeitet (einmal verbraucht — auch bei Fehler, damit ein fehlerhafter Job dasselbe Ereignis nicht ewig neu ausführt). Der Drain ist begrenzt: er nimmt das neueste Ereignis pro Namen (Koaleszierung, siehe oben) und stoppt nach einem 60-s-Wall-Clock-Budget, damit eine gesättigte Queue den Rest des Ticks nicht verhungern lässt — die verbleibenden Events setzen einfach mit dem nächsten Tick fort. Jeder verbrauchende Lauf wird zusätzlich über die `cj_event_run`-Kreuztabelle mit dem Ereignis verknüpft (ein Ereignis kann mehrere Jobs triggern), und die Ausführungshistorie zeigt, welches Ereignis einen Lauf ausgelöst hat. Verarbeitete Events werden nach 7 Tagen gelöscht (ihre `cj_event_run`-Links werden per FK-Cascade entfernt); Events ohne passenden Job werden verworfen, damit die Queue nicht wächst.

**Ereignis-Payload im Kindprozess.** Event-Läufe übergeben das Ereignis dem Job-Skript über die Prozessumgebung (das argv ist für die statischen, strikt validierten Argumente des Jobs reserviert):

| Variable | Gesetzt bei | Wert |
| :--- | :--- | :--- |
| `CRONJOB_EVENT` | jeder Event-Lauf | der Ereignisname (dient zugleich als Marker "dies ist ein Event-Lauf") |
| `CRONJOB_EVENT_PAYLOAD` | Event-Lauf **mit** nicht-leerem Payload | der Event-Payload als JSON (Route-Parameter, Log-Tabelle-Einträge, Webhook-`data`) |

```php
$event   = getenv('CRONJOB_EVENT');                       // false bei Zeitplan-/manuellen Läufen
$payload = json_decode((string) getenv('CRONJOB_EVENT_PAYLOAD'), true) ?: [];
$xref    = $payload['xref'] ?? '';                        // z.B. von einem _route:* Event
```

- **Variable nicht vorhanden = kein Payload.** Der Payload wird beim Queueen auf 4000 Bytes gekürzt; ein JSON, das mitten durchgeschnitten ist, dekodiert nicht mehr, in dem Fall ist `CRONJOB_EVENT_PAYLOAD` *gar nicht gesetzt* (nie leer, nie gebrochenes JSON).
- **Behandeln Sie den Payload als nicht vertrauenswürdige Eingabe** — vor allem für Webhook-`data`, dessen Absender extern ist. Validieren Sie Werte (XREFs, Namen, …) vor der Verwendung.
- Der Payload ist unabhängig vom Befehlstyp: Modul-[CLI-Skripte](../cli-scripts) *und* Core-Befehle (die die Variablen einfach ignorieren) funktionieren beide bei Event-Läufen.
- Der Payload ist auch in der Ausführungshistorie (Ereignis-Detail) sichtbar, unabhängig vom Kindprozess.

---

Section pages:

- [Pseudo-Events](/wt-modules/de/docs/cronjob/events/pseudo/): Log-Tabelle-Polling
- [Route-Events](/wt-modules/de/docs/cronjob/events/route/): request-getriggert
