feat(webhooks): paginate list + call-history endpoints (server + web UI) (#1268)

Add total-count pagination to the webhook admin endpoints and the web UI.

Store:
- WebhookStore/WebhookCallStore gain Count; WebhookListFilter gains
  IncludeRevoked + Query (PG + SQLite, parameterized, tenant-scoped)

API:
- GET /v1/webhooks and GET /v1/webhooks/{id}/calls return
  {items, total, limit, offset} with server-side search + revoked filtering

Web UI:
- server-driven list pager + search/revoked filter; call-history dialog uses
  the real total (fixes the full-page "has more" boundary bug)
- i18n pager labels (en/vi/zh)

Tests: store pagination integration test; mock stores implement Count.
No schema migration (read-only COUNT).
This commit is contained in:
thotam authored and GitHub committed 2026-06-24 10:39:24 +07:00
1 parent 2945d4db6c
commit 06ef06ca37
22 files changed
+472 -54

No files matched your search

@@ -238,6 +238,31 @@ func (s *SQLiteWebhookCallStore) List(ctx context.Context, f store.WebhookCallLi
return out, rows.Err()
}
func (s *SQLiteWebhookCallStore) Count(ctx context.Context, f store.WebhookCallListFilter) (int, error) {
tid, err := requireTenantID(ctx)
if err != nil {
return 0, err
}
q := `SELECT COUNT(*) FROM webhook_calls WHERE tenant_id = ?`
args := []any{tid}
if f.WebhookID != nil {
q += ` AND webhook_id = ?`
args = append(args, *f.WebhookID)
}
if f.Status != "" {
q += ` AND status = ?`
args = append(args, f.Status)
}
var total int
if err := s.db.QueryRowContext(ctx, q, args...).Scan(&total); err != nil {
return 0, err
}
return total, nil
}
func (s *SQLiteWebhookCallStore) DeleteOlderThan(ctx context.Context, tenantID uuid.UUID, ts time.Time) (int64, error) {
var res sql.Result
var err error
+35
View File
@@ -154,6 +154,13 @@ func (s *SQLiteWebhookStore) List(ctx context.Context, f store.WebhookListFilter
q += ` AND agent_id = ?`
args = append(args, *f.AgentID)
}
if !f.IncludeRevoked {
q += ` AND revoked = 0`
}
if f.Query != "" {
q += ` AND (name LIKE ? COLLATE NOCASE OR secret_prefix LIKE ?)`
args = append(args, "%"+f.Query+"%", f.Query+"%")
}
q += ` ORDER BY created_at DESC`
limit := f.Limit
@@ -180,6 +187,34 @@ func (s *SQLiteWebhookStore) List(ctx context.Context, f store.WebhookListFilter
return out, rows.Err()
}
func (s *SQLiteWebhookStore) Count(ctx context.Context, f store.WebhookListFilter) (int, error) {
tid, err := requireTenantID(ctx)
if err != nil {
return 0, err
}
q := `SELECT COUNT(*) FROM webhooks WHERE tenant_id = ?`
args := []any{tid}
if f.AgentID != nil {
q += ` AND agent_id = ?`
args = append(args, *f.AgentID)
}
if !f.IncludeRevoked {
q += ` AND revoked = 0`
}
if f.Query != "" {
q += ` AND (name LIKE ? COLLATE NOCASE OR secret_prefix LIKE ?)`
args = append(args, "%"+f.Query+"%", f.Query+"%")
}
var total int
if err := s.db.QueryRowContext(ctx, q, args...).Scan(&total); err != nil {
return 0, err
}
return total, nil
}
func (s *SQLiteWebhookStore) Update(ctx context.Context, id uuid.UUID, updates map[string]any) error {
tid, err := requireTenantID(ctx)
if err != nil {