Add a statistics pane to the webhook page (closes #368) #403

Merged
clawbot merged 1 commits from issue-368-webhook-stats into next 2026-10-02 06:41:00 +02:00
Collaborator

Adds a statistics pane at the top of the webhook page: entrypoints and targets (and how many are active), deliveries in progress, the last event's time, the retention period; events, deliveries and failures, lifetime and within retention; and events, failures and failure percentage over the last 10 minutes and 24 hours.

Each webhook's event database now keeps running totals: one row for its events, with when the newest arrived, and one row per target for that target's deliveries, how many became delivered and how many failed, each with what retention removed. Storing an event, creating a delivery (fan-out or replay) and a delivery becoming delivered or failed each add to them in the same transaction as the row they count; a delivery already delivered or failed is not settled again. The webhook's delivery figures are the target rows summed; #372 can read the same rows for the target list.

Deliveries gain a finished_at column, set when a delivery becomes delivered or failed. It and target_id end the existing status index, so each target's deliveries finished in a window come from one index-range query grouped by target. A window's failure percentage is the failed deliveries out of all that finished in it, and a dash when none did.

Retention deletes at most 1000 expired events per transaction, with their delivery results, deliveries and totals update, pausing 200 ms between them so other writers get in, and stops between them when the app stops.

An existing database must be recreated: the schema changes in place and nothing is back-filled.

Model: opus-5-5

Adds a statistics pane at the top of the webhook page: entrypoints and targets (and how many are active), deliveries in progress, the last event's time, the retention period; events, deliveries and failures, lifetime and within retention; and events, failures and failure percentage over the last 10 minutes and 24 hours. Each webhook's event database now keeps running totals: one row for its events, with when the newest arrived, and one row per target for that target's deliveries, how many became delivered and how many failed, each with what retention removed. Storing an event, creating a delivery (fan-out or replay) and a delivery becoming delivered or failed each add to them in the same transaction as the row they count; a delivery already delivered or failed is not settled again. The webhook's delivery figures are the target rows summed; https://git.eeqj.de/sneak/webhooker/issues/372 can read the same rows for the target list. Deliveries gain a `finished_at` column, set when a delivery becomes delivered or failed. It and `target_id` end the existing status index, so each target's deliveries finished in a window come from one index-range query grouped by target. A window's failure percentage is the failed deliveries out of all that finished in it, and a dash when none did. Retention deletes at most 1000 expired events per transaction, with their delivery results, deliveries and totals update, pausing 200 ms between them so other writers get in, and stops between them when the app stops. An existing database must be recreated: the schema changes in place and nothing is back-filled. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 22:09:39 +02:00
clawbot self-assigned this 2026-10-01 22:09:39 +02:00
Author
Collaborator

Review: changes needed.

  1. The per-target counting required by #368 (comment) is not built. internal/database/model_totals.go:18: Totals is one row per webhook, with no target and no count of delivered deliveries, and the status index (internal/database/model_delivery.go:56) has no target column. As it stands, #372 would need a second mechanism, which that comment rules out. Acceptable: delivery totals kept per target, both delivered and failed (for example one totals row per target, with the webhook's figures summed from those rows), each still written in the same transaction as the rows it counts. Each target's deliveries that finished in a window should come from an index range, in one query grouped by target.

  2. The retention sweep's single transaction stalls the receiver. internal/database/retention.go:274 (reapExpired) deletes every expired row in one transaction. A prune of a few hundred thousand events holds the event database's write lock for longer than the 10-second busy timeout. Inbound webhooks that arrive meanwhile wait, then get a 500. A prune that size happens whenever retention is shortened on a busy webhook. Acceptable: delete in bounded batches, a fixed number of expired events per transaction. Each batch deletes those events' delivery results and deliveries and updates the totals in that same transaction, and the sweep commits between batches. Add a test that a prune larger than one batch removes every expired row and leaves the totals right.

  3. Nothing tests the retention period in the pane, which #368 (comment) requires. internal/handlers/webhook_stats_test.go:275 checks other text only. The existing "Retention: N days" assertions match the line at the foot of the page, so deleting the retention entry at templates/webhook_stats.html:28 leaves every test passing. Acceptable: a test that the rendered statistics pane itself shows the retention period, for a finite webhook and a forever one.

  4. At phone width the figure columns do not line up. templates/webhook_stats.html:42 and the other figure cells: each w-32 cell shrinks by a different amount depending on its row's label. At 375 px the numbers drift off-centre from the "Lifetime", "Within retention", "Last 10 minutes" and "Last 24 hours" headings and from each other. Acceptable: figure cells that keep their width (for example shrink-0), or a table, so each column stays aligned at any width.

  5. README.md:1766 says AutoMigrate creates the listed indexes "on a fresh and on an existing database". That is no longer true of the changed deliveries status index: on an existing database, AutoMigrate keeps the old two-column index of the same name. Acceptable: drop "and on an existing database". No upgrade handling is needed, per #368 (comment).

  • Judgement call: the retention period sits in the pane's top row, one line above the within-retention column. I take that as meeting "next to the within-retention figures".

Model: opus-5-5

Review: changes needed. 1. The per-target counting required by https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108111 is not built. `internal/database/model_totals.go:18`: `Totals` is one row per webhook, with no target and no count of delivered deliveries, and the status index (`internal/database/model_delivery.go:56`) has no target column. As it stands, https://git.eeqj.de/sneak/webhooker/issues/372 would need a second mechanism, which that comment rules out. Acceptable: delivery totals kept per target, both delivered and failed (for example one totals row per target, with the webhook's figures summed from those rows), each still written in the same transaction as the rows it counts. Each target's deliveries that finished in a window should come from an index range, in one query grouped by target. 2. The retention sweep's single transaction stalls the receiver. `internal/database/retention.go:274` (`reapExpired`) deletes every expired row in one transaction. A prune of a few hundred thousand events holds the event database's write lock for longer than the 10-second busy timeout. Inbound webhooks that arrive meanwhile wait, then get a 500. A prune that size happens whenever retention is shortened on a busy webhook. Acceptable: delete in bounded batches, a fixed number of expired events per transaction. Each batch deletes those events' delivery results and deliveries and updates the totals in that same transaction, and the sweep commits between batches. Add a test that a prune larger than one batch removes every expired row and leaves the totals right. 3. Nothing tests the retention period in the pane, which https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108272 requires. `internal/handlers/webhook_stats_test.go:275` checks other text only. The existing "Retention: N days" assertions match the line at the foot of the page, so deleting the retention entry at `templates/webhook_stats.html:28` leaves every test passing. Acceptable: a test that the rendered statistics pane itself shows the retention period, for a finite webhook and a forever one. 4. At phone width the figure columns do not line up. `templates/webhook_stats.html:42` and the other figure cells: each `w-32` cell shrinks by a different amount depending on its row's label. At 375 px the numbers drift off-centre from the "Lifetime", "Within retention", "Last 10 minutes" and "Last 24 hours" headings and from each other. Acceptable: figure cells that keep their width (for example `shrink-0`), or a table, so each column stays aligned at any width. 5. `README.md:1766` says `AutoMigrate` creates the listed indexes "on a fresh and on an existing database". That is no longer true of the changed `deliveries` status index: on an existing database, `AutoMigrate` keeps the old two-column index of the same name. Acceptable: drop "and on an existing database". No upgrade handling is needed, per https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108055. - Judgement call: the retention period sits in the pane's top row, one line above the within-retention column. I take that as meeting "next to the within-retention figures". Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 22:45:00 +02:00
clawbot force-pushed issue-368-webhook-stats from 7287e163e7 to ff0018cf43 2026-10-01 23:05:18 +02:00 Compare
clawbot force-pushed issue-368-webhook-stats from ff0018cf43 to 840c795c66 2026-10-01 23:28:28 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-01 23:47:35 +02:00
Author
Collaborator

Rework for the review above.

  1. Delivery totals are now one row per target (TargetTotals: deliveries, delivered, failed, and what retention removed of deliveries and failures), plus one row for the events (EventTotals), each written in the transaction of the rows it counts; the webhook's figures are the target rows summed. target_id now ends the status index, and each target's deliveries finished in a window come from one covering index-range query grouped by target, which the pane sums.
  2. Retention deletes at most 1000 expired events per transaction, each batch with their delivery results, deliveries and the totals update, committing between batches. A new test prunes one batch plus one and checks the rows left and every total.
  3. A new test takes the statistics pane out of the rendered page and checks it shows "30 days" for a finite webhook and "forever" for a forever one.
  4. The figures are two tables, so each column stays aligned at any width. shrink-0 is not in the committed stylesheet, and two 128 px columns do not fit at 375 px.
  5. The README now says AutoMigrate creates those indexes on a fresh database.
  • Judgement call: retention selects expired events without the soft-delete condition, through the created_at index, so each batch deletes exactly the events it selected; the README index table is updated to match.
  • Unverified: that a writer waiting on the busy timeout gets the write lock between batches, which run back to back with no pause.

Model: opus-5-5

Rework for the review above. 1. Delivery totals are now one row per target (`TargetTotals`: deliveries, delivered, failed, and what retention removed of deliveries and failures), plus one row for the events (`EventTotals`), each written in the transaction of the rows it counts; the webhook's figures are the target rows summed. `target_id` now ends the status index, and each target's deliveries finished in a window come from one covering index-range query grouped by target, which the pane sums. 2. Retention deletes at most 1000 expired events per transaction, each batch with their delivery results, deliveries and the totals update, committing between batches. A new test prunes one batch plus one and checks the rows left and every total. 3. A new test takes the statistics pane out of the rendered page and checks it shows "30 days" for a finite webhook and "forever" for a forever one. 4. The figures are two tables, so each column stays aligned at any width. `shrink-0` is not in the committed stylesheet, and two 128 px columns do not fit at 375 px. 5. The README now says `AutoMigrate` creates those indexes on a fresh database. - Judgement call: retention selects expired events without the soft-delete condition, through the `created_at` index, so each batch deletes exactly the events it selected; the README index table is updated to match. - Unverified: that a writer waiting on the busy timeout gets the write lock between batches, which run back to back with no pause. Model: opus-5-5
Author
Collaborator

Review: changes needed.

  1. Retention batches shut out other writers. internal/database/retention.go:279 (reapExpired) starts each batch the moment the last one commits. A receiver or delivery-worker write waiting on the busy timeout sleeps between retries (up to 100 ms at a time), and by the time it retries the next batch already holds the write lock. A prune longer than the 10-second busy timeout therefore still fails inbound webhooks with a 500 and fails delivery status writes, which is the stall finding 2 of #403 (comment) asked to remove. Acceptable: a short pause between batches, longer than the longest wait between retries (for example 150 to 200 ms), and a test that a write made while a prune of several batches is running succeeds.

  2. One terminal failure is counted against no target. internal/delivery/engine.go:521 loads a retrying delivery with only its id and status, and abandonRetryForMissingTarget (line 545) fails it before d.TargetID is set at line 566. writeDeliveryStatus (line 1583) then adds the failure to a target totals row whose target id is empty instead of the deleted target's row. The target's own totals miss that failure (its failures within retention can go below zero once retention removes the delivery), and the empty row keeps it for good. Acceptable: the delivery carries its target id before anything on that path can fail it (or the status write reads the target id from the stored row), with a test that the failure is added to the target's own row.

  3. The tests do not check most of the figures the pane shows. internal/handlers/webhook_stats_test.go:350 looks for only "Within retention", "50.0%" and "66.7%" in the rendered pane, and line 419 renders the pane for a webhook with no events without checking anything in it. A pane that shows the wrong entrypoint count, always 0 deliveries in progress, "none" for the last event, the lifetime and within-retention columns swapped, or the two windows' failures swapped still passes every test. The definition of done asks for tests that cover every figure. Acceptable: assertions on the rendered pane that tie each figure to its label and column, after the prune (where lifetime and within retention differ), and that the pane for a webhook with no finished deliveries shows a dash for both percentages.

  4. README.md:1791 says that in events, created_at is compared with less-than. That is no longer true of the events (deleted_at, created_at) index the sentence explains: its only user now is the statistics count, which compares created_at with greater-than-or-equal, and retention's less-than comparison uses the created_at index instead. Acceptable: describe it as a range comparison by the statistics, which SQLite narrows only on the last column it uses.

Model: opus-5-5

Review: changes needed. 1. Retention batches shut out other writers. `internal/database/retention.go:279` (`reapExpired`) starts each batch the moment the last one commits. A receiver or delivery-worker write waiting on the busy timeout sleeps between retries (up to 100 ms at a time), and by the time it retries the next batch already holds the write lock. A prune longer than the 10-second busy timeout therefore still fails inbound webhooks with a 500 and fails delivery status writes, which is the stall finding 2 of https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-109055 asked to remove. Acceptable: a short pause between batches, longer than the longest wait between retries (for example 150 to 200 ms), and a test that a write made while a prune of several batches is running succeeds. 2. One terminal failure is counted against no target. `internal/delivery/engine.go:521` loads a retrying delivery with only its id and status, and `abandonRetryForMissingTarget` (line 545) fails it before `d.TargetID` is set at line 566. `writeDeliveryStatus` (line 1583) then adds the failure to a target totals row whose target id is empty instead of the deleted target's row. The target's own totals miss that failure (its failures within retention can go below zero once retention removes the delivery), and the empty row keeps it for good. Acceptable: the delivery carries its target id before anything on that path can fail it (or the status write reads the target id from the stored row), with a test that the failure is added to the target's own row. 3. The tests do not check most of the figures the pane shows. `internal/handlers/webhook_stats_test.go:350` looks for only "Within retention", "50.0%" and "66.7%" in the rendered pane, and line 419 renders the pane for a webhook with no events without checking anything in it. A pane that shows the wrong entrypoint count, always 0 deliveries in progress, "none" for the last event, the lifetime and within-retention columns swapped, or the two windows' failures swapped still passes every test. The definition of done asks for tests that cover every figure. Acceptable: assertions on the rendered pane that tie each figure to its label and column, after the prune (where lifetime and within retention differ), and that the pane for a webhook with no finished deliveries shows a dash for both percentages. 4. `README.md:1791` says that in `events`, `created_at` is compared with less-than. That is no longer true of the `events` (`deleted_at`, `created_at`) index the sentence explains: its only user now is the statistics count, which compares `created_at` with greater-than-or-equal, and retention's less-than comparison uses the `created_at` index instead. Acceptable: describe it as a range comparison by the statistics, which SQLite narrows only on the last column it uses. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 00:18:59 +02:00
clawbot force-pushed issue-368-webhook-stats from 840c795c66 to 235a8e3024 2026-10-02 01:21:04 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 01:21:27 +02:00
Author
Collaborator

Rework for #403 (comment).

  1. Retention now pauses 200 ms after each batch before starting the next. A new test stores an event once the first of three batches is deleted and checks that it is stored while expired events are still left; with the pause removed, that test fails.
  2. A retrying delivery now takes its event and target IDs from the task as soon as it is loaded, before the deleted-target check can fail it. A new test checks that the failure lands on that target's own totals row and that no other totals row exists.
  3. The tests now read the rendered pane as text and, after the prune, check the top row and both tables label by label with each figure in its column. The pane for a webhook with no events is checked for "Last event none" and a dash for both percentages.
  4. The README now says the statistics compare created_at with a range, which SQLite narrows only on the last column it uses. The sentence before it now says "a range" instead of less-than, for the same reason.
  • Judgement call: the pause does not watch for shutdown. A stop during a long prune waits for it up to the stop timeout, as it did for the earlier single transaction.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-109500. 1. Retention now pauses 200 ms after each batch before starting the next. A new test stores an event once the first of three batches is deleted and checks that it is stored while expired events are still left; with the pause removed, that test fails. 2. A retrying delivery now takes its event and target IDs from the task as soon as it is loaded, before the deleted-target check can fail it. A new test checks that the failure lands on that target's own totals row and that no other totals row exists. 3. The tests now read the rendered pane as text and, after the prune, check the top row and both tables label by label with each figure in its column. The pane for a webhook with no events is checked for "Last event none" and a dash for both percentages. 4. The README now says the statistics compare `created_at` with a range, which SQLite narrows only on the last column it uses. The sentence before it now says "a range" instead of less-than, for the same reason. - Judgement call: the pause does not watch for shutdown. A stop during a long prune waits for it up to the stop timeout, as it did for the earlier single transaction. Model: opus-5-5
Author
Collaborator

Review: changes needed.

  1. Stopping the app during a long retention prune now uses up the whole stop budget. internal/database/retention.go:316 sleeps between batches without watching the sweep's context, and reapWebhook (line 229) does not pass the context down, so a stop that arrives mid-prune waits for every remaining batch. Once the prune has more than 5 seconds left, the reaper's stop hook times out (cmd/webhooker/main.go:50) and fx skips every stop hook after it: the HTTP drain, the delivery engine and the database closes. The disclosed judgement call does not hold: the old single transaction had no point to stop at, and the batches do. Acceptable: reapExpired takes the sweep's context and returns between batches once it is cancelled (waiting out the pause or the cancellation, whichever comes first), leaving the rest for the next sweep, with a test that stopping the reaper during a prune of several batches returns promptly with totals that match the rows left.

  2. A delivery settled twice is counted twice. writeDeliveryStatus (internal/delivery/engine.go:1595) updates the row by id alone and adds one to its target's delivered or failed total whenever a row matches, even when the delivery was already delivered or failed. That happens: reconcileDelivered (line 962) takes ownership and settles a delivery as delivered without re-reading its status, so a delivery that a worker settled and released after the sweep read it is counted again (failUnretryableRetry has the same gap). The lifetime and per-target totals then stay too high for good. Acceptable: the final-status write changes only a delivery that is not yet delivered or failed, and the total moves only when that write changed a row, with a test that settling an already finished delivery again leaves the totals unchanged.

  3. "Last event" says "none" once retention has removed every stored event. internal/handlers/webhook_stats.go:162 reads the newest event still stored, so a webhook whose last event is older than its retention period shows its lifetime events next to "Last event none" (templates/webhook_stats.html:25), which says no event ever arrived. Acceptable: keep the newest arrival time with the event totals, written in the transaction that stores the event, so retention cannot erase it, with a test that it survives a prune that removes every event.

  • Judgement call: finding 3 reads the plan's "when the last event arrived" (#368 (comment)) as the last arrival, not the newest event still stored.

Model: opus-5-5

Review: changes needed. 1. Stopping the app during a long retention prune now uses up the whole stop budget. `internal/database/retention.go:316` sleeps between batches without watching the sweep's context, and `reapWebhook` (line 229) does not pass the context down, so a stop that arrives mid-prune waits for every remaining batch. Once the prune has more than 5 seconds left, the reaper's stop hook times out (`cmd/webhooker/main.go:50`) and fx skips every stop hook after it: the HTTP drain, the delivery engine and the database closes. The disclosed judgement call does not hold: the old single transaction had no point to stop at, and the batches do. Acceptable: `reapExpired` takes the sweep's context and returns between batches once it is cancelled (waiting out the pause or the cancellation, whichever comes first), leaving the rest for the next sweep, with a test that stopping the reaper during a prune of several batches returns promptly with totals that match the rows left. 2. A delivery settled twice is counted twice. `writeDeliveryStatus` (`internal/delivery/engine.go:1595`) updates the row by id alone and adds one to its target's delivered or failed total whenever a row matches, even when the delivery was already delivered or failed. That happens: `reconcileDelivered` (line 962) takes ownership and settles a delivery as delivered without re-reading its status, so a delivery that a worker settled and released after the sweep read it is counted again (`failUnretryableRetry` has the same gap). The lifetime and per-target totals then stay too high for good. Acceptable: the final-status write changes only a delivery that is not yet delivered or failed, and the total moves only when that write changed a row, with a test that settling an already finished delivery again leaves the totals unchanged. 3. "Last event" says "none" once retention has removed every stored event. `internal/handlers/webhook_stats.go:162` reads the newest event still stored, so a webhook whose last event is older than its retention period shows its lifetime events next to "Last event none" (`templates/webhook_stats.html:25`), which says no event ever arrived. Acceptable: keep the newest arrival time with the event totals, written in the transaction that stores the event, so retention cannot erase it, with a test that it survives a prune that removes every event. - Judgement call: finding 3 reads the plan's "when the last event arrived" (https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108022) as the last arrival, not the newest event still stored. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 02:36:36 +02:00
clawbot force-pushed issue-368-webhook-stats from 235a8e3024 to 75a15eabae 2026-10-02 02:47:18 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 02:53:00 +02:00
Author
Collaborator

Rework for #403 (comment).

  1. The sweep's context now reaches retention's batches. Between two batches, retention returns as soon as the reaper is stopped instead of waiting out the pause, and leaves the remaining expired events for the next sweep. A new test stops the reaper during a prune of two batches and one more event. It checks that the stop returns inside the app's stop timeout with expired events still left, and that the event and target totals match the rows left.
  2. The final-status write now changes only a delivery that is not yet delivered or failed, and its target's total moves only when that write changed a row. A new test settles a delivered and a failed delivery again with both statuses, and checks that the stored status, the finish time and the totals stay as they were.
  3. The row of event totals now keeps when the newest event arrived, written in the transaction that stores the event. Retention leaves it as it is, and the pane reads it from there. A new test receives one event, lets retention remove it, and checks that the pane still shows its arrival time. The README's totals and index tables say the same.
  • Judgement call: each stored event overwrites the arrival time, so when two events are stored at almost the same moment, the earlier of the two times can be kept. The gap is at most the wait for the write lock.
  • event_totals gains a last_event_at column in place, so an existing database must be recreated, as the PR already says.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-110452. 1. The sweep's context now reaches retention's batches. Between two batches, retention returns as soon as the reaper is stopped instead of waiting out the pause, and leaves the remaining expired events for the next sweep. A new test stops the reaper during a prune of two batches and one more event. It checks that the stop returns inside the app's stop timeout with expired events still left, and that the event and target totals match the rows left. 2. The final-status write now changes only a delivery that is not yet delivered or failed, and its target's total moves only when that write changed a row. A new test settles a delivered and a failed delivery again with both statuses, and checks that the stored status, the finish time and the totals stay as they were. 3. The row of event totals now keeps when the newest event arrived, written in the transaction that stores the event. Retention leaves it as it is, and the pane reads it from there. A new test receives one event, lets retention remove it, and checks that the pane still shows its arrival time. The README's totals and index tables say the same. - Judgement call: each stored event overwrites the arrival time, so when two events are stored at almost the same moment, the earlier of the two times can be kept. The gap is at most the wait for the write lock. - `event_totals` gains a `last_event_at` column in place, so an existing database must be recreated, as the PR already says. Model: opus-5-5
Author
Collaborator

Review: changes needed.

  1. The pane tests still do not tie every figure to its label. In internal/handlers/webhook_stats_test.go:131 (seedStatsHistory), after the prune the Events and Failures rows hold the same numbers in both tables (3 and 2, then 1 and 2). Deliveries in progress is 1, the same as the last 10 minutes' events and failures. So a pane that shows the failures in the Events rows, or the last 10 minutes' events as deliveries in progress, passes every test (assertStatsPaneAfterPrune, line 283). Acceptable: a seeded history where, after the prune, no two figures in the top row or in the same table are equal, so showing any figure in another's place fails the test.

  2. Two README sentences are not true of the tree. README.md:1813 says the two totals tables "hold only counts", but event_totals also holds last_event_at. README.md:3101 says every entity but Setting carries BaseModel and so is soft-deleted, but EventTotals and TargetTotals now do not. Acceptable: both sentences corrected to match the tree.

  3. At desktop width the left table's rows are spread out. In templates/webhook_stats.html:33 the Lifetime / Within retention table sits directly in the two-column grid, so it stretches to the height of the right column (its table plus the note under it). Its rows end up further apart than the right table's and out of line with them. Acceptable: both tables keep their natural row height side by side, for example by wrapping the left table in a div like the right one, or items-start on the grid.

  4. The commit message body is about 130 words, over the limit of about 120. Acceptable: trimmed below it.

  5. internal/database/event_tier_indexes_test.go:219 splits queryPlan out of assertPlanUses, its only caller. The change does not need that existing helper reshaped. Acceptable: assertPlanUses left as it was.

  • Judgement call: the race on the last event's arrival time disclosed in the last rework is not a finding. Every transaction takes the write lock when it begins and the event's time is set after that, so a later commit cannot carry an earlier time.

Model: opus-5-5

Review: changes needed. 1. The pane tests still do not tie every figure to its label. In `internal/handlers/webhook_stats_test.go:131` (`seedStatsHistory`), after the prune the Events and Failures rows hold the same numbers in both tables (3 and 2, then 1 and 2). Deliveries in progress is 1, the same as the last 10 minutes' events and failures. So a pane that shows the failures in the Events rows, or the last 10 minutes' events as deliveries in progress, passes every test (`assertStatsPaneAfterPrune`, line 283). Acceptable: a seeded history where, after the prune, no two figures in the top row or in the same table are equal, so showing any figure in another's place fails the test. 2. Two README sentences are not true of the tree. `README.md:1813` says the two totals tables "hold only counts", but `event_totals` also holds `last_event_at`. `README.md:3101` says every entity but `Setting` carries `BaseModel` and so is soft-deleted, but `EventTotals` and `TargetTotals` now do not. Acceptable: both sentences corrected to match the tree. 3. At desktop width the left table's rows are spread out. In `templates/webhook_stats.html:33` the Lifetime / Within retention table sits directly in the two-column grid, so it stretches to the height of the right column (its table plus the note under it). Its rows end up further apart than the right table's and out of line with them. Acceptable: both tables keep their natural row height side by side, for example by wrapping the left table in a `div` like the right one, or `items-start` on the grid. 4. The commit message body is about 130 words, over the limit of about 120. Acceptable: trimmed below it. 5. `internal/database/event_tier_indexes_test.go:219` splits `queryPlan` out of `assertPlanUses`, its only caller. The change does not need that existing helper reshaped. Acceptable: `assertPlanUses` left as it was. - Judgement call: the race on the last event's arrival time disclosed in the last rework is not a finding. Every transaction takes the write lock when it begins and the event's time is set after that, so a later commit cannot carry an earlier time. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 03:21:08 +02:00
clawbot force-pushed issue-368-webhook-stats from 75a15eabae to 926d34b4b8 2026-10-02 03:34:14 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 03:34:19 +02:00
Author
Collaborator

Rework for #403 (comment).

  1. The seeded history now has six events (one past retention, one six hours old, four just in) and five entrypoints, four of them active. After the prune the top row reads 5 (4 active), 3 (2 active) and 7 deliveries in progress; the first table 6/5, 13/10 and 3/2; the second 4/5, 1/2 and 50.0%/66.7%. No two figures in the top row or in the same table are equal.
  2. The common-fields sentence now says the totals tables hold counts plus last_event_at in event_totals. The soft-delete line now names EventTotals and TargetTotals with Setting as the entities without BaseModel.
  3. The left table is wrapped in a div like the right one, so each keeps its natural row height side by side.
  4. Left for the squash, as directed.
  5. assertPlanUses is back to how it is on next.
  • The branch is rebased onto next, which now has #404.
  • Judgement call: I counted the retention period ("1 day") as a top-row figure, so no other top-row figure is 1.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-110717. 1. The seeded history now has six events (one past retention, one six hours old, four just in) and five entrypoints, four of them active. After the prune the top row reads 5 (4 active), 3 (2 active) and 7 deliveries in progress; the first table 6/5, 13/10 and 3/2; the second 4/5, 1/2 and 50.0%/66.7%. No two figures in the top row or in the same table are equal. 2. The common-fields sentence now says the totals tables hold counts plus `last_event_at` in `event_totals`. The soft-delete line now names `EventTotals` and `TargetTotals` with `Setting` as the entities without `BaseModel`. 3. The left table is wrapped in a `div` like the right one, so each keeps its natural row height side by side. 4. Left for the squash, as directed. 5. `assertPlanUses` is back to how it is on `next`. - The branch is rebased onto `next`, which now has https://git.eeqj.de/sneak/webhooker/pulls/404. - Judgement call: I counted the retention period ("1 day") as a top-row figure, so no other top-row figure is 1. Model: opus-5-5
Author
Collaborator

Review: changes needed.

  1. templates/webhook_stats.html:25 prints the last event's arrival time in the server's local time zone but labels it UTC. On a host whose time zone is not UTC (a container with TZ set or /etc/localtime mounted, or a local run), "Last event" is off by the zone's offset and disagrees with the UTC times the event list lower on the same page shows, which converts first (internal/handlers/recent_events.go:197). The tests build their expected text the same unconverted way, so they cannot catch it. Acceptable: convert the time to UTC before formatting it, as the event list does, and build the tests' expected text from the UTC time.

  2. The pane test still cannot tell the "Within retention" column from the "Last 24 hours" column. In the history seedStatsHistory builds (internal/handlers/webhook_stats_test.go:133), after the prune both columns show 5 events and 2 failures, so a pane that shows the within-retention events or failures under "Last 24 hours" still passes assertStatsPaneAfterPrune. Acceptable: a seeded history where those figures differ too, so every figure in the two tables is distinct.

Model: opus-5-5

Review: changes needed. 1. `templates/webhook_stats.html:25` prints the last event's arrival time in the server's local time zone but labels it UTC. On a host whose time zone is not UTC (a container with `TZ` set or `/etc/localtime` mounted, or a local run), "Last event" is off by the zone's offset and disagrees with the UTC times the event list lower on the same page shows, which converts first (`internal/handlers/recent_events.go:197`). The tests build their expected text the same unconverted way, so they cannot catch it. Acceptable: convert the time to UTC before formatting it, as the event list does, and build the tests' expected text from the UTC time. 2. The pane test still cannot tell the "Within retention" column from the "Last 24 hours" column. In the history `seedStatsHistory` builds (`internal/handlers/webhook_stats_test.go:133`), after the prune both columns show 5 events and 2 failures, so a pane that shows the within-retention events or failures under "Last 24 hours" still passes `assertStatsPaneAfterPrune`. Acceptable: a seeded history where those figures differ too, so every figure in the two tables is distinct. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 04:07:44 +02:00
clawbot force-pushed issue-368-webhook-stats from 926d34b4b8 to 1d37f0ca09 2026-10-02 04:23:53 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 04:24:01 +02:00
Author
Collaborator

Rework for #403 (comment).

  1. The pane now converts when the last event arrived to UTC before formatting it, as the event list does, and the tests build their expected text from the UTC time. A new test stores that time in a zone five hours behind UTC, as a host in that zone would, and checks that the pane shows it in UTC, so dropping the conversion fails it. That time is the only one the pane shows.
  2. The seeded history now has 14 days of retention, eleven entrypoints (ten active), five targets (two active) and nine events: one past retention, one 30 hours old (both of its deliveries failed 29 hours ago), one six hours old and six just in. After the prune the top row reads 11 (10 active), 5 (2 active), 12 deliveries in progress and 14 days; the first table 9/8, 19/16 and 4/3; the second 6/7, 0/1 and 0.0%/50.0%. No figure on the pane equals any other.
  • Deviation: the new test stores the time in another zone instead of setting time.Local, which is shared by the whole test process and would race with the package's parallel tests.
  • Judgement call: the retention period counts as a figure, as before.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-110933. 1. The pane now converts when the last event arrived to UTC before formatting it, as the event list does, and the tests build their expected text from the UTC time. A new test stores that time in a zone five hours behind UTC, as a host in that zone would, and checks that the pane shows it in UTC, so dropping the conversion fails it. That time is the only one the pane shows. 2. The seeded history now has 14 days of retention, eleven entrypoints (ten active), five targets (two active) and nine events: one past retention, one 30 hours old (both of its deliveries failed 29 hours ago), one six hours old and six just in. After the prune the top row reads 11 (10 active), 5 (2 active), 12 deliveries in progress and 14 days; the first table 9/8, 19/16 and 4/3; the second 6/7, 0/1 and 0.0%/50.0%. No figure on the pane equals any other. - Deviation: the new test stores the time in another zone instead of setting `time.Local`, which is shared by the whole test process and would race with the package's parallel tests. - Judgement call: the retention period counts as a figure, as before. Model: opus-5-5
Author
Collaborator

Review: changes needed.

  1. The tests cannot catch a "Deliveries in progress" figure that leaves out retrying deliveries. In the history seedStatsHistory builds (internal/handlers/webhook_stats_test.go:133), every delivery not yet finished is pending. None is retrying, and no other test counts a retrying delivery as in progress. So a pane that counts only pending deliveries (internal/handlers/webhook_stats.go:147) passes every test, though retrying is where deliveries wait while a target is down. Acceptable: the seeded history includes at least one retrying delivery, the expected in-progress figure counts it, and every figure on the pane stays distinct from every other.

Model: opus-5-5

Review: changes needed. 1. The tests cannot catch a "Deliveries in progress" figure that leaves out retrying deliveries. In the history `seedStatsHistory` builds (`internal/handlers/webhook_stats_test.go:133`), every delivery not yet finished is pending. None is retrying, and no other test counts a retrying delivery as in progress. So a pane that counts only pending deliveries (`internal/handlers/webhook_stats.go:147`) passes every test, though retrying is where deliveries wait while a target is down. Acceptable: the seeded history includes at least one retrying delivery, the expected in-progress figure counts it, and every figure on the pane stays distinct from every other. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-02 05:00:41 +02:00
clawbot added 1 commit 2026-10-02 05:26:31 +02:00
Each webhook's event database keeps running totals: one row for its
events, with when the newest arrived, and one row per target for its
deliveries, delivered and failed, each with what retention removed.
Every write to them shares the transaction of the rows it counts, and
a delivery already delivered or failed is not settled again.
Deliveries get a finished_at column; it and target_id end the status
index, so each target's deliveries finished in a window come from one
index-range query grouped by target. Retention deletes 1000 expired
events per transaction, pausing 200 ms between them so other writers
get in, and stops between them on shutdown. The pane is its own
template, its figures in tables.

The schema changes in place with nothing back-filled, so an existing
database must be recreated.

Model: opus-5-5
clawbot force-pushed issue-368-webhook-stats from 1d37f0ca09 to 88c9a000e4 2026-10-02 05:26:31 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-02 05:26:42 +02:00
Author
Collaborator

Rework for #403 (comment).

  1. The newest event's delivery to the second target is now retrying, and the expected deliveries in progress count it (15 before the prune, 13 after). Checking the other figures the same way: no delivery had failed in the last 10 minutes, so that column's failures and percentage counted nothing. One delivery of the six-hour-old event now fails a minute ago, and one of the 30-hour-old event 23 hours ago, so each window also counts a delivery that finished in it though its event arrived earlier. To keep every figure distinct, the history now has ten events, twelve entrypoints (eleven active) and six targets (two active). After the prune the pane reads 12 (11 active), 6 (2 active), 13 in progress and 14 days; 10/9, 21/18 and 5/4; 7/8, 1/3 and 50.0%/75.0%.
  • Deviation: two literals in seedStatsHistory now take fewer lines, to keep the function within the linter's 80-line limit.
  • Judgement call: a delivery that finished inside a window for an event that arrived before it is a case each window's figures must count.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/webhooker/pulls/403#issuecomment-111181. 1. The newest event's delivery to the second target is now retrying, and the expected deliveries in progress count it (15 before the prune, 13 after). Checking the other figures the same way: no delivery had failed in the last 10 minutes, so that column's failures and percentage counted nothing. One delivery of the six-hour-old event now fails a minute ago, and one of the 30-hour-old event 23 hours ago, so each window also counts a delivery that finished in it though its event arrived earlier. To keep every figure distinct, the history now has ten events, twelve entrypoints (eleven active) and six targets (two active). After the prune the pane reads 12 (11 active), 6 (2 active), 13 in progress and 14 days; 10/9, 21/18 and 5/4; 7/8, 1/3 and 50.0%/75.0%. - Deviation: two literals in `seedStatsHistory` now take fewer lines, to keep the function within the linter's 80-line limit. - Judgement call: a delivery that finished inside a window for an event that arrived before it is a case each window's figures must count. Model: opus-5-5
Author
Collaborator

Review passed.

Model: opus-5-5

Review passed. Model: opus-5-5
Author
Collaborator

Re-gate passed on current next.

Model: opus-5-5

Re-gate passed on current next. Model: opus-5-5
clawbot merged commit 803a94be37 into next 2026-10-02 06:41:00 +02:00
clawbot deleted branch issue-368-webhook-stats 2026-10-02 06:41:01 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#403