diff --git a/ordinis-admin-ui/src/components/webhooks/WebhookStatsCards.tsx b/ordinis-admin-ui/src/components/webhooks/WebhookStatsCards.tsx
new file mode 100644
index 0000000..2f5ed4d
--- /dev/null
+++ b/ordinis-admin-ui/src/components/webhooks/WebhookStatsCards.tsx
@@ -0,0 +1,153 @@
+import { useTranslation } from 'react-i18next'
+import {
+ CheckCircleIcon,
+ ArrowClockwiseIcon,
+ ClockIcon,
+ XCircleIcon,
+} from '@phosphor-icons/react'
+import { useWebhookDeliveries } from '@/api/queries'
+import { LoadingBlock } from '@/ui'
+
+/**
+ * Stat cards для webhook subscription detail. 4 status counters
+ * (pending / retrying / delivered / dlq) + computed success rate.
+ *
+ *
Implementation: 4 parallel React Query subscriptions с
+ * {@code size=1, page=0} per status — backend возвращает
+ * {@code totalElements} count'ом без heavy payload. Cheap, дешевле
+ * чем добавлять отдельный /stats endpoint backend.
+ *
+ *
Refetch piggybacks на cache invalidation которая уже работает
+ * (после retry / status filter changes). Поэтому без явного refetchInterval.
+ *
+ *
Phase B (followup): real-time bar chart per hour за последние 24h —
+ * требует /stats?bucketBy=hour endpoint в backend.
+ */
+export function WebhookStatsCards({ subscriptionId }: { subscriptionId: string }) {
+ const { t } = useTranslation()
+ // 4 parallel queries — все по одному status'у с size=1 чтобы получить
+ // только totalElements. React Query dedupe гарантирует что параллельные
+ // деs c одинаковым key не дублируют network call.
+ const pending = useWebhookDeliveries(subscriptionId, 0, 1, 'pending')
+ const retrying = useWebhookDeliveries(subscriptionId, 0, 1, 'retrying')
+ const delivered = useWebhookDeliveries(subscriptionId, 0, 1, 'delivered')
+ const dlq = useWebhookDeliveries(subscriptionId, 0, 1, 'dlq')
+
+ const loading =
+ pending.isLoading || retrying.isLoading || delivered.isLoading || dlq.isLoading
+ if (loading) {
+ return
+ }
+
+ const pCount = pending.data?.totalElements ?? 0
+ const rCount = retrying.data?.totalElements ?? 0
+ const dCount = delivered.data?.totalElements ?? 0
+ const dlqCount = dlq.data?.totalElements ?? 0
+
+ // Success rate = delivered / (terminal) — DLQ это failed terminal. Pending/retrying
+ // ещё в полёте, не учитываем (rate был бы заниженный).
+ const terminal = dCount + dlqCount
+ const successRate = terminal > 0 ? Math.round((dCount / terminal) * 100) : null
+
+ return (
+
+ }
+ label={t('webhooks.stats.pending', { defaultValue: 'В очереди' })}
+ value={pCount}
+ tone="neutral"
+ />
+ }
+ label={t('webhooks.stats.retrying', { defaultValue: 'Retry' })}
+ value={rCount}
+ tone="warn"
+ />
+ }
+ label={t('webhooks.stats.delivered', { defaultValue: 'Доставлено' })}
+ value={dCount}
+ tone="success"
+ />
+ }
+ label={t('webhooks.stats.dlq', { defaultValue: 'DLQ' })}
+ value={dlqCount}
+ tone="danger"
+ />
+ {/* 5-я карточка только когда хоть один terminal был — иначе rate
+ undefined и показ его как «—» бесполезный noise. */}
+ {successRate !== null && (
+
+ )}
+
+ )
+}
+
+function StatCard({
+ icon,
+ label,
+ value,
+ tone,
+}: {
+ icon: React.ReactNode
+ label: React.ReactNode
+ value: number
+ tone: 'success' | 'warn' | 'danger' | 'neutral'
+}) {
+ // Tone → background + text colour для icon + value highlight.
+ // bg-* / text-* tokens из tailwind theme.
+ const styles: Record = {
+ success: { iconClass: 'text-green', valueClass: 'text-green' },
+ warn: { iconClass: 'text-warn', valueClass: 'text-warn' },
+ danger: { iconClass: 'text-pink', valueClass: 'text-pink' },
+ neutral: { iconClass: 'text-mute', valueClass: 'text-ink' },
+ }
+ const { iconClass, valueClass } = styles[tone]
+ return (
+
+
+
+ {value.toLocaleString()}
+
+
+ )
+}
+
+function SuccessRateCard({ rate, total }: { rate: number; total: number }) {
+ const { t } = useTranslation()
+ // Tone по rate'у: ≥95 success, 70-94 warn, <70 danger.
+ // 95% — health threshold по умолчанию для webhook reliability.
+ const tone: 'success' | 'warn' | 'danger' =
+ rate >= 95 ? 'success' : rate >= 70 ? 'warn' : 'danger'
+ const styles = {
+ success: 'text-green border-green/30 bg-green-bg',
+ warn: 'text-warn border-warn/30 bg-warn-bg',
+ danger: 'text-pink border-pink/30 bg-pink-bg',
+ } as const
+ return (
+
+
+
+ {t('webhooks.stats.successRate', { defaultValue: 'Success rate' })}
+
+
+
+ {rate}%
+
+ {t('webhooks.stats.successRateOf', {
+ count: total,
+ defaultValue: `из ${total} завершённых`,
+ })}
+
+
+
+ )
+}
diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts
index a634c1d..504b1d2 100644
--- a/ordinis-admin-ui/src/i18n.ts
+++ b/ordinis-admin-ui/src/i18n.ts
@@ -175,6 +175,14 @@ i18n
'webhooks.secret.acknowledge': 'Сохранил',
'webhooks.deliveries.title': 'История доставок',
'webhooks.deliveries.empty': 'Доставок ещё не было',
+ 'webhooks.stats.pending': 'В очереди',
+ 'webhooks.stats.retrying': 'Retry',
+ 'webhooks.stats.delivered': 'Доставлено',
+ 'webhooks.stats.dlq': 'DLQ',
+ 'webhooks.stats.successRate': 'Success rate',
+ 'webhooks.stats.successRateOf_one': 'из {{count}} завершённой',
+ 'webhooks.stats.successRateOf_few': 'из {{count}} завершённых',
+ 'webhooks.stats.successRateOf_many': 'из {{count}} завершённых',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Попыток',
'webhooks.deliveries.col.lastAttempt': 'Последняя попытка',
@@ -815,6 +823,13 @@ i18n
'webhooks.secret.acknowledge': 'Saved it',
'webhooks.deliveries.title': 'Delivery history',
'webhooks.deliveries.empty': 'No deliveries yet',
+ 'webhooks.stats.pending': 'Pending',
+ 'webhooks.stats.retrying': 'Retrying',
+ 'webhooks.stats.delivered': 'Delivered',
+ 'webhooks.stats.dlq': 'DLQ',
+ 'webhooks.stats.successRate': 'Success rate',
+ 'webhooks.stats.successRateOf_one': 'of {{count}} terminal',
+ 'webhooks.stats.successRateOf_other': 'of {{count}} terminal',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Attempts',
'webhooks.deliveries.col.lastAttempt': 'Last attempt',
diff --git a/ordinis-admin-ui/src/routes/webhooks.$id.tsx b/ordinis-admin-ui/src/routes/webhooks.$id.tsx
index e82d9e8..0b9a6d5 100644
--- a/ordinis-admin-ui/src/routes/webhooks.$id.tsx
+++ b/ordinis-admin-ui/src/routes/webhooks.$id.tsx
@@ -36,6 +36,7 @@ import {
} from '@/api/mutations'
import type { WebhookTestPingResult } from '@/api/client'
import { SCOPE_DOT } from '@/lib/scope-style'
+import { WebhookStatsCards } from '@/components/webhooks/WebhookStatsCards'
export const Route = createFileRoute('/webhooks/$id')({
component: WebhookDetailPage,
@@ -205,6 +206,11 @@ function WebhookDetailPage() {
+ {/* Stats cards — счётчики per status + success rate. Aggregate
+ из 4 параллельных queries с size=1 (cheap). Visual time-series
+ chart — deferred follow-up (требует /stats endpoint). */}
+
+