diff --git a/ordinis-admin-ui/src/components/schema/EventsPreviewTab.tsx b/ordinis-admin-ui/src/components/schema/EventsPreviewTab.tsx
new file mode 100644
index 0000000..6063c46
--- /dev/null
+++ b/ordinis-admin-ui/src/components/schema/EventsPreviewTab.tsx
@@ -0,0 +1,78 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Badge, Tabs, type TabItem } from '@nstart/ui'
+import { generateEventPreviews } from './eventPreview'
+import type { DataScope, JsonSchema } from '@/api/client'
+
+type Props = {
+ dictionaryName: string
+ dataScope: DataScope
+ bundle: string
+ schema: JsonSchema
+ businessKeyField?: string
+}
+
+/**
+ * Preview tab inside DictionaryEditorDialog. Показывает sample JSON envelope
+ * для каждого event type, который Kafka получит после save'а dict / records.
+ *
+ *
Помогает integrators (Альтум, Геопортал, etc) понять shape события до
+ * того как dict published. Без этого они вынуждены ждать live event'а
+ * чтобы посмотреть payload.
+ *
+ *
Pure client-side preview — не отправляет ничего в Kafka. Sample data
+ * генерится из schema properties (string → "sample", integer → 42, и т.д.).
+ */
+export const EventsPreviewTab = ({
+ dictionaryName,
+ dataScope,
+ bundle,
+ schema,
+ businessKeyField,
+}: Props) => {
+ const { t } = useTranslation()
+ const previews = generateEventPreviews(
+ dictionaryName || 'sample_dict',
+ dataScope,
+ bundle || 'cuod',
+ schema,
+ businessKeyField,
+ )
+ const [active, setActive] = useState(previews[0]?.eventType ?? 'RecordCreated')
+ const current = previews.find((p) => p.eventType === active) ?? previews[0]
+ const tabs: TabItem[] = previews.map((p) => ({
+ id: p.eventType,
+ label: p.label,
+ }))
+
+ return (
+
+
+ {t('schema.events.intro')}
+
+
+
+
+ {current && (
+
+
+ {current.eventType}
+
+ {t('schema.events.topic')}:
+
+
+ {current.topic}
+
+
+
{current.description}
+
+ {JSON.stringify(current.envelope, null, 2)}
+
+
+ {t('schema.events.disclaimer')}
+
+
+ )}
+
+ )
+}
diff --git a/ordinis-admin-ui/src/components/schema/eventPreview.ts b/ordinis-admin-ui/src/components/schema/eventPreview.ts
new file mode 100644
index 0000000..3c491bb
--- /dev/null
+++ b/ordinis-admin-ui/src/components/schema/eventPreview.ts
@@ -0,0 +1,200 @@
+/**
+ * Webhook event preview generator (Phase 4.5 / CEO plan v1 polish).
+ *
+ *
При schema definition'е admin видит какой shape JSON event получит
+ * downstream consumer. Это важно для integrators (Альтум, Геопортал, etc) —
+ * перед published'ом нового справочника они хотят посмотреть payload.
+ *
+ *
Функции генерируют sample envelope + payload для трёх record event
+ * types + двух definition event types. Sample data заполняется placeholder'ами
+ * на основе schema property types (string → "sample-string", number → 42, etc).
+ *
+ *
Output не отправляется в Kafka — это pure client-side preview. Backend
+ * shapes (см. ordinis-events-api/src/main/java/.../events/) — единственный
+ * source of truth; preview обновляется вместе с DTOs если schema меняется.
+ */
+import type { DataScope, JsonSchema } from '@/api/client'
+
+const SAMPLE_UUID = '00000000-0000-0000-0000-000000000000'
+const SAMPLE_TIMESTAMP = '2026-05-08T12:00:00+03:00'
+
+/** Generate a placeholder value for one schema property based on type. */
+const sampleValueFor = (prop: JsonSchema | undefined): unknown => {
+ if (!prop) return null
+ if (prop.enum && prop.enum.length > 0) return prop.enum[0]
+ if (prop['x-localized']) return { 'ru-RU': 'Пример', 'en-US': 'Sample' }
+ if (prop.type === 'integer') return 42
+ if (prop.type === 'number') return 3.14
+ if (prop.type === 'boolean') return true
+ if (prop.type === 'array') {
+ return [sampleValueFor(prop.items)]
+ }
+ if (prop.type === 'object' && prop.properties) {
+ const out: Record = {}
+ for (const [k, v] of Object.entries(prop.properties)) {
+ out[k] = sampleValueFor(v)
+ }
+ return out
+ }
+ // string fallback
+ if (prop.format === 'date') return '2026-01-01'
+ if (prop.format === 'date-time') return SAMPLE_TIMESTAMP
+ if (prop['x-references']) return 'REF-001'
+ return 'sample'
+}
+
+/** Build sample data record matching the schema's properties. */
+export const sampleDataFor = (schema: JsonSchema): Record => {
+ const out: Record = {}
+ if (!schema.properties) return out
+ for (const [key, prop] of Object.entries(schema.properties)) {
+ out[key] = sampleValueFor(prop)
+ }
+ return out
+}
+
+type EnvelopeOptions = {
+ dictionaryName: string
+ dataScope: DataScope
+ bundle: string
+ eventType: string
+}
+
+const envelope = (opts: EnvelopeOptions, payload: T) => ({
+ eventId: SAMPLE_UUID,
+ occurredAt: SAMPLE_TIMESTAMP,
+ schemaVersion: '1.0.0',
+ bundle: opts.bundle,
+ dictionaryName: opts.dictionaryName,
+ dataScope: opts.dataScope,
+ traceId: 'sample-trace-id',
+ spanId: 'sample-span-id',
+ payload: { type: opts.eventType, ...payload },
+})
+
+export type EventPreview = {
+ /** "RecordCreated" / "RecordUpdated" / "RecordDeleted" / "DefinitionCreated" / "DefinitionUpdated". */
+ eventType: string
+ /** Прозрачное название для UI tab. */
+ label: string
+ /** Краткое описание когда это событие fires. */
+ description: string
+ /** Topic name pattern в Kafka. */
+ topic: string
+ /** Полный JSON envelope + payload. */
+ envelope: unknown
+}
+
+/** Генерирует все 5 sample events для текущей schema. */
+export const generateEventPreviews = (
+ dictionaryName: string,
+ dataScope: DataScope,
+ bundle: string,
+ schema: JsonSchema,
+ businessKeyField?: string,
+): EventPreview[] => {
+ const sampleData = sampleDataFor(schema)
+ const businessKey =
+ businessKeyField && typeof sampleData[businessKeyField] === 'string'
+ ? (sampleData[businessKeyField] as string)
+ : 'BK-001'
+ const opts = (eventType: string): EnvelopeOptions => ({
+ dictionaryName,
+ dataScope,
+ bundle,
+ eventType,
+ })
+ const topic = `ordinis.${dictionaryName}.${dataScope.toLowerCase()}`
+
+ return [
+ {
+ eventType: 'RecordCreated',
+ label: 'Record Created',
+ description: 'Новая запись добавлена в справочник',
+ topic,
+ envelope: envelope(opts('RecordCreated'), {
+ recordId: SAMPLE_UUID,
+ businessKey,
+ data: sampleData,
+ geometryWkt: null,
+ validFrom: SAMPLE_TIMESTAMP,
+ validTo: '9999-12-31T23:59:59Z',
+ createdAt: SAMPLE_TIMESTAMP,
+ createdBy: 'admin@example.org',
+ }),
+ },
+ {
+ eventType: 'RecordUpdated',
+ label: 'Record Updated',
+ description: 'Создана новая версия записи (старая закрыта validTo=now)',
+ topic,
+ envelope: envelope(opts('RecordUpdated'), {
+ recordId: SAMPLE_UUID,
+ previousRecordId: '11111111-1111-1111-1111-111111111111',
+ businessKey,
+ data: sampleData,
+ previousData: { ...sampleData, _previous: true },
+ geometryWkt: null,
+ validFrom: SAMPLE_TIMESTAMP,
+ validTo: '9999-12-31T23:59:59Z',
+ updatedAt: SAMPLE_TIMESTAMP,
+ updatedBy: 'admin@example.org',
+ }),
+ },
+ {
+ eventType: 'RecordDeleted',
+ label: 'Record Deleted (closed)',
+ description: 'Запись закрыта (valid_to = now). Реального DELETE нет — для audit',
+ topic,
+ envelope: envelope(opts('RecordDeleted'), {
+ recordId: SAMPLE_UUID,
+ businessKey,
+ closedAt: SAMPLE_TIMESTAMP,
+ deletedAt: SAMPLE_TIMESTAMP,
+ deletedBy: 'admin@example.org',
+ reason: 'manual close — example reason',
+ }),
+ },
+ {
+ eventType: 'DefinitionCreated',
+ label: 'Definition Created',
+ description: 'Справочник создан / реrgистрирован в каталоге',
+ topic: `ordinis.definitions`,
+ envelope: envelope(opts('DefinitionCreated'), {
+ dictionaryId: SAMPLE_UUID,
+ name: dictionaryName,
+ displayName: dictionaryName,
+ description: 'Sample dictionary description',
+ scope: dataScope,
+ schemaJson: schema,
+ schemaVersion: '1.0.0',
+ bundle,
+ supportedLocales: ['ru-RU', 'en-US'],
+ defaultLocale: 'ru-RU',
+ createdAt: SAMPLE_TIMESTAMP,
+ createdBy: 'admin@example.org',
+ }),
+ },
+ {
+ eventType: 'DefinitionUpdated',
+ label: 'Definition Updated',
+ description: 'Schema справочника изменена (новый schemaVersion)',
+ topic: `ordinis.definitions`,
+ envelope: envelope(opts('DefinitionUpdated'), {
+ dictionaryId: SAMPLE_UUID,
+ name: dictionaryName,
+ displayName: dictionaryName,
+ description: 'Updated dictionary description',
+ scope: dataScope,
+ schemaJson: schema,
+ previousSchemaJson: { ...schema, _previous: true },
+ schemaVersion: '1.1.0',
+ bundle,
+ supportedLocales: ['ru-RU', 'en-US'],
+ defaultLocale: 'ru-RU',
+ updatedAt: SAMPLE_TIMESTAMP,
+ updatedBy: 'admin@example.org',
+ }),
+ },
+ ]
+}
diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts
index 7b592a1..12ff9ba 100644
--- a/ordinis-admin-ui/src/i18n.ts
+++ b/ordinis-admin-ui/src/i18n.ts
@@ -191,6 +191,10 @@ i18n
'schema.tabs.metadata': 'Метаданные',
'schema.tabs.schema': 'Поля',
'schema.tabs.preview': 'JSON',
+ 'schema.tabs.events': 'События',
+ 'schema.events.intro': 'Превью JSON событий, которые будут опубликованы в Kafka после save справочника. Данные подставлены на основе типов полей schema. Полезно интеграторам (Альтум, Геопортал) для понимания shape\'а до live-выкатки.',
+ 'schema.events.topic': 'Topic',
+ 'schema.events.disclaimer': 'Это client-side preview. Реальные события содержат actual data + traceId/spanId из request scope. Schema events ordinis-events-api/src/main/java/.../events/ — единственный source of truth.',
'schema.name': 'Имя',
'schema.nameHint': 'snake_case, начинается с буквы (например my_dictionary)',
'schema.nameInvalid': 'Только snake_case: a-z, 0-9, _; начинается с буквы',
@@ -536,6 +540,10 @@ i18n
'schema.tabs.metadata': 'Metadata',
'schema.tabs.schema': 'Fields',
'schema.tabs.preview': 'JSON',
+ 'schema.tabs.events': 'Events',
+ 'schema.events.intro': 'Preview of JSON events that will be published to Kafka after the dictionary is saved. Data is filled in based on schema field types. Useful for integrators (Altum, Geoportal) to understand the shape before going live.',
+ 'schema.events.topic': 'Topic',
+ 'schema.events.disclaimer': 'This is a client-side preview. Real events contain actual data + traceId/spanId from request scope. The events ordinis-events-api/src/main/java/.../events/ is the single source of truth.',
'schema.name': 'Name',
'schema.nameHint': 'snake_case, starts with letter (e.g. my_dictionary)',
'schema.nameInvalid': 'Only snake_case: a-z, 0-9, _; must start with letter',