Files
webhooker/internal/handlers/entrypoint_view.go
T
clawbot 8c5ab00524
check / check (push) Successful in 3m23s
Show each entrypoint's last event and event count on the webhook page (closes #393)
Each entrypoint in the webhook page's entrypoint list now shows when its last event arrived (relative, with the full UTC time on hover), or "never", and how many events arrived through it within the webhook's retention period. Both come from one query per page, grouped by entrypoint, over a new events index on entrypoint_id, deleted_at and created_at, so the page reads only the index entries it counts. Pre-1.0: the index goes into the schema in place. A test checks the database's plan for the statement as the code builds it; another shows two entrypoints with different traffic each with their own figures, an event older than retention left out, and an unused entrypoint reading "never".

Model: opus-5-5
2026-10-02 19:56:29 +00:00

111 lines
2.7 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 through the entrypoint within
// the webhook's retention period. LastEvent is when the newest of
// them arrived, relative, and LastEventUTC the full time; both are
// empty when none did.
Events int64
LastEvent string
LastEventUTC string
}
// entrypointEvents is one entrypoint's row read by addEntrypointEvents.
type entrypointEvents struct {
EntrypointID string
Events int64
LastEventAt time.Time
}
// 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, in one query over the events'
// entrypoint_id index. Only events received since the webhook's
// retention cutoff are read.
func addEntrypointEvents(
webhookDB *gorm.DB,
webhook *database.Webhook,
views []EntrypointView,
now time.Time,
) error {
ids := make([]string, len(views))
for i := range views {
ids[i] = views[i].ID
}
// max(created_at) picks each entrypoint's newest row, and SQLite
// takes the bare created_at beside it from that row. Unlike max(),
// the plain column keeps its datetime type, so the driver returns
// it as a time.
query := webhookDB.Model(&database.Event{}).
Select("entrypoint_id, count(*) AS events, "+
"max(created_at), created_at AS last_event_at").
Where("entrypoint_id IN ?", ids)
cutoff, finite := webhook.RetentionCutoff(now)
if finite {
query = query.Where("created_at >= ?", cutoff)
}
var rows []entrypointEvents
err := query.Group("entrypoint_id").Find(&rows).Error
if err != nil {
return fmt.Errorf("counting events by entrypoint: %w", err)
}
byID := make(map[string]entrypointEvents, len(rows))
for _, row := range rows {
byID[row.EntrypointID] = row
}
for i := range views {
row, found := byID[views[i].ID]
if !found {
continue
}
views[i].Events = row.Events
views[i].LastEvent = humanize.Time(row.LastEventAt)
views[i].LastEventUTC =
row.LastEventAt.UTC().Format(time.DateTime) + " UTC"
}
return nil
}