Route events
A complementary, immediate detection mechanism: instead of polling state on a schedule, the module observes the HTTP request itself. When a request to a mapped mutating route has been handled successfully (2xx/3xx), it queues the matching event into the same cj_event queue — a listening event job then reacts at the next tick (~60 s), with no polling latency.
Which routes are mapped — two rules, both derived from the live webtrees route table. Common prerequisites: path starts with /tree/{tree}/, the handler is in webtrees’ request-handler namespace (RequestHandlers in 2.2.6, Controllers in 2.3), and the request is a POST mutation. In 2.2.6 this is enforced at map time (GET page routes are not mapped); in 2.3 the route carries no HTTP method, so the POST check happens at fire-time and the GET form-load of the same route is ignored.
- Record routes (rule-based — core updates are picked up automatically): the path contains
{xref}(the target record) and the handler class name starts withEdit,Delete,Add,Create,Link,Paste,ReorderorPending. The event name is the third path segment, namespaced_route:<segment>; when several mapped routes share a segment (delete,edit-raw,accept,reject), the following path parameters are appended, hyphen-separated. - Tree-level routes (small curated allowlist): the significant tree-wide mutations that carry no
{xref}in the URL (import, merge, renumber, …). Each allowlist entry has an explicit event name. New tree-wide mutations in a core update need a module update (the record-route rule stays fully automatic).
| Event | Route (under /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 — the media object; the target record is a form field, not in the 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 — the chunked import continuation fires once per 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 (bulk) | tree |
_route:change-family-members | POST change-family-members | tree — the family xref is a form field |
In total 38 events (27 record routes + 11 tree-level routes). The payload always contains tree (id) + tree_name plus all matched route parameters. Record routes (with {xref}) additionally carry change_pending (bool): whether the change is still pending in the change table after this request (the user’s auto-accept preference is off) or already applied (auto-accept on). A pending change is applied when an admin accepts it — which fires its own _route:accept* events. Tree-level routes write directly (no change rows) and carry no such flag.
Breaking change (this version). Record routes without
{xref}in the URL — all_route:create-*events plus_route:add-unlinked-individual— are no longer mapped: they carry no target record. Jobs listening to those events will never fire again; the admin table flags such triggers with an unknown route event badge (a generic check over all jobs’_route:*triggers against the live map), so re-pointing them is visible instead of silent.
_route:accept*/_route:reject*fire when a pending change is accepted or rejected — accepting a pending deletion is when the record is actually removed.
How it works
- Gated twice, off by default. An event fires only while the admin toggle “Route events” (Events section) is on and at least one enabled event-triggered job listens for that event name. A request that is not mapped costs nothing (one in-memory lookup); only a successful request on a mapped route does two tiny indexed DB reads.
- Transactional. The
cj_eventinsert runs in the same DB transaction as the mutation (the module middleware sits inside webtrees’ request transaction), so the queued event commits or rolls back together with the change — no drift, no double fire on a failed save. - Never blocks. Detection runs after the handler, is exception-wrapped, and can never alter the response.
- Robust to core upgrades. The map is rebuilt from the route table (keyed by handler class strings, no
::class), so a renamed/removed handler simply stops matching instead of fatalling. If the route table is not available (e.g. CLI), a static fallback map with the note route keeps the pilot working. The record-route rule is fully automatic; only the small tree-level allowlist is curated (see above), and jobs pointing at an event that no longer exists — a removed route event or a removed built-in pseudo-event — are flagged in the admin table (unknown event badge).
To act on one, create an event-triggered job whose Event name matches (e.g. _route:update-record) and enable the “Route events” toggle. Note the overlap with the cronjob:log-edit-update / cronjob:log-edit-delete pollers: every record edit/delete is also written to the log table, so the matching log event follows at the next poll as well — pick the mechanism whose latency/precision fits the job (route events are near-instant and precise per object, and carry change_pending; log pollers lag ~5 min but also catch changes made outside the web UI).