feat(webhook): Phase 4 — admin-ui /webhooks page

Routes:
- /webhooks (layout) → /webhooks/index (list) + /webhooks/$id (detail)
- nav link добавлен в __root.tsx между Outbox и Language switch

List page (webhooks.index.tsx):
- Table: name | URL | scope filter (colored dots) | dictionaries | status
- ScopeChips reuse SCOPE_DOT из lib/scope-style.ts (consistency с
  dictionaries scope coloring)
- Create dialog: name, URL, scope filter (MultiSelect), dictionary &
  event type (CSV inputs), description. Client-side validation
  (URL scheme, name required) дублирует server-side @Pattern.
- Secret reveal modal после create: warning + plaintext + copy button.
  Single-time view — после закрытия secret не получить, только rotate.

Detail page (webhooks.$id.tsx):
- Subscription metadata grid (URL, status, filters, masked secret,
  createdBy + timestamp)
- Action buttons: rotate-secret + delete (с confirmation)
- Delivery history table: outboxEventId | status badge (delivered/
  retrying/pending/dlq) | attempts | last attempt | HTTP code | error
- Pagination 50/page
- Status badges: delivered=success, retrying=warning, pending=neutral,
  dlq=error

API client (queries + mutations):
- 4 queries: subscriptions list, subscription by id, deliveries page,
  DLQ page (last unused в UI пока, но готов для admin DLQ tab)
- 4 mutations: create, update, delete, rotateSecret. Все invalidate
  правильные query keys.

i18n: 50 ключей × 2 локали (ru-RU + en-US) под webhooks.*
This commit is contained in:
Zimin A.N.
2026-05-06 15:22:28 +03:00
parent e9a7278709
commit 96bfac174d
9 changed files with 1078 additions and 1 deletions
+53
View File
@@ -173,3 +173,56 @@ export type DlqPage = {
number: number
size: number
}
// === Webhooks (v2) ===
export type WebhookSubscription = {
id: string
name: string
url: string
scopeFilter?: DataScope[] | null
dictionaryFilter?: string[] | null
eventTypeFilter?: string[] | null
/** Masked `sk_****<last4>` или plaintext только в response создания/rotate. */
hmacSecret: string
active: boolean
createdAt: string
createdBy: string
updatedAt: string
description?: string | null
}
export type CreateWebhookSubscriptionRequest = {
name: string
url: string
scopeFilter?: DataScope[]
dictionaryFilter?: string[]
eventTypeFilter?: string[]
active?: boolean
description?: string
}
export type WebhookDeliveryStatus = 'pending' | 'retrying' | 'delivered' | 'dlq'
export type WebhookDelivery = {
id: number
subscriptionId: string
outboxEventId: number
status: WebhookDeliveryStatus
attemptCount: number
createdAt: string
nextAttemptAt: string
lastAttemptAt?: string | null
deliveredAt?: string | null
lastError?: string | null
lastStatusCode?: number | null
dlqAt?: string | null
}
export type WebhookDeliveryPage = {
content: WebhookDelivery[]
totalElements: number
totalPages: number
number: number
size: number
}