feat(audit): полноценный writer в audit_log + admin UI (audit + outbox DLQ)

До этого audit_log entity была определена, но никто туда не писал — реальный
аудит шёл только через Hibernate Envers (без user/IP/trace). Теперь:

- AuditLogger service пишет в той же транзакции что и CRUD (DictionaryRecordService).
  Захватывает: user (JWT sub либо preferred_username), scope, IP (X-Forwarded-For),
  UA, trace_id (MDC), request_id. Не ломает основной flow при сбое — только
  громко логирует.
- AuditLogRepository: гибкий search() с nullable фильтрами (dictionary, user,
  action, businessKey, from, to) + Pageable.
- AuditAdminController: GET /admin/audit (paginated) + /admin/audit/export.csv
  (cap 10k строк против OOM).

Frontend:
- /audit route: фильтр-панель, таблица с цветными бейджами (CREATE/UPDATE/CLOSE),
  expand-row с JSON before/after diff, footer IP/UA/req_id, кнопка экспорта CSV.
  Pagination 50/100/200.
- /outbox route: stat-cards pending/DLQ + DLQ-таблица с last_error.
- Nav links + i18n (RU + EN).
This commit is contained in:
Zimin A.N.
2026-05-04 15:44:14 +03:00
parent e182b30c58
commit 7b2fe6f2d5
13 changed files with 1191 additions and 5 deletions
+90
View File
@@ -14,6 +14,51 @@ i18n
translation: {
'app.title': 'Ordinis НСИ ЦУОД',
'nav.dictionaries': 'Справочники',
'nav.audit': 'Аудит',
'nav.outbox': 'Outbox',
'audit.title': 'Журнал аудита',
'audit.subtitle': 'Юридически значимый лог изменений НСИ',
'audit.filter.dictionary': 'Справочник',
'audit.filter.user': 'Пользователь',
'audit.filter.action': 'Действие',
'audit.filter.businessKey': 'Бизнес-ключ',
'audit.filter.from': 'С',
'audit.filter.to': 'По',
'audit.filter.reset': 'Сбросить',
'audit.filter.apply': 'Применить',
'audit.col.time': 'Время',
'audit.col.action': 'Действие',
'audit.col.dictionary': 'Справочник',
'audit.col.businessKey': 'Бизнес-ключ',
'audit.col.user': 'Пользователь',
'audit.col.scope': 'Scope',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Изменения',
'audit.action.expand': 'Раскрыть',
'audit.action.export': 'Экспорт CSV',
'audit.empty': 'Нет записей по выбранным фильтрам',
'audit.action.CREATE': 'Создание',
'audit.action.UPDATE': 'Изменение',
'audit.action.CLOSE': 'Закрытие',
'audit.diff.before': 'Было',
'audit.diff.after': 'Стало',
'audit.page.of': 'Стр. {{cur}} из {{total}}',
'audit.page.size': 'На странице',
'audit.page.prev': 'Назад',
'audit.page.next': 'Вперёд',
'outbox.title': 'Outbox events',
'outbox.subtitle': 'Очередь публикации в Kafka и DLQ',
'outbox.stats.pending': 'В очереди',
'outbox.stats.dlq': 'В DLQ',
'outbox.dlq.col.id': 'ID',
'outbox.dlq.col.eventType': 'Тип',
'outbox.dlq.col.dictionary': 'Справочник',
'outbox.dlq.col.aggregateId': 'Aggregate ID',
'outbox.dlq.col.topic': 'Topic',
'outbox.dlq.col.attempts': 'Попыток',
'outbox.dlq.col.lastError': 'Последняя ошибка',
'outbox.dlq.col.dlqAt': 'В DLQ с',
'outbox.dlq.empty': 'DLQ пуста — все события публикуются успешно',
'header.scope': 'Доступный scope',
'dict.list.subtitle': 'Каталоги НСИ ЦУОД ОДХ',
'dict.empty': 'В этом справочнике пока нет записей',
@@ -133,6 +178,51 @@ i18n
translation: {
'app.title': 'Ordinis MDM',
'nav.dictionaries': 'Dictionaries',
'nav.audit': 'Audit',
'nav.outbox': 'Outbox',
'audit.title': 'Audit log',
'audit.subtitle': 'Legally significant log of NSI changes',
'audit.filter.dictionary': 'Dictionary',
'audit.filter.user': 'User',
'audit.filter.action': 'Action',
'audit.filter.businessKey': 'Business key',
'audit.filter.from': 'From',
'audit.filter.to': 'To',
'audit.filter.reset': 'Reset',
'audit.filter.apply': 'Apply',
'audit.col.time': 'Time',
'audit.col.action': 'Action',
'audit.col.dictionary': 'Dictionary',
'audit.col.businessKey': 'Business key',
'audit.col.user': 'User',
'audit.col.scope': 'Scope',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Diff',
'audit.action.expand': 'Expand',
'audit.action.export': 'Export CSV',
'audit.empty': 'No entries match the filters',
'audit.action.CREATE': 'Create',
'audit.action.UPDATE': 'Update',
'audit.action.CLOSE': 'Close',
'audit.diff.before': 'Before',
'audit.diff.after': 'After',
'audit.page.of': 'Page {{cur}} of {{total}}',
'audit.page.size': 'Per page',
'audit.page.prev': 'Prev',
'audit.page.next': 'Next',
'outbox.title': 'Outbox events',
'outbox.subtitle': 'Publication queue and DLQ',
'outbox.stats.pending': 'Pending',
'outbox.stats.dlq': 'In DLQ',
'outbox.dlq.col.id': 'ID',
'outbox.dlq.col.eventType': 'Type',
'outbox.dlq.col.dictionary': 'Dictionary',
'outbox.dlq.col.aggregateId': 'Aggregate ID',
'outbox.dlq.col.topic': 'Topic',
'outbox.dlq.col.attempts': 'Attempts',
'outbox.dlq.col.lastError': 'Last error',
'outbox.dlq.col.dlqAt': 'DLQ since',
'outbox.dlq.empty': 'DLQ empty — all events publish successfully',
'header.scope': 'Allowed scope',
'dict.list.subtitle': 'Reference data catalogues',
'dict.empty': 'No records in this dictionary yet',