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) ждут ревью`, })}