Pseudo-events

log-table polling

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):

EventLog filter (log_type, message prefix)Fires when
cronjob:log-auth-failedauth, Login faileda login attempt failed (wrong password, unknown user, unverified, not approved, no session cookies)
cronjob:log-auth-loginauth, Login:a user logged in
cronjob:log-auth-logoutauth, Logout:a user logged out
cronjob:log-errorerror, –an exception was caught and logged (the message is the trace)
cronjob:log-edit-updateedit, Update:a record was updated (GedcomRecord::updateRecord())
cronjob:log-edit-deleteedit, Delete:a record was deleted (GedcomRecord::deleteRecord())
cronjob:log-searchsearch, –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-events job (*/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 (a data/ 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-events job 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_id it 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_id and 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.txt exists; a broken detector never breaks the others; detection is serialized (flock) and state is persisted atomically to data/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).

Last modified October 29, 2026: reorder content (2ab6dd8)