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
+96
View File
@@ -16,6 +16,54 @@ i18n
'nav.dictionaries': 'Справочники',
'nav.audit': 'Аудит',
'nav.outbox': 'Outbox',
'nav.webhooks': 'Webhooks',
'webhooks.title': 'Webhook subscriptions',
'webhooks.subtitle': 'HTTP-доставка событий не-Kafka подписчикам',
'webhooks.empty': 'Нет подписок. Создайте первую через кнопку справа.',
'webhooks.active': 'Активна',
'webhooks.inactive': 'Отключена',
'webhooks.allScopes': 'все scope',
'webhooks.allDicts': 'все справочники',
'webhooks.allEvents': 'все события',
'webhooks.notFound': 'Подписка не найдена',
'webhooks.col.name': 'Имя',
'webhooks.col.url': 'URL',
'webhooks.col.scopes': 'Scope filter',
'webhooks.col.dictionaries': 'Справочники',
'webhooks.col.status': 'Статус',
'webhooks.action.create': 'Создать',
'webhooks.action.delete': 'Удалить',
'webhooks.action.rotateSecret': 'Сменить secret',
'webhooks.confirmDelete': 'Удалить подписку «{{name}}»? Все pending deliveries удалятся каскадом.',
'webhooks.confirmRotate': 'Сменить HMAC secret? Старый сразу перестанет работать — receiver надо обновить заранее.',
'webhooks.field.name': 'Имя подписки',
'webhooks.field.url': 'Webhook URL',
'webhooks.field.urlHint': 'https:// или http://. Private CIDR (10/8, 192.168/16, 127/8) запрещены SSRF guard',
'webhooks.field.scopeFilter': 'Scope filter',
'webhooks.field.scopeFilterHint': 'Пусто = все scope',
'webhooks.field.dictionaryFilter': 'Справочники (CSV)',
'webhooks.field.dictionaryFilterHint': 'Через запятую: spacecraft, satellite_type. Пусто = все',
'webhooks.field.eventTypeFilter': 'Типы событий (CSV)',
'webhooks.field.eventTypeFilterHint': 'record.created, record.updated, record.closed. Пусто = все',
'webhooks.field.description': 'Описание',
'webhooks.field.createdBy': 'Создатель',
'webhooks.error.nameUrlRequired': 'Имя и URL обязательны',
'webhooks.error.urlScheme': 'URL должен начинаться с http:// или https://',
'webhooks.secret.title': 'HMAC secret для «{{name}}»',
'webhooks.secret.warningTitle': 'Скопируй сейчас!',
'webhooks.secret.warning': 'Этот secret показывается ОДИН раз. Дальше будет замаскирован. Передай receiver чтобы он мог проверять X-Ordinis-Signature header.',
'webhooks.secret.rotatedTitle': 'Secret обновлён',
'webhooks.secret.rotatedWarning': 'Новый secret. Старый перестал работать. Обнови receiver.',
'webhooks.secret.copy': 'Копировать',
'webhooks.secret.copied': 'Скопировано',
'webhooks.secret.acknowledge': 'Сохранил',
'webhooks.deliveries.title': 'История доставок',
'webhooks.deliveries.empty': 'Доставок ещё не было',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Попыток',
'webhooks.deliveries.col.lastAttempt': 'Последняя попытка',
'webhooks.deliveries.col.lastStatusCode': 'HTTP статус',
'webhooks.deliveries.col.lastError': 'Последняя ошибка',
'audit.title': 'Журнал аудита',
'audit.subtitle': 'Юридически значимый лог изменений НСИ',
'audit.filter.dictionary': 'Справочник',
@@ -192,6 +240,54 @@ i18n
'nav.dictionaries': 'Dictionaries',
'nav.audit': 'Audit',
'nav.outbox': 'Outbox',
'nav.webhooks': 'Webhooks',
'webhooks.title': 'Webhook subscriptions',
'webhooks.subtitle': 'HTTP delivery of events to non-Kafka consumers',
'webhooks.empty': 'No subscriptions yet. Create one via the button on the right.',
'webhooks.active': 'Active',
'webhooks.inactive': 'Disabled',
'webhooks.allScopes': 'all scopes',
'webhooks.allDicts': 'all dictionaries',
'webhooks.allEvents': 'all events',
'webhooks.notFound': 'Subscription not found',
'webhooks.col.name': 'Name',
'webhooks.col.url': 'URL',
'webhooks.col.scopes': 'Scope filter',
'webhooks.col.dictionaries': 'Dictionaries',
'webhooks.col.status': 'Status',
'webhooks.action.create': 'Create',
'webhooks.action.delete': 'Delete',
'webhooks.action.rotateSecret': 'Rotate secret',
'webhooks.confirmDelete': 'Delete subscription "{{name}}"? All pending deliveries will be cascade-deleted.',
'webhooks.confirmRotate': 'Rotate HMAC secret? The old one stops working immediately — update the receiver first.',
'webhooks.field.name': 'Subscription name',
'webhooks.field.url': 'Webhook URL',
'webhooks.field.urlHint': 'https:// or http://. Private CIDRs (10/8, 192.168/16, 127/8) blocked by SSRF guard',
'webhooks.field.scopeFilter': 'Scope filter',
'webhooks.field.scopeFilterHint': 'Empty = all scopes',
'webhooks.field.dictionaryFilter': 'Dictionaries (CSV)',
'webhooks.field.dictionaryFilterHint': 'Comma-separated: spacecraft, satellite_type. Empty = all',
'webhooks.field.eventTypeFilter': 'Event types (CSV)',
'webhooks.field.eventTypeFilterHint': 'record.created, record.updated, record.closed. Empty = all',
'webhooks.field.description': 'Description',
'webhooks.field.createdBy': 'Created by',
'webhooks.error.nameUrlRequired': 'Name and URL are required',
'webhooks.error.urlScheme': 'URL must start with http:// or https://',
'webhooks.secret.title': 'HMAC secret for "{{name}}"',
'webhooks.secret.warningTitle': 'Copy now!',
'webhooks.secret.warning': 'This secret is shown ONCE. It will be masked afterwards. Pass it to the receiver so it can verify the X-Ordinis-Signature header.',
'webhooks.secret.rotatedTitle': 'Secret rotated',
'webhooks.secret.rotatedWarning': 'New secret. Old one stopped working. Update the receiver.',
'webhooks.secret.copy': 'Copy',
'webhooks.secret.copied': 'Copied',
'webhooks.secret.acknowledge': 'Saved it',
'webhooks.deliveries.title': 'Delivery history',
'webhooks.deliveries.empty': 'No deliveries yet',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Attempts',
'webhooks.deliveries.col.lastAttempt': 'Last attempt',
'webhooks.deliveries.col.lastStatusCode': 'HTTP status',
'webhooks.deliveries.col.lastError': 'Last error',
'audit.title': 'Audit log',
'audit.subtitle': 'Legally significant log of NSI changes',
'audit.filter.dictionary': 'Dictionary',