Commit Graph

405 Commits

Author SHA1 Message Date
Zimin A.N. a1e83a86bc fix(admin-ui): 403 + отсутствует accessToken → trigger reauth handler
Симптом: после ~30 мин админ видит 403 на /admin/*, перелогин чинит.

Цепочка событий:
1. access_token истекает
2. oidc-client-ts автоматический silent renew падает (refresh_token expired
   за 30 мин SSO Session Idle / iframe заблокирован)
3. auth.user становится undefined (oidc убирает после full expiry)
4. TokenSync setAccessToken(null) — accessToken в apiClient зануляется
5. Следующий запрос идёт БЕЗ Authorization header
6. Backend (ORDINIS_AUTH_REQUIRED=false) пропускает как anonymous →
   ScopeContext = PUBLIC → controller с requireInternal() кидает 403

401-handler не срабатывает на 403 → юзер залипает.

Также мой предыдущий useEffect в TokenSync (MR !267) триггерит на
auth.user?.expired === true, но если user удалён полностью —
optional-chaining вернёт undefined и useEffect не сработает.

Fix: расширил interceptor чтобы на (status === 403 && accessToken == null)
тоже вызывать unauthorizedHandler. Этот handler — single source of
truth, делает signinSilent → fallback signinRedirect.

Не цепляет legitimate 403 (когда у юзера ЕСТЬ токен но scope недостаточно —
например viewer пытается dropить INTERNAL dict). accessToken != null
в таких случаях → ветка не сработает.
2026-05-25 19:19:39 +03:00
semantic-release-bot 172a6bd9cc chore(release): v2.42.4
## [2.42.4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.3...v2.42.4) (2026-05-25)

### 🐛 Fixes

* **auth:** TokenSync — проактивная re-auth при expired user ([4cd92ae](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/4cd92aeb60fcdd509e9aeedd3024cb3638949559))
* **auth:** роль admin даёт RESTRICTED scope (видит всё) ([c5adbdf](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/c5adbdf39c3e44b76028b37173ef97d0123fb7be))
2026-05-25 16:06:18 +00:00
Zimin A.N. 4cd92aeb60 fix(auth): TokenSync — проактивная re-auth при expired user
Симптом: после ~30 мин неактивности пользователь видит 403 на /admin/audit
и /admin/users. Перелогин решает.

Корень. automaticSilentRenew: true в oidcConfig должен обновлять
access_token, но падает молча когда:
- refresh_token истёк (Keycloak SSO Session Idle = 30 мин)
- silent renew iframe заблокирован 3rd-party cookies
- silentRenewError не подцеплен глобально

В этих случаях user.expired=true, accessToken=null. Дальнейшие запросы идут
БЕЗ Authorization header → backend с ORDINIS_AUTH_REQUIRED=false пропускает
как anonymous → requireInternal() видит PUBLIC scope → 403 (не 401).
401-handler в interceptor'е не срабатывает (статус не тот) → юзер залипает.

Fix. Новый useEffect в TokenSync watches auth.user?.expired. На transition
в true: signinSilent (вдруг refresh ещё жив), на failure signinRedirect.
Anti-loop ref предотвращает повторные попытки пока expired=true.

Бонусом — за компанию с MR !267 (admin → RESTRICTED) — после v2.42.4 деплоя
админ может работать сессией дольше 30 мин без 403'ов.
2026-05-25 18:54:37 +03:00
semantic-release-bot 42dede1e2b chore(release): v2.42.3
## [2.42.3](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.2...v2.42.3) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** не резолвить «anonymous» через /admin/users/{sub}/display ([8c0b74c](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/8c0b74cbf86b936d64af8c03434af3b9c8386bfe))
2026-05-25 15:41:03 +00:00
Zimin A.N. 8c0b74cbf8 fix(admin-ui): не резолвить «anonymous» через /admin/users/{sub}/display
Backend currentUserId() возвращает литерал "anonymous" когда JWT отсутствует
(ORDINIS_AUTH_REQUIRED=false fallback). Audit / draft entries сохраняют
этот sub в БД. Frontend UserCell дёргал /admin/users/anonymous/display
→ 404 → catch → display "—". Логически ок, но в консоли DevTools мусорный
404 на каждом рендере таблицы.

Skip endpoint когда uuid === "anonymous":
- useResolveUserDisplay: enabled = uuid && uuid !== 'anonymous'
- useUserDisplay: short-circuit ветка возвращает display: 'anonymous'

UX без изменений (раньше показывал «—» с full UUID в tooltip; теперь
показывает «anonymous» без 404 в консоли).
2026-05-25 18:34:20 +03:00
semantic-release-bot 936231c8e3 chore(release): v2.42.2
## [2.42.2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.1...v2.42.2) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** UserPicker — порядок хуков (rules-of-hooks) ([cc85b3b](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/cc85b3b337e5beed1686cbd4728ec2ddef37cd99))
2026-05-25 15:20:41 +00:00
Zimin A.N. cc85b3b337 fix(admin-ui): UserPicker — порядок хуков (rules-of-hooks)
Early-return на TextInput-fallback стоял ДО useMemo(triggerLabel) и
useMemo(selected). React падает «rendered fewer hooks than expected»
когда количество вызовов хуков меняется между рендерами.

Reproduce: loading=true первый рендер → ВСЕ хуки выполнились (включая
triggerLabel). loading=false с data=[] второй рендер → fallback
возвращается до triggerLabel useMemo → меньше хуков → error boundary
ловит → юзер видит «Что-то пошло не так».

Перенёс все useMemo выше early-return. Логика рендера без изменений.
2026-05-25 18:13:56 +03:00
semantic-release-bot 5268df0653 chore(release): v2.42.1
## [2.42.1](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.0...v2.42.1) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** UserPicker — TextInput fallback когда cache пустой ([838138a](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/838138af5eec4cafdade630fec0f3e5d1704d0b1))
* **admin-ui:** полноценные стили DayPicker'а + 1 месяц в PeriodFilter ([07a748b](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/07a748be53b592903191445e3075efefe544db82))
2026-05-25 14:56:31 +00:00
Zimin A.N. 838138af5e fix(admin-ui): UserPicker — TextInput fallback когда cache пустой
Если /api/v1/admin/users возвращает [] (cache пустой / writer только-что
рестартован / у юзера нет INTERNAL scope → 403 → catch → []), picker
показывал «Пользователи не найдены» и фильтр становился недоступен. До
MR !263 здесь был обычный TextInput, не лишаем юзера возможности вписать
UUID руками.

При isLoading=false, isError=false, data.length===0 — рендерим TextInput
с подсказкой «Список юзеров пуст — введите вручную». Backend cache
прогреется по мере того как юзеры будут делать authenticated requests
(JwtUserCaptureFilter перепишет user_display_cache); тогда picker
включится автоматически.
2026-05-25 17:50:23 +03:00
Zimin A.N. 07a748be53 fix(admin-ui): полноценные стили DayPicker'а + 1 месяц в PeriodFilter
PeriodFilter рендерил DayPicker numberOfMonths={2} со стандартными
rdp-* стилями — `.rdp-months` имеет `flex-wrap: wrap`, при недостатке
ширины два месяца стакались вертикально, ячейки большие/разреженные,
selected/hover состояний под наш accent не было. Выглядело rough.

Два фикса:

1. PeriodFilter → numberOfMonths={1}. Для audit-фильтрации одного
   месяца достаточно (presets покрывают «7д/30д/Этот месяц»; календарь
   нужен для arbitrary range). Простой layout, не зависит от ширины
   popover'а.

2. Полноценные стили `.ord-day-picker` в styles.css (~80 строк):
   - tight cells (2rem × 2rem)
   - hover state на surface-2
   - selected day → accent bg + on-accent text
   - range_middle → accent-bg, обнулённый radius
   - range_start/end → polo'асn'ое closure radius
   - today → 1px outline
   - outside-month days → mute opacity
   - nav buttons → accent цвет + hover
   - mono font на числа (JetBrains Mono), Inter на caption/weekday

Бенефит распространяется и на DateRangePicker (использует тот же класс).
flex-wrap: nowrap override — на случай если когда-то понадобится
numberOfMonths={2} в другом контексте, не будет вертикального стека.
2026-05-25 17:45:39 +03:00
semantic-release-bot 0352e5f212 chore(release): v2.42.0
## [2.42.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.41.4...v2.42.0) (2026-05-25)

###  Features

* /audit поле «Пользователь» — typeahead picker из user-cache ([3f05741](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/3f05741ed16730dd98ef8b42dd9af3a584922e2d))
* **admin-ui:** кастомный период-фильтр для /audit — preset chips + календарь ([e72aa37](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/e72aa37e847f65c143e0443d925e62dac2c664c7))
2026-05-25 14:33:52 +00:00
Zimin A.N. 3f05741ed1 feat: /audit поле «Пользователь» — typeahead picker из user-cache
Backend: GET /api/v1/admin/users (INTERNAL+ scope) — list cached юзеров
из user_display_cache. UserDisplayService.listAll(limit) сортирует по
preferred_username, hard-cap 1000. Контроллер: UserDisplayController.list().

Frontend:
- queries.ts → useAllUsers() / allUsersQuery (5min stale, 403/404 → []).
- components/audit/UserPicker.tsx (новый, ~140 строк) — cmdk-powered
  combobox: trigger показывает username (или UUID prefix fallback),
  popover c поиском по preferredUsername/email/UUID. X сбрасывает selection.
- routes/audit.tsx — TextInput → UserPicker.

UX: раньше юзер вручную копипастил UUID из audit-row'а; теперь поиск
по имени/email типа `solov` → находит `solovyev.da`.
2026-05-25 16:48:41 +03:00
Zimin A.N. e72aa37e84 feat(admin-ui): кастомный период-фильтр для /audit — preset chips + календарь
Заменил голый DateRangePicker (только календарь, требует двух кликов на любой
typical период) на preset-driven PeriodFilter — pattern из Datadog/Grafana:

Triггер компактный (h-9), показывает active preset label либо forматированный
range («04.05.2026 — 29.05.2026»). При клике открывается popover с:
- Список пресетов слева: Сегодня / Вчера / Последние 7 дней / 30 дней /
  Этот месяц / Прошлый месяц
- «Произвольный диапазон» внизу — раскрывает 2-month DayPicker справа

Active preset подсвечивается. Custom range детектится sameRange сравнением —
если value матчит preset, его и подсвечиваем. X-кнопка в trigger'е чистит
период без открытия popover'а.

DayPicker lazy-loaded (~80kb gz, не пакуется в initial audit bundle).
ISO bounds в audit.tsx не трогали — onChange отдаёт Date'ы, нормализация
T00:00:00/T23:59:59 + local TZ остаётся прежней.

Файлы:
- components/audit/PeriodFilter.tsx (новый, ~190 строк)
- routes/audit.tsx — заменил DateRangePicker → PeriodFilter, убран import

171/171 tests, TSC чистый.
2026-05-25 16:42:50 +03:00
semantic-release-bot 278b500590 chore(release): v2.41.4
## [2.41.4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.41.3...v2.41.4) (2026-05-25)

### 🐛 Fixes

* **ci:** retag — фиксированный 8-char short-SHA вместо git %h ([7686515](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/768651528c3796c751deab5521ce6b87a98976b5))
2026-05-25 13:12:01 +00:00
semantic-release-bot 39fbbfe06a chore(release): v2.41.3
## [2.41.3](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.41.2...v2.41.3) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** audit — компактнее панель фильтров + popover календаря ([2548f24](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/2548f24aff5df52e66f560111d2338a99d6c57ea))
2026-05-25 12:47:32 +00:00
Александр Зимин 3d3a03ab97 Merge branch 'fix/audit-filter-compact-datepicker' into 'main'
fix(admin-ui): audit — компактнее панель фильтров + popover календаря

See merge request 2-6/2-6-4/terravault/ordinis!261
2026-05-25 12:37:47 +00:00
semantic-release-bot 98977716fb chore(release): v2.41.2
## [2.41.2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.41.1...v2.41.2) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** sidebar «Мои черновики» badge — считаем только actionable ([7b1d883](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/7b1d8834a1cb7a7a11f29fe98b73788793b38d2f))
* **admin-ui:** убрать badge с «Справочники» — total count не actionable ([7a9170d](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/7a9170d96eefbf5182f49261f7c99e65ffcafbaa))
2026-05-25 12:36:32 +00:00
Zimin A.N. 2548f24aff fix(admin-ui): audit — компактнее панель фильтров + popover календаря
1. DateRangePicker popover. PopoverContent имел default w-72 (288px),
   а DayPicker с numberOfMonths={2} требует ~560px. Из-за этого второй
   месяц переносился под первый, popover вылазил поверх таблицы аудита
   и кнопок «Сбросить/Применить». Override на p-3 w-auto max-w-[min(95vw,640px)]
   — два месяца теперь side-by-side, popover floats как должно.

2. Панель фильтров /audit. Было: 3-col grid с 4 input'ами + DateRange
   span'ит 2 cols на отдельной строке. Стало: на xl (≥1280px) все 5
   полей inline в одну строку (lg → 3 cols, sm → 2, mobile → 1).
   Padding p-4 space-y-3 → p-3 space-y-2. Убран border-t на action row
   — визуальный шум.

Изменения только на /audit (date-range-picker — shared компонент,
но используется только в audit-фильтрах сейчас; рендер других мест
не страдает — popover просто шире, w-auto не ломает узкий контент).
2026-05-25 15:36:32 +03:00
Zimin A.N. 7a9170d96e fix(admin-ui): убрать badge с «Справочники» — total count не actionable
Цифра у иконки словарей показывала общее число справочников (~40 на проде).
Не меняется в течение работы юзера — не actionable, только визуальный шум.

Actionable badges остаются: «Мои черновики», «На ревью», «Outbox», «Webhooks» —
там цифра реально что-то означает (надо что-то сделать или разобраться).

Заодно убрал unused dictsQuery / dictsCount + import useDictionaries из обоих
сайдбаров (desktop + mobile). TSC noUnusedLocals чистый.
2026-05-25 15:30:04 +03:00
Zimin A.N. 7b1d8834a1 fix(admin-ui): sidebar «Мои черновики» badge — считаем только actionable
Badge в сайдбаре (desktop + mobile) показывал totalElements от /drafts/me
и /admin/schema-drafts/me — это сумма ВСЕХ статусов (PENDING + APPROVED +
REJECTED + WITHDRAWN). После approve/reject счётчик не уменьшался, юзер
видел «висящие» цифры на драфтах которые уже terminal.

Tab-бейдж в ReviewsPage давно делает правильно через classifyRecordStatus
/ classifySchemaStatus. Подтягиваем sidebar к той же логике:
- fetch size 50 вместо 1 (нужны .items[] для filter; TanStack дедуплицирует
  query — на /reviews этот же ключ уже зовётся, лишних запросов нет)
- record → 'pending', schema → 'pending'|'wip'
- approved schema всё ещё попадает в pending по дизайну (maker должен
  publish'нуть; см. classifySchemaStatus комментарий)
2026-05-25 15:28:00 +03:00
semantic-release-bot 997d5eb1a6 chore(release): v2.41.1
## [2.41.1](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.41.0...v2.41.1) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** badge на «Мои» не обновлялся + editor wider на десктопе ([b828225](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/b828225b28dfe75b254e0c3deca3391d38ab66e0))
* **admin-ui:** reviews — резолвить dictionaryId в имя словаря ([126b797](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/126b797212a57ec259faa7de3e92d55fe8ebfd37))
* **admin-ui:** reviews drawer — dict link, live FK, JSON syntax+diff ([ac4f554](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/ac4f554e34db32598388b3c05d7460f01e716b6b))
* **admin-ui:** сайдбар в collapsed-mode показывает badge как corner overlay ([85177b5](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/85177b530ccdacc21bc69fb82df0ca50daacd94a))
2026-05-25 12:09:31 +00:00
Zimin A.N. 85177b530c fix(admin-ui): сайдбар в collapsed-mode показывает badge как corner overlay
В collapsed-режиме (56px полоска) рендерили только иконку без label
и без badge — счётчик pending записей/драфтов был не виден, юзер не
понимал что есть необработанные элементы пока не раскроет сайдбар.

Теперь в collapsed: badge → маленький круглый pill в углу иконки
(top-right, 16×16, ring-1 от bg чтобы хорошо отделялся от иконки).
Cap "99+" если число большое — uniform width. Tooltip на link
включает label + (count) при hover.

Expanded-mode без изменений — badge справа от label как был.
2026-05-25 15:02:13 +03:00
Zimin A.N. b828225b28 fix(admin-ui): badge на «Мои» не обновлялся + editor wider на десктопе
1. useApproveDraft/useRejectDraft теперь инвалидируют ['drafts'] key
   (my-drafts + by-dict lists). До этого инвалидировался только ['draft']
   (single by id), поэтому after-approve "Мои" badge продолжал показывать
   pending-счётчик даже когда все драфты уже APPROVED — useMyDrafts отдавал
   стейл-кеш. useWithdrawDraft уже это делал, теперь подтянули approve/reject.

2. DictionaryEditorDialog: на десктопе (≥880px) modal теперь max-w 1400px
   вместо max-w-5xl (880px после маппинга в SIZE_CLASSES.xl). На <880px
   осталось w-[95vw] (compact mobile modal). На 1440p мониторе editor
   больше не выглядит вжатым, tabs+schema+preview помещаются без скролла.
   Override через panelClassName c `!` чтобы перебить SIZE_CLASSES.
2026-05-25 14:59:57 +03:00
Zimin A.N. ac4f554e34 fix(admin-ui): reviews drawer — dict link, live FK, JSON syntax+diff
Три проблемы по reviews-drawer (по фидбеку: «Открыть ломается / Согласование
не показывает справочник / не показывает live / JSON не подсвечен»):

1. useLiveRecord(draft.dictionaryId, ...) → useLiveRecord(dictName, ...).
   Endpoint /api/v1/dictionaries/{name}/records/{bk} ожидает name, не UUID;
   до фикса валился с 404 «Dictionary not found: <uuid>» → live-блок был
   пустой и показывался "noLive" даже для UPDATE/CLOSE drafts.

2. Header drawer'а теперь рендерит "<dictName> / <businessKey>" вместо
   просто businessKey. dictName — link на /dictionaries/$name. Fallback
   на UUID-prefix без link'а если dict не виден (scope-hide).

3. JsonView (новый компонент, ~120 строк, без deps) заменяет голые <pre>
   JSON.stringify(...):
   - syntax highlight: ключи (aurora), строки (pink), числа (warn),
     true/false/null (accent), пунктуация (mute)
   - diff highlight: top-level ключи из shallowDiffSummary получают
     фон+border (зелёный added / красный removed / жёлтый changed).
     В live-колонке — removed+changed, в proposed — added+changed.

Также добавлен error-state для liveQ (раньше 404 от useLiveRecord на UUID
молча превращался в "noLive"; теперь explicit "не удалось загрузить").
2026-05-25 14:55:51 +03:00
Zimin A.N. 126b797212 fix(admin-ui): reviews — резолвить dictionaryId в имя словаря
В таблицах /reviews (Запись + Мои → Запись) колонка «Объект» показывала
UUID-prefix («254a662f…/REFLEXIO-OPTIC») вместо имени словаря, а ссылка
вела на /dictionaries/<uuid> — 404 (route ожидает name, не UUID).

DraftResponse несёт только dictionaryId (UUID); dictionaryName есть только
у schema-drafts. Резолвим через useDictionaries() — кешируется TanStack
Query, дополнительных запросов не делает.

Fallback на UUID-prefix без link'а, когда dict не в выдаче (scope-hide /
удалён) — лучше чем ведущая в 404 ссылка.

Files:
- routes/reviews.tsx — records-tab table (queue, ReviewsPage)
- components/reviews/MyDraftsPanel.tsx — RecordRow ("Мои" tab)
2026-05-25 14:51:01 +03:00
semantic-release-bot e40689006a chore(release): v2.41.0
## [2.41.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.40.0...v2.41.0) (2026-05-25)

###  Features

* **cuod:** mission.spacecraft_codes — FK на spacecraft.designator ([fd7d7d4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/fd7d7d4535f2a05516a8b340cf0ba62d6e8b0059))
* **cuod:** остальные FK — country, industry_sector, frequency_band ([cd5ee1f](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/cd5ee1fcf6a0555e1969c83bdea2e6c347fef2cd))
2026-05-25 11:30:31 +00:00
semantic-release-bot 99fabc3245 chore(release): v2.40.0
## [2.40.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.39.0...v2.40.0) (2026-05-25)

###  Features

* **admin-ui,cuod:** array-FK picker для instrument ([1cb0d86](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/1cb0d868c867dc9b48dec4de06602e7bffee21b4))
2026-05-25 10:47:40 +00:00
Zimin A.N. 1cb0d868c8 feat(admin-ui,cuod): array-FK picker для instrument
Три массива в instrument были comma-separated текстом с подсказкой
«businessKey'и из X». Пользователь не знал что туда вписать — справочника
доступных значений рядом нет.

UI: добавлен ReferenceMultiSelectField — array-аналог ReferenceSelectField.
Триггер — `items["x-references"]` на string-массиве. Под капотом — готовый
MultiSelect (popover + cmdk-поиск). Fallback на comma-separated TextInput
сохраняется когда target dict недоступен (scope-hide / 403 / 404), сервер
всё равно валидирует FK через ReferenceValidator.

Schema:
- spacecraft_codes      → items.x-references: spacecraft.designator
- supported_format_codes → items.x-references: data_format.code
- supported_level_codes  → items.x-references: processing_level.code

instrument schemaVersion 1.0.0 → 1.1.0, bundle 1.3.2 → 1.3.3 —
CuodBundleImporter перепишет schema_json в БД на старте writer'а,
admin-ui подхватит свежую схему через /api/v1/dictionaries/instrument.

Tests: 2 новых кейса в SchemaDrivenForm.test.tsx — happy path (MultiSelect
рендерится, запрос ушёл на правильный dict) + fallback (comma-separated
с подсказкой «список недоступен»). 171/171 passed.
2026-05-25 13:28:03 +03:00
semantic-release-bot 1ea402e7ec chore(release): v2.39.0
## [2.39.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.38.0...v2.39.0) (2026-05-23)

###  Features

* **cuod:** instrument — approvalRequired=true ([af9e76a](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/af9e76a2b5dff3126e6768ddb27b26639062fa12))
2026-05-23 15:51:30 +00:00
semantic-release-bot a77b613efc chore(release): v2.38.0
## [2.38.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.37.4...v2.38.0) (2026-05-21)

###  Features

* **cuod:** spacecraft.mission — x-localized текст → FK на mission.code ([656da8c](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/656da8c9b904ae2da1f0a017c205b8a00e09ddc9))
2026-05-21 13:45:56 +00:00
semantic-release-bot 2115212b8a chore(release): v2.37.4
## [2.37.4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.37.3...v2.37.4) (2026-05-21)

### 🐛 Fixes

* **admin-ui:** хвосты — 404 на CREATE-черновиках + Radix DialogTitle warning ([2a82a87](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/2a82a875c8eb7f12bc6a376b2f1f1f3097bcf096))
2026-05-21 13:11:27 +00:00
Zimin A.N. 2a82a875c8 fix(admin-ui): хвосты — 404 на CREATE-черновиках + Radix DialogTitle warning
1. reviews.tsx: ReviewDrawer звал useLiveRecord для ЛЮБОГО черновика,
   включая CREATE. У CREATE-черновика live-записи нет по определению →
   GET /dictionaries/{id}/records/{key} → 404 (красный шум в консоли на
   каждый CREATE-draft). Теперь для CREATE запрос не запускается, блок
   «текущая версия» показывает «нет текущей версии».

2. modal.tsx: Modal рендерил DialogTitle только при наличии prop title.
   Без title Radix писал в консоль «DialogContent requires a DialogTitle»
   + warning про aria-describedby. Теперь Modal всегда рендерит DialogTitle
   (sr-only fallback) и гасит aria-describedby когда описания нет.
2026-05-21 16:05:09 +03:00
semantic-release-bot 7621f0a95f chore(release): v2.37.3
## [2.37.3](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.37.2...v2.37.3) (2026-05-21)

### 🐛 Fixes

* **admin-ui:** «Сохранить» всегда даёт видимый ответ ([b48a5dd](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/b48a5dd94c6752a14505f1b8668747ee5911d5a2))
* **admin-ui:** запись не сохранялась при пустом optional x-localized поле ([dcb5612](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/dcb5612c949dde3c8eaf122673833371b9249a6d))
2026-05-21 12:58:54 +00:00
Zimin A.N. b48a5dd94c fix(admin-ui): «Сохранить» всегда даёт видимый ответ
Принцип: клик по «Сохранить» не должен оставаться без реакции.

Раньше submit мог молча оборваться: AJV-ошибка ложилась через setError
на nested-путь (data.<field>.<locale>), а FieldError читает data.<field>
→ ошибка невидима, форма «не реагирует».

Добавлено:
- blockedMsg — состояние с причиной, рендерится видимым Alert сверху
  формы при любой блокировке submit (validFrom/validTo, AJV).
- onInvalid — обработчик RHF handleSubmit на случай когда RHF сам
  блокирует submit (required-поля); тоже ставит blockedMsg.
- scrollIntoView к началу формы — Alert гарантированно в зоне видимости.

Теперь Save либо сохраняет, либо показывает «Не удалось сохранить —
проверьте поля: …». Молчания не будет.
2026-05-21 15:52:06 +03:00
Zimin A.N. dcb5612c94 fix(admin-ui): запись не сохранялась при пустом optional x-localized поле
Симптом: «Сохранить» (create и edit) ничего не делал, ошибок не видно.

Причина: x-localized поле рендерит Controller на каждую локаль
(data.<field>.<locale>). RHF при сабмите реконструирует values из имён
зарегистрированных полей и материализует родительский объект даже когда
исходное значение было null/отсутствовало → optional поле (например
mission) превращалось в { "ru-RU": undefined, "en-US": undefined }.

AJV отвергал undefined-локали ("must be string"), submit() делал return
до onSubmit — запрос не уходил. Ошибка ложилась через setError на
data.<field>.<locale>, а FieldError читает data.<field> → невидима.

Фикс: pruneEmpty() в submit() рекурсивно убирает undefined и опустевшие
объекты из data перед AJV-валидацией и отправкой — нетронутое
optional-поле снова валидируется как «отсутствует». Не трогает
null/''/0/false.

Откат временной диагностики [ordinis-diag] из MR !251.
Regression-тест падает без фикса, проходит с фиксом.
2026-05-21 15:34:18 +03:00
semantic-release-bot ffffacb538 chore(release): v2.37.2
## [2.37.2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.37.1...v2.37.2) (2026-05-21)

### 🐛 Fixes

* **rest-api:** bean-валидация возвращает 400 вместо 500 ([b6a41f4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/b6a41f47a8fa1cead1aeefef55848bf8acea1897))

### 📝 Docs

* **api:** локализация — только Accept-Language header (не ?locale= query) ([11b6ca9](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/11b6ca98354b19cf558bb987427cd81668c60827))
* **api:** уточнение — locale работает только на records, не на schema ([ee4f495](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/ee4f4959732272cba310421924d96705b87b62c1))
* **cheatsheet:** добавить конкретные примеры с реальными dicts ([f3c3e33](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/f3c3e33289003f6f07d41af00d79df277f33032b))
* HTML-вариант регламента «что заводить в НСИ» ([20763fe](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/20763feeb173000625c4e5ab23a72b7f4d286d6d)), closes [#B05A2E](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/B05A2E)
* **integration:** регламент НСИ + текст рассылки для интеграторов ([5b96f98](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/5b96f98fb81e1c6148b1cbe10c0813a5e8c173e6))
* **quickstart:** добавить Приложение A · Cheat Sheet ([5eecfda](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/5eecfdaad57793966a2cbc38b035efb16b756bfa))
* **toc:** + bundle-format-spec.md (fixes Diplodoc build — link unreachable) ([8025320](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/802532057b5282ce4c20788bee099aeaa234c5bb))
* **toc:** зарегистрировать quickstart + cheatsheet + регламент в Diplodoc nav ([3365e94](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/3365e9462a3495546cd444a75019b756799f5901))
2026-05-21 10:38:49 +00:00
Zimin A.N. bc426a9975 chore(admin-ui): диагностическая инструментация сохранения записи [TEMP]
ВРЕМЕННО — для поиска причины «Save ничего не делает». Откатить
после диагностики (все логи помечены [ordinis-diag]).

SchemaDrivenForm: console.warn в submit() — событие формы, RHF
onInvalid, результат AJV, ветки return, вызов onSubmit.
dictionaries.$name: логи в route.handleSubmit + createMut/updateMut
mutate + onSuccess/onError со статусом ответа.

Также добавлен постоянный регресс-тест: повторный submit после
исправления валидации вызывает onSubmit.
2026-05-21 13:28:38 +03:00
semantic-release-bot c1f87d638e chore(release): v2.37.1
## [2.37.1](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.37.0...v2.37.1) (2026-05-19)

### 🐛 Fixes

* **snapshots:** handle Instant return from pgJDBC 42.7+ (was 500) ([d908feb](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/d908febc91524c03b83a34b0e5b2a0ac9c7c4f4b))

### 📝 Docs

* **api:** cheatsheet + quickstart sync с реальным API после Postman audit ([ae05e17](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/ae05e17f96c4410ddd95c0a51c6364c4707df1bd))
* **api:** Quick Start guide + Cheat Sheet для интеграторов ([04e8251](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/04e82519415015a71f9db1ea0e55268f031b4c21))
2026-05-19 20:31:20 +00:00
semantic-release-bot 2f06b408b8 chore(release): v2.37.0
## [2.37.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.36.0...v2.37.0) (2026-05-17)

###  Features

* **graph:** добавлен поиск по справочникам в Graph view ([d7de1eb](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/d7de1eb7404f3d51c61a01724a37a32a3e108203))

### 🐛 Fixes

* **admin-ui:** QA report sweep — modal/table layout, i18n, sidebar polish ([239cecd](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/239cecd2426480d3a2237c7961b56064a0e41346)), closes [#1](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/1) [#2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/2) [#3](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/3) [#4](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/4) [#5](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/5) [#6](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/6) [#7](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/7) [#8](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/8)

### 📝 Docs

* **changelog:** fill 22-release gap v2.15.0 → v2.36.0 ([a600664](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/a6006642900bfc34e37554994f5394e6ff39398e))
2026-05-17 12:45:50 +00:00
Александр Зимин 37c9ae7040 Merge branch 'fix/qa-report-sweep' into 'main'
fix(admin-ui): QA report sweep — modal/table layout, i18n, sidebar polish

See merge request 2-6/2-6-4/terravault/ordinis!239
2026-05-17 12:37:29 +00:00
Zimin A.N. d7de1eb740 feat(graph): добавлен поиск по справочникам в Graph view
Top-left dark-glass input на странице /graph. Substring match
case-insensitive по name + displayName (admin помнит либо
technical id `antenna`, либо русское название `Антенны`).

UX:
- Matched nodes остаются в full opacity, остальные dim'ятся (0.25) —
  как при hover-focus. Force-layout не трогаем (positions стабильны).
- При единственном match → matched node получает focused border
  (orange accent + glow) — легко найти результат.
- Edges светятся только когда оба конца — match. Иначе dim.
- Counter chip `N из M` под input'ом + `ничего не найдено` для empty.
- Esc clears query и снимает focus. ✕-кнопка тоже clears (focus
  остаётся на input).
- Search dominates над hover dim — non-matched nodes остаются dim
  даже если hover их затрагивает.

i18n: graph.search.placeholder / .empty / .count для ru-RU и en-US.

Force-simulation не пересчитываем при изменении query — это намеренно.
Иначе nodes прыгали бы по экрану на каждый символ, ломая spatial
recognition (юзер уже привык где какой node лежит).
2026-05-17 15:36:06 +03:00
Zimin A.N. 239cecd242 fix(admin-ui): QA report sweep — modal/table layout, i18n, sidebar polish
Восемь дефектов из QA-отчёта одним MR.

#1 — Records-table теряла первую колонку после закрытия Create-modal.
Корень: lg:grid-cols-[220px_1fr] без minmax(0,…) — 1fr имел implicit
min-content (=max-content), позволяя table вырасти шире viewport.
После закрытия Radix Dialog снималось body padding-right (~15px), grid
пересчитывался и клиппил колонку под overflow-hidden card.
Fix: grid-cols-[220px_minmax(0,1fr)] + убран overflow-hidden с card
(Table primitive имеет свой overflow-x-auto).

#2 — Esc закрывал modal с задержкой (визуал/DOM рассинхрон).
duration-200 на slow CPU ощущался как несколько кадров между state=closed
и unmount. Снижено до duration-150 — совпадает с tw-animate-css дефолтами.

#3 — «+ Создать» обрезалось до «+ Соз» на ~1075px на detail-странице.
EditorTabBar: 6 tabs + 3 кнопки actions в одном flex row без shrink-0
давили друг друга. Решение: tabs в overflow-x-auto wrapper с min-w-0,
actions в shrink-0 контейнере — tabs скроллятся, actions всегда видны
полностью. scrollbar скрыт визуально (height-консистентность).

#4 — Обрезка названий в records-table при PUBLIC+INTERNAL.
Добавлен div.overflow-x-auto вокруг table + min-w-[760px] на table +
min-w-[180px] max-w-[320px] truncate на name cell. Catalog table также
получает min-w-[220px] на name column.

#5 — Заголовки колонок и кнопка «Граф» не локализованы на EN.
defaultValue:'Название' inline в коде падал на ru-text при EN locale.
Вынесены в i18n: dict.col.title/id/bundle/records/updated/outgoingFk/
incomingFk + dict.list.graph для обеих локалей.

#6 — Modal backdrop почти невидим.
bg-black/40 + 1px blur → bg-black/55 + 2px blur. Заметно темнее при
сохранении читабельности фона, фокус на modal.

#7 — Неестественная подсказка в форме «Создать»:
«Имя оставьте уникальное; версия сбросится на 1.0.0» →
«Укажите уникальное имя — версия сбросится на 1.0.0».

#8 — «Главная» в сайдбаре сбивала active state.
Клик на «Главную» вёл на `/` → redirect на /dictionaries → подсветка
уезжала на «Справочники». Logo сверху уже даёт home affordance, поэтому
duplicate nav item удалён (desktop + mobile).
2026-05-17 15:23:01 +03:00
semantic-release-bot 2fda3db547 chore(release): v2.36.0
## [2.36.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.35.0...v2.36.0) (2026-05-17)

###  Features

* **notifications:** Phase C — Don't Disturb (quiet hours window) ([74704db](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/74704dbf5b364fabacedcce66a8cce3b1ba2b28b))
2026-05-17 08:20:35 +00:00
Zimin A.N. 74704dbf5b feat(notifications): Phase C — Don't Disturb (quiet hours window)
User задаёт quiet window (e.g. 22:00–08:00 Europe/Moscow) — external
channels (email + express) DROP'аются dispatcher'ом. In-app bell badge
остаётся (юзер видит при следующем визите UI). Cross-midnight aware.

## Backend

- Migration 0026: user_notification_settings table (user_id PK + quiet_hours_start/end LocalTime + tz)
- Entity UserNotificationSettings + Repository (stock CRUD)
- UserPreferencesGate extended:
  - Constructor takes settingsRepo
  - allows() checks isQuiet(userId) первым for external channels (EMAIL/EXPRESS/TELEGRAM)
  - isQuiet: load settings → if hasEffectiveQuietHours() → check current LocalTime в user's TZ
  - inWindow() helper: cross-midnight semantics (22:00–08:00 wraps midnight)
  - Invalid TZ → fallback к DEFAULT_TZ (Europe/Moscow) с WARN log
  - Test seam: setClockForTesting(Clock) для deterministic tests
- MeNotificationsSettingsController: GET/PUT /api/v1/me/notifications/settings
  - HH:mm input parsing, IANA TZ validation, enabled=false clears window
  - Auth required (JWT sub claim)

## Frontend

- NotificationSettings type в client.ts
- useMyNotificationSettings query (60s stale)
- useUpdateNotificationSettings mutation
- QuietHoursSection в /me/notifications/preferences route:
  - Switch toggle + 2 time inputs + TZ select (12 RU/UTC zones)
  - Optimistic local state synced from query
  - Save button с pending/success/error states
- i18n RU + EN

## Tests

- 9 new tests UserPreferencesGateQuietHoursTest:
  - Same-day window match (inclusive start, exclusive end)
  - Cross-midnight window match (22:00–08:00)
  - No settings row → не блокирует
  - Quiet window active → email + express dropped
  - Outside window → email allowed
  - start == end → disabled (zero-duration treated as off)
  - Reviewer pool bypasses (always ON)
  - Invalid TZ → fallback default
  - Specific pref ON + quiet ON → quiet wins (drop)
- NotificationsDispatcherTest constructor updated к new signature
- Total notifications tests: 53 → 62, все green

## Design rationale (drop vs defer)

Drop chosen за defer (queue + re-fire scheduler) потому что:
- Defer needs persistent queue + scheduled retry — high complexity
- Most notifications time-relevant (draft N hours ago less actionable)
- Bell badge показывает full feed — ничего не «теряется»
- Phase D summary digest можно build later если demand surfaces
2026-05-17 11:11:21 +03:00
semantic-release-bot 6ed0b459de chore(release): v2.35.0
## [2.35.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.34.1...v2.35.0) (2026-05-17)

###  Features

* **bundle:** bundle hygiene v2 — manifest spec + signing verifier + docs ([8adfd39](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/8adfd39bff0e04017634f753c46ab9f7fc52f83d)), closes [#2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/2) [#2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/2) [#2](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/issues/2)
2026-05-17 07:48:10 +00:00
semantic-release-bot f93751629b chore(release): v2.34.1
## [2.34.1](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.34.0...v2.34.1) (2026-05-16)

### 🐛 Fixes

* **nginx:** route /api/v1/ai/* к writer (был fall-through к read-api → 500) ([c86ce8c](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/c86ce8c8d5966470d38e0e88e6c138070c8c327b))
2026-05-16 16:56:21 +00:00
Александр Зимин c3c3693c7f Merge branch 'fix/nginx-route-ai' into 'main'
fix(nginx): route /api/v1/ai/* к writer

See merge request 2-6/2-6-4/terravault/ordinis!235
2026-05-16 16:51:26 +00:00
Zimin A.N. c86ce8c8d5 fix(nginx): route /api/v1/ai/* к writer (был fall-through к read-api → 500)
После !234 ship'нул AiSchemaController на writer, frontend hit /api/v1/ai/info
получает 500 — generic /api/v1/ location route к read-api который не имеет
этих handlers. Тот же pattern fix что для /drafts/, /me/, /admin/ — explicit
location к writer перед generic /api/v1/ fallback.

Без этого AI feature unusable в staging (даже если backend healthy).
2026-05-16 19:51:04 +03:00
semantic-release-bot 4b1293b845 chore(release): v2.34.0
## [2.34.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.33.0...v2.34.0) (2026-05-16)

###  Features

* **ai:** Phase 1 step 3-6 — LLM adapter + per-field AI suggest endpoint + UI ([e4cd7b9](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/e4cd7b9709da01c55cfba7f82107eec55f346b1d))
2026-05-16 16:32:52 +00:00
Zimin A.N. e4cd7b9709 feat(ai): Phase 1 step 3-6 — LLM adapter + per-field AI suggest endpoint + UI
Approach C upgrade на templates foundation (!233). Admin может попросить AI
предложить новое поле — system prompt force'ит structural JSON Schema
fragment, admin review через accept/edit/reject. Bitemporal draft workflow
unchanged downstream.

## Backend
- `LlmAdapter` — OpenAI-compatible /v1/chat/completions client (Ollama,
  vLLM, OpenAI). Bean conditional на ordinis.ai.enabled=true.
- `AiSchemaService` — system prompt + 3 few-shot examples, JSON output
  parsing (с markdown fence stripping), validation (fieldName snake_case).
  In-memory circuit breaker: 10 fails в 60s → open 5 min.
- `AiSchemaController`:
  - GET /api/v1/ai/info → 200 (enabled) / 404 (disabled). Frontend probe.
  - POST /api/v1/ai/suggest-field → suggestion. Per-IP rate limit 30/min.
- All bean-conditional: ordinis.ai.enabled=false → controllers нет →
  endpoints 404 → frontend hides AI buttons automatically.

## Frontend
- `useAiFeatureAvailable` query — probes /ai/info, cache 5 min
- `useAiSuggestField` mutation — 30s timeout (LLM может быть slow)
- `AiFieldSuggestPanel` component — inline expandable: prompt → preview →
  accept/retry/reject. Error handling per HTTP status (503/429/422/502/404).
- DictionaryEditorDialog: AI toggle visible на schema tab когда
  aiAvailable=true. Accept wraps suggestion в mini-schema → parseSchemaJson
  (reuse existing kind-inference) → push в properties (overwrite если
  duplicate fieldName).
- i18n RU + EN

## Config
- application.yml: ordinis.ai.{enabled,endpoint,model,bearer-token,
  timeout-seconds}
- Helm writer.yaml: ORDINIS_AI_* env vars, optional secretRef для bearer
- values.yaml: ai.{enabled,endpoint,model,timeoutSeconds,bearerTokenSecretRef}
  defaults disabled

Pair с infra MR — values-staging enables AI с vortex.nstart.cloud Ollama
serving qwen2.5:7b.
2026-05-16 13:55:16 +03:00