Compare commits

1 Commits
Author SHA1 Message Date
clawbot 1adb625a7a Show each entrypoint's last event and event count on the webhook page (closes #393)
check / check (push) Successful in 3m8s
Each entrypoint in the webhook page's entrypoint list shows when the last event arrived on its URL (relative, with the full UTC time on hover), or "never" if none ever has, and how many events arrived on it within the webhook's retention period. The last event is kept per entrypoint in a new EntrypointTotals row in the event database, written in the transaction that stores the event, so retention leaves it in place. The count is one query per page, grouped by entrypoint, over a new events index on entrypoint_id, deleted_at, resubmitted_from_id and created_at. Resubmitted copies did not arrive on the URL and count in neither. Pre-1.0: the table and index go into the schema in place.

Model: opus-5-5
2026-10-02 20:52:16 +00:00
+29 -30
View File
@@ -1892,19 +1892,18 @@ that arrives on its URL:
| `entrypoint_id` | UUID | The entrypoint (primary key) | | `entrypoint_id` | UUID | The entrypoint (primary key) |
| `last_event_at` | timestamp | When the newest event arrived on its URL; a resubmitted event leaves it as it is, and so does retention | | `last_event_at` | timestamp | When the newest event arrived on its URL; a resubmitted event leaves it as it is, and so does retention |
Each count changes in the transaction that writes or deletes the rows it Each count changes in the transaction that writes or deletes the rows it counts,
counts, and each `last_event_at` in the transaction that stores the event. The pane's lifetime events are `events`, and its lifetime and each `last_event_at` in the transaction that stores the event. The pane's
deliveries and failures are `deliveries` and `failed` summed over the lifetime events are `events`, and its lifetime deliveries and failures are
targets; each figure within retention is the same less what retention `deliveries` and `failed` summed over the targets; each figure within retention
removed, so neither needs the rows themselves. Its last event is is the same less what retention removed, so neither needs the rows themselves.
`last_event_at` in `EventTotals`, so it still shows once retention has Its last event is `last_event_at` in `EventTotals`, so it still shows once
removed every event; each entrypoint's last event, from `EntrypointTotals`, retention has removed every event; each entrypoint's last event, from
does too. Its last-10-minutes and `EntrypointTotals`, does too. Its last-10-minutes and last-24-hours figures are
last-24-hours figures are counted from the `events` and `deliveries` counted from the `events` and `deliveries` indexes over just that window, the
indexes over just that window, the deliveries in one query grouped by deliveries in one query grouped by target. Its failure percentage for a window
target. Its failure percentage for a window is the deliveries that became is the deliveries that became `failed` in it out of all that became `delivered`
`failed` in it out of all that became `delivered` or `failed` in it, and or `failed` in it, and a dash when none did.
a dash when none did.
The webhook list at `/hooks` shows three of the pane's figures for each The webhook list at `/hooks` shows three of the pane's figures for each
webhook: its events within retention and its last event, both from webhook: its events within retention and its last event, both from
@@ -1929,27 +1928,28 @@ tags, so `AutoMigrate` creates them on a fresh database:
| `events` | `entrypoint_id`, `deleted_at`, `resubmitted_from_id`, `created_at` | The webhook page's entrypoint list, which counts the events that arrived on each entrypoint's URL within the retention period | | `events` | `entrypoint_id`, `deleted_at`, `resubmitted_from_id`, `created_at` | The webhook page's entrypoint list, which counts the events that arrived on each entrypoint's URL within the retention period |
| `events` | `created_at` | Retention, which selects expired events by age | | `events` | `created_at` | Retention, which selects expired events by age |
GORM's soft delete adds `deleted_at IS NULL` to these queries; retention GORM's soft delete adds `deleted_at IS NULL` to these queries; retention leaves
leaves it out. SQLite keeps no statistics on these tables, and without them it it out. SQLite keeps no statistics on these tables, and without them it rates
rates the `deleted_at` index, which every live row matches, above an index on the `deleted_at` index, which every live row matches, above an index on a column
a column matched against several values or compared with a range. So every matched against several values or compared with a range. So every index but the
index but the last also covers `deleted_at`. It comes second in the `event_id` last also covers `deleted_at`. It comes second in the `event_id` and
and `delivery_id` indexes, so that retention can use them without it. The event `delivery_id` indexes, so that retention can use them without it. The event
log's count, the one query on the `resubmitted_from_id` index, always carries log's count, the one query on the `resubmitted_from_id` index, always carries
`deleted_at IS NULL` and uses both columns. The entrypoint list's count, the one `deleted_at IS NULL` and uses both columns. The entrypoint list's count, the one
query on the `entrypoint_id` index, uses all four, `resubmitted_from_id IS NULL` query on the `entrypoint_id` index, uses all four, `resubmitted_from_id IS NULL`
leaving out resubmitted copies and `created_at` last because it compares it with leaving out resubmitted copies and `created_at` last because it compares it with
a range (`>=`). In the statistics' `events` index a range (`>=`). In the statistics' `events` index `deleted_at` comes first,
`deleted_at` comes first, because they compare `created_at` with a range (`>=`) because they compare `created_at` with a range (`>=`) and SQLite narrows by a
and SQLite narrows by a range only on the last column it uses. range only on the last column it uses.
#### Common Fields #### Common Fields
Every entity except `Setting`, `EventTotals`, `TargetTotals` and Every entity except `Setting`, `EventTotals`, `TargetTotals` and
`EntrypointTotals` includes these fields from `BaseModel`. `Setting` is a `EntrypointTotals` includes these fields from `BaseModel`. `Setting` is a bare
bare key-value row with no `id`, no timestamps and no soft delete, and the key-value row with no `id`, no timestamps and no soft delete. Of the three
three totals tables hold counts and `last_event_at`, keyed by a numeric totals tables, `event_totals` holds counts and `last_event_at`, keyed by a
`id`, by `target_id` and by `entrypoint_id`: numeric `id`; `target_totals` holds counts, keyed by `target_id`; and
`entrypoint_totals` holds `last_event_at`, keyed by `entrypoint_id`:
| Field | Type | Description | | Field | Type | Description |
| ------------ | --------- | ----------- | | ------------ | --------- | ----------- |
@@ -3334,10 +3334,9 @@ check, see [The login endpoint](#the-login-endpoint).
`ENTRYPOINT` script, which sets the data directory's owner and mode `ENTRYPOINT` script, which sets the data directory's owner and mode
before the app starts; the image's health check; and `docker exec`, before the app starts; the image's health check; and `docker exec`,
unless given `--user` unless given `--user`
- GORM soft deletes on every entity that carries `BaseModel`, which is - GORM soft deletes on every entity that carries `BaseModel`, which is all of
all of them but `Setting`, `EventTotals`, `TargetTotals` and them but `Setting`, `EventTotals`, `TargetTotals` and `EntrypointTotals`
`EntrypointTotals` (data (data preserved for audit)
preserved for audit)
### Shutdown ### Shutdown