Compare commits

1 Commits
Author SHA1 Message Date
clawbot 4b577ea733 Show each entrypoint's last event and event count on the webhook page (closes #393)
check / check (push) Successful in 3m18s
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:30:24 +00:00
+30 -29
View File
@@ -1892,18 +1892,19 @@ 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 counts, Each count changes in the transaction that writes or deletes the rows it
and each `last_event_at` in the transaction that stores the event. The pane's counts, and each `last_event_at` in the transaction that stores the event. The pane's lifetime events are `events`, and its lifetime
lifetime events are `events`, and its lifetime deliveries and failures are deliveries and failures are `deliveries` and `failed` summed over the
`deliveries` and `failed` summed over the targets; each figure within retention targets; each figure within retention is the same less what retention
is the same less what retention removed, so neither needs the rows themselves. removed, so neither needs the rows themselves. Its last event is
Its last event is `last_event_at` in `EventTotals`, so it still shows once `last_event_at` in `EventTotals`, so it still shows once retention has
retention has removed every event; each entrypoint's last event, from removed every event; each entrypoint's last event, from `EntrypointTotals`,
`EntrypointTotals`, does too. Its last-10-minutes and last-24-hours figures are does too. Its last-10-minutes and
counted from the `events` and `deliveries` indexes over just that window, the last-24-hours figures are counted from the `events` and `deliveries`
deliveries in one query grouped by target. Its failure percentage for a window indexes over just that window, the deliveries in one query grouped by
is the deliveries that became `failed` in it out of all that became `delivered` target. Its failure percentage for a window is the deliveries that became
or `failed` in it, and a dash when none did. `failed` in it out of all that became `delivered` or `failed` in it, and
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
@@ -1928,28 +1929,27 @@ 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 leaves GORM's soft delete adds `deleted_at IS NULL` to these queries; retention
it out. SQLite keeps no statistics on these tables, and without them it rates leaves it out. SQLite keeps no statistics on these tables, and without them it
the `deleted_at` index, which every live row matches, above an index on a column rates the `deleted_at` index, which every live row matches, above an index on
matched against several values or compared with a range. So every index but the a column matched against several values or compared with a range. So every
last also covers `deleted_at`. It comes second in the `event_id` and index but the last also covers `deleted_at`. It comes second in the `event_id`
`delivery_id` indexes, so that retention can use them without it. The event and `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 `deleted_at` comes first, a range (`>=`). In the statistics' `events` index
because they compare `created_at` with a range (`>=`) and SQLite narrows by a `deleted_at` comes first, because they compare `created_at` with a range (`>=`)
range only on the last column it uses. and SQLite narrows by a 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 bare `EntrypointTotals` includes these fields from `BaseModel`. `Setting` is a
key-value row with no `id`, no timestamps and no soft delete. Of the three bare key-value row with no `id`, no timestamps and no soft delete, and the
totals tables, `event_totals` holds counts and `last_event_at`, keyed by a three totals tables hold counts and `last_event_at`, keyed by a numeric
numeric `id`; `target_totals` holds counts, keyed by `target_id`; and `id`, by `target_id` and by `entrypoint_id`:
`entrypoint_totals` holds `last_event_at`, keyed by `entrypoint_id`:
| Field | Type | Description | | Field | Type | Description |
| ------------ | --------- | ----------- | | ------------ | --------- | ----------- |
@@ -3334,9 +3334,10 @@ 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 all of - GORM soft deletes on every entity that carries `BaseModel`, which is
them but `Setting`, `EventTotals`, `TargetTotals` and `EntrypointTotals` all of them but `Setting`, `EventTotals`, `TargetTotals` and
(data preserved for audit) `EntrypointTotals` (data
preserved for audit)
### Shutdown ### Shutdown