Pseudo-events
webtrees has no event bus, so core actions (logins, failed logins, logouts, record edits, errors, searches) are detected by polling the webtrees log table (app/Log.php, table log) and firing an event per new matching log entry. It is off by default: nothing runs until you enable the offered cronjob:pseudo-events job.
Built-in events (one cj_event per new log row, payload = the row):
| Event | Log filter (log_type, message prefix) | Fires when |
|---|---|---|
cronjob:log-auth-failed | auth, Login failed | a login attempt failed (wrong password, unknown user, unverified, not approved, no session cookies) |
cronjob:log-auth-login | auth, Login: | a user logged in |
cronjob:log-auth-logout | auth, Logout: | a user logged out |
cronjob:log-error | error, – | an exception was caught and logged (the message is the trace) |
cronjob:log-edit-update | edit, Update: | a record was updated (GedcomRecord::updateRecord()) |
cronjob:log-edit-delete | edit, Delete: | a record was deleted (GedcomRecord::deleteRecord()) |
cronjob:log-search | search, – | a search ran (one log row per searched tree) |
Payload: log_id, log_time, log_message (truncated to 250 characters, UTF-8-safe) plus gedcom_id / user_id when the log row carries them. The message prefixes are the hard-coded English strings of the current core; a core update that changes them silently stops the matching event (no fallback by design) — the event catalog descriptions and the test-pseudo-events suite pin the current mapping.
How it runs
- One trigger, one cooldown. Detection runs only from the offered
cronjob:pseudo-eventsjob (*/5 * * * *by default), in the tick’s child process — isolated from any web request and bounded by the job’s timeout, so a heavy detector can never block or slow down a page load. A 5-minute cooldown (adata/mtime marker) plus a lock keep repeated runs cheap and idempotent; the cooldown also caps a more frequent job cron. - Enabled by the job, gated per event. Enabling the
cronjob:pseudo-eventsjob turns detection on (there is no separate switch — the tick only runs enabled jobs). A detector whose event no enabled event-triggered job listens for is skipped entirely — no query, no push. - Checkpoint + backpressure. Each detector remembers the highest
log_idit processed (state file). At most 200 rows are fired per run per detector; the checkpoint advances only as far as the last fired row, so a flood is drained over several runs — no data loss, only latency. - Baseline on first run. The first detection only records the current max
log_idand fires nothing, so enabling the job does not retroactively fire on existing data. (Re-listening to a previously unlistened event catches up its backlog, capped as above.) - Reset, not re-fire, on table shrink. If the table shrank below a checkpoint (TRUNCATE / DB restore — plain row deletions cannot do this, MySQL auto-increment continues after deletes), the detector re-baselines silently.
- Offline-safe & isolated. Skipped while
data/offline.txtexists; a broken detector never breaks the others; detection is serialized (flock) and state is persisted atomically todata/cronjob/pseudo-events.json.
The log table is the source of truth: under a flood the tick’s coalescing runs a job once per event name per tick with the newest payload — older rows are not re-delivered, but they stay in the log table, and log_id in the payload lets a job that needs completeness re-read the range itself.
If an event name vanishes, the admin table flags such triggers with the unknown event badge.
Detected events go into the same cj_event queue as webhooks and are consumed by the tick’s normal event drain. To act on one, create an event-triggered job whose Event name matches (e.g. alert on cronjob:log-auth-failed, re-index on cronjob:log-edit-update).