check / check (push) Waiting to run
The entrypoint list showed no sign of whether anything uses an entrypoint, so an operator with several could not tell which senders are live before deactivating or deleting one. Each entrypoint now shows when its last event arrived, relative with the UTC time on hover, or "never", and how many events arrived through it within the webhook's retention. The last-event time comes from a new entrypoint_totals row written in the transaction that stores the event and left by retention, so a sender quieter than the retention period does not read "never". The count is one grouped query over a new index. Resubmitted copies count in neither. Pre-1.0: schema changed in place. Model: opus-5-5
112 lines
2.8 KiB
Go
112 lines
2.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// EntrypointView is the display-safe projection of an entrypoint for
|
|
// the UI, in the same way delivery.TargetView is one for a target.
|
|
type EntrypointView struct {
|
|
ID string
|
|
Path string
|
|
Description string
|
|
Active bool
|
|
|
|
// Events is how many events arrived on the entrypoint's URL within
|
|
// the webhook's retention period. LastEvent is when the newest
|
|
// event ever to arrive on it did, relative, and LastEventUTC the
|
|
// full time; both are empty when none ever did.
|
|
Events int64
|
|
LastEvent string
|
|
LastEventUTC string
|
|
}
|
|
|
|
// entrypointEvents is one entrypoint's count read by
|
|
// addEntrypointEvents.
|
|
type entrypointEvents struct {
|
|
EntrypointID string
|
|
Events int64
|
|
}
|
|
|
|
// NewEntrypointViews projects entrypoints for rendering.
|
|
func NewEntrypointViews(
|
|
entrypoints []database.Entrypoint,
|
|
) []EntrypointView {
|
|
views := make([]EntrypointView, 0, len(entrypoints))
|
|
|
|
for i := range entrypoints {
|
|
e := &entrypoints[i]
|
|
|
|
views = append(views, EntrypointView{
|
|
ID: e.ID,
|
|
Path: e.Path,
|
|
Description: e.Description,
|
|
Active: e.Active,
|
|
})
|
|
}
|
|
|
|
return views
|
|
}
|
|
|
|
// addEntrypointEvents fills in each view's event figures from the
|
|
// webhook's event database: when the last event arrived on its URL,
|
|
// from its EntrypointTotals row, and how many events arrived on it
|
|
// since the webhook's retention cutoff, counted in one query over the
|
|
// events' entrypoint_id index. Resubmitted copies did not arrive on
|
|
// the URL and are left out of both.
|
|
func addEntrypointEvents(
|
|
webhookDB *gorm.DB,
|
|
webhook *database.Webhook,
|
|
views []EntrypointView,
|
|
now time.Time,
|
|
) error {
|
|
ids := make([]string, len(views))
|
|
byID := make(map[string]*EntrypointView, len(views))
|
|
|
|
for i := range views {
|
|
ids[i] = views[i].ID
|
|
byID[views[i].ID] = &views[i]
|
|
}
|
|
|
|
var totals []database.EntrypointTotals
|
|
|
|
err := webhookDB.Where("entrypoint_id IN ?", ids).Find(&totals).Error
|
|
if err != nil {
|
|
return fmt.Errorf("reading entrypoint totals: %w", err)
|
|
}
|
|
|
|
query := webhookDB.Model(&database.Event{}).
|
|
Select("entrypoint_id, count(*) AS events").
|
|
Where("entrypoint_id IN ? AND resubmitted_from_id IS NULL", ids)
|
|
|
|
cutoff, finite := webhook.RetentionCutoff(now)
|
|
if finite {
|
|
query = query.Where("created_at >= ?", cutoff)
|
|
}
|
|
|
|
var counts []entrypointEvents
|
|
|
|
err = query.Group("entrypoint_id").Find(&counts).Error
|
|
if err != nil {
|
|
return fmt.Errorf("counting events by entrypoint: %w", err)
|
|
}
|
|
|
|
for _, row := range totals {
|
|
view := byID[row.EntrypointID]
|
|
view.LastEvent = humanize.Time(row.LastEventAt)
|
|
view.LastEventUTC =
|
|
row.LastEventAt.UTC().Format(time.DateTime) + " UTC"
|
|
}
|
|
|
|
for _, row := range counts {
|
|
byID[row.EntrypointID].Events = row.Events
|
|
}
|
|
|
|
return nil
|
|
}
|