diff --git a/ordinis-admin-ui/src/api/client.ts b/ordinis-admin-ui/src/api/client.ts
index dd9378a..dc92cfc 100644
--- a/ordinis-admin-ui/src/api/client.ts
+++ b/ordinis-admin-ui/src/api/client.ts
@@ -659,12 +659,20 @@ export type PublishSchemaDraftResult = {
publishedAt: string
}
-/** Pool queue для /admin/schema-reviews/pending. */
+/** Pool queue для /admin/schema-reviews/pending.
+ *
+ *
Backend сериализует Spring {@code Page} как:
+ * {@code {items, page, size, totalElements, totalPages}}. Раньше type
+ * объявлял {@code total} что было mismatched — consumers (`reviews.tsx`,
+ * `useReviewsBadgeCount`) читали {@code data.total} который === undefined,
+ * fallback'или на {@code items.length} (или 0 для badge — отсюда баг
+ * "бэйджа нет"). Type совпадает с фактическим контрактом backend'а. */
export type SchemaReviewQueuePage = {
items: SchemaDraft[]
page: number
size: number
- total: number
+ totalElements: number
+ totalPages: number
}
export type ChangelogDiff = {
diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts
index efa4022..a89817c 100644
--- a/ordinis-admin-ui/src/api/queries.ts
+++ b/ordinis-admin-ui/src/api/queries.ts
@@ -647,7 +647,7 @@ export const useReviewsBadgeCount = (enabled = true) => {
refetchInterval: 60_000,
})
const recordCount = recordQ.data?.totalElements ?? 0
- const schemaCount = schemaQ.data?.total ?? 0
+ const schemaCount = schemaQ.data?.totalElements ?? 0
return {
total: recordCount + schemaCount,
recordCount,
diff --git a/ordinis-admin-ui/src/components/layout/Sidebar.tsx b/ordinis-admin-ui/src/components/layout/Sidebar.tsx
index 84fa3be..0d5f690 100644
--- a/ordinis-admin-ui/src/components/layout/Sidebar.tsx
+++ b/ordinis-admin-ui/src/components/layout/Sidebar.tsx
@@ -14,7 +14,13 @@ import {
} from 'lucide-react'
import { useAuth } from 'react-oidc-context'
import { useCanMutate } from '@/auth/useCanMutate'
-import { useDictionaries, useMyDrafts, useReviewsBadgeCount } from '@/api/queries'
+import {
+ useDictionaries,
+ useMyDrafts,
+ useOutboxStats,
+ useReviewsBadgeCount,
+ useWebhookSubscriptions,
+} from '@/api/queries'
import { Sheet, SheetContent } from '@/ui'
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
import { cn } from '@/lib/utils'
@@ -56,13 +62,22 @@ export function Sidebar() {
// Loaded just for сидбар counter — light request, кэшируется.
const myDrafts = useMyDrafts(0, 1)
// Reviews badge — суммирует record-reviews + schema-reviews pending queues
- // через size=1 paged запросы (cheap: одна запись + metadata). Gated на
- // authenticated, чтобы anonymous не пытался admin endpoint'ы.
+ // через size=1 paged запросы. Gated на authenticated.
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
+ // Outbox/Webhooks badges — light periodic poll. Backend гейтит, поэтому
+ // anonymous safe (просто 401 проигнорируется).
+ const outboxStats = useOutboxStats()
+ const webhooks = useWebhookSubscriptions()
const dictsCount = dictsQuery.data?.length ?? 0
const myDraftsCount = myDrafts.data?.totalElements ?? 0
const reviewsCount = reviews.total
+ // Pending events ждут dispatch. dlq отдельно — alarming, но не блокирует
+ // нормальную работу, можно увидеть на /outbox. Badge показывает только
+ // pending (actionable: 'надо разобраться почему queue растёт').
+ const outboxPending = outboxStats.data?.pending ?? 0
+ const webhooksActive =
+ webhooks.data?.filter((w) => w.active).length ?? 0
// Flat list per redesign/compact.html — no section labels, /graph removed
// (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная →
@@ -91,8 +106,20 @@ export function Sidebar() {
badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true,
},
- { to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
- { to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
+ {
+ to: '/outbox',
+ label: t('nav.outbox'),
+ icon: Inbox,
+ badge: outboxPending > 0 ? outboxPending : undefined,
+ authRequired: true,
+ },
+ {
+ to: '/webhooks',
+ label: t('nav.webhooks'),
+ icon: Webhook,
+ badge: webhooksActive > 0 ? webhooksActive : undefined,
+ authRequired: true,
+ },
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/search', label: t('nav.search'), icon: Search },
],
@@ -394,10 +421,15 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
const dictsQuery = useDictionaries()
const myDrafts = useMyDrafts(0, 1)
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
+ const outboxStats = useOutboxStats()
+ const webhooks = useWebhookSubscriptions()
const dictsCount = dictsQuery.data?.length ?? 0
const myDraftsCount = myDrafts.data?.totalElements ?? 0
const reviewsCount = reviews.total
+ const outboxPending = outboxStats.data?.pending ?? 0
+ const webhooksActive =
+ webhooks.data?.filter((w) => w.active).length ?? 0
// Flat list per redesign — same as desktop Sidebar above.
const sections: NavSection[] = [
@@ -424,8 +456,20 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true,
},
- { to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
- { to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
+ {
+ to: '/outbox',
+ label: t('nav.outbox'),
+ icon: Inbox,
+ badge: outboxPending > 0 ? outboxPending : undefined,
+ authRequired: true,
+ },
+ {
+ to: '/webhooks',
+ label: t('nav.webhooks'),
+ icon: Webhook,
+ badge: webhooksActive > 0 ? webhooksActive : undefined,
+ authRequired: true,
+ },
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
{ to: '/search', label: t('nav.search'), icon: Search },
],
diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts
index 9c06035..ae03752 100644
--- a/ordinis-admin-ui/src/i18n.ts
+++ b/ordinis-admin-ui/src/i18n.ts
@@ -268,7 +268,12 @@ i18n
'audit.col.dictionary': 'Справочник',
'audit.col.businessKey': 'Бизнес-ключ',
'audit.col.user': 'Пользователь',
- 'audit.col.scope': 'Scope',
+ // 'Scope' колонка показывает scope актора (claim из JWT), а НЕ
+ // scope ресурса. Backend audit log не хранит scope dictionary'я.
+ // User confusion: 'правки в public были, а тут RESTRICTED' —
+ // потому что у пользователя scope=RESTRICTED привилегия. Label
+ // 'Доступ актора' разъясняет это.
+ 'audit.col.scope': 'Доступ актора',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Изменения',
'audit.action.expand': 'Раскрыть',
@@ -972,7 +977,7 @@ i18n
'audit.col.dictionary': 'Dictionary',
'audit.col.businessKey': 'Business key',
'audit.col.user': 'User',
- 'audit.col.scope': 'Scope',
+ 'audit.col.scope': 'Actor scope',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Diff',
'audit.action.expand': 'Expand',
diff --git a/ordinis-admin-ui/src/routes/reviews.tsx b/ordinis-admin-ui/src/routes/reviews.tsx
index 209e502..5adecab 100644
--- a/ordinis-admin-ui/src/routes/reviews.tsx
+++ b/ordinis-admin-ui/src/routes/reviews.tsx
@@ -58,7 +58,7 @@ function ReviewsPage() {
const queue = useReviewQueue(0, 50)
const schemaQueue = useSchemaReviewQueue(0, 50)
const recordsCount = queue.data?.totalElements ?? 0
- const schemasCount = schemaQueue.data?.total ?? 0
+ const schemasCount = schemaQueue.data?.totalElements ?? 0
// Tab дефолтит на records (старее, чаще). Если schema queue не пустой
// а record queue пустой — переключаемся на schemas чтобы reviewer'у не
// пришлось кликать дважды.
@@ -590,8 +590,8 @@ function SchemaReviewQueuePanel() {
{t('reviews.schema.queueTotal', {
- count: queue.data?.total ?? items.length,
- defaultValue: `${queue.data?.total ?? items.length} draft(s) ждут ревью`,
+ count: queue.data?.totalElements ?? items.length,
+ defaultValue: `${queue.data?.totalElements ?? items.length} draft(s) ждут ревью`,
})}