package handlers import ( "fmt" "net/http" "time" "sneak.berlin/go/webhooker/internal/database" ) // WebhookListItem holds data for the webhook list view. type WebhookListItem struct { database.Webhook EntrypointCount int InactiveEntrypointCount int TargetCount int InactiveTargetCount int // EventCount is how many events the webhook holds, LastEventAt // when the newest arrived (nil before the first), and // FailedLast24Hours how many of its deliveries failed in the last // 24 hours. When the webhook's event database could not be read, // EventsUnreadable is set and these three are not known. EventCount int64 LastEventAt *time.Time FailedLast24Hours int64 EventsUnreadable bool } // HandleSourceList shows a list of user's webhooks. func (h *Handlers) HandleSourceList() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserID(r) if !ok { http.Redirect( w, r, "/pages/login", http.StatusSeeOther, ) return } var webhooks []database.Webhook err := h.db.DB().Where( "user_id = ?", userID, ).Order("created_at DESC").Find(&webhooks).Error if err != nil { h.serverError(w, r, "failed to list webhooks", err) return } items, err := h.buildWebhookListItems(webhooks) if err != nil { h.serverError(w, r, "failed to list webhooks", err) return } data := map[string]any{ "Webhooks": items, } h.renderTemplate(w, r, "sources_list.html", data) } } // buildWebhookListItems builds the list's entry for each webhook. It // fails when the main database cannot be read. A webhook whose event // database cannot be read is marked on its own entry, and the error is // logged. func (h *Handlers) buildWebhookListItems( webhooks []database.Webhook, ) ([]WebhookListItem, error) { items := make([]WebhookListItem, len(webhooks)) since := time.Now().Add(-longWindow) for i := range webhooks { item := &items[i] item.Webhook = webhooks[i] var err error item.EntrypointCount, item.InactiveEntrypointCount, err = h.countWithInactive(&database.Entrypoint{}, item.ID) if err != nil { return nil, err } item.TargetCount, item.InactiveTargetCount, err = h.countWithInactive(&database.Target{}, item.ID) if err != nil { return nil, err } // Opening an event database that does not exist would create // it, and it would hold nothing to count. if !h.dbMgr.DBExists(item.ID) { continue } err = h.readListEventFigures(item, since) if err != nil { h.log.Error( "failed to read webhook list figures", "webhook_id", item.ID, "error", err, ) item.EventsUnreadable = true } } return items, nil } // countWithInactive returns how many entrypoints or targets, as model // says, a webhook has, and how many of them are inactive. func (h *Handlers) countWithInactive( model any, webhookID string, ) (int, int, error) { var active []bool err := h.db.DB().Model(model). Where("webhook_id = ?", webhookID). Pluck("active", &active).Error if err != nil { return 0, 0, fmt.Errorf( "reading active flags of webhook %s: %w", webhookID, err, ) } inactive := 0 for _, a := range active { if !a { inactive++ } } return len(active), inactive, nil } // readListEventFigures fills in the figures the list shows from the // webhook's event database, with the statistics pane's own queries: // the event count and last arrival from the event totals row, and the // deliveries that failed since the given time from the deliveries' // status index. func (h *Handlers) readListEventFigures( item *WebhookListItem, since time.Time, ) error { webhookDB, err := h.dbMgr.GetDB(item.ID) if err != nil { return err } var totals database.EventTotals err = webhookDB.Take(&totals).Error if err != nil { return fmt.Errorf("reading event totals: %w", err) } item.EventCount = totals.Events - totals.EventsRemoved item.LastEventAt = totals.LastEventAt byTarget, err := finishedByTarget(webhookDB, since) if err != nil { return err } for _, f := range byTarget { item.FailedLast24Hours += f.Failed } return nil }