Commit Graph

851 Commits

Author SHA1 Message Date
Zimin A.N. da0f894342 feat(auth): Phase 1b — swap default Keycloak realm на altum
Frontend default: https://auth.nstart.space/realms/nstarthttps://altum.nstart.cloud/auth/realms/altum. Shared SSO с altum portal.

Keycloak setup (Keycloak admin actions требуются ДО merge):
- В realm `altum` создать client `ordinis` (тип: public, standard flow)
- Redirect URIs:
    https://ordinis.nstart.cloud/*
    https://ordinis.k8s.265.nstart.cloud/*
    http://localhost:5173/*
- Web origins: те же
- Client-roles: ordinis-public, ordinis-internal, ordinis-restricted,
  admin (admin даёт RESTRICTED через ScopeContext.isAdminRole)

Backend issuerUri меняется отдельным MR в ordinis-infra
(charts/ordinis/values.yaml).

После настройки Keycloak + деплоя — старый realm auth.nstart.space/nstart
больше не используется. Все юзеры должны иметь учётки в realm altum
(вероятно brokered к nstart-id если SSO с New Start ID настроен).

171/171 tests, TSC чистый.
2026-05-26 13:11:49 +03:00
semantic-release-bot 6e5e64d558 chore(release): v2.43.0
## [2.43.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.6...v2.43.0) (2026-05-26)

###  Features

* **auth:** миграция oidc-client-ts → keycloak-js (altum-pattern) ([7e435e0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/7e435e07b7eb532dd99c03e02d9e76ecbcf9f451))
2026-05-26 08:37:11 +00:00
Александр Зимин aaae04b4ca Merge branch 'feat/keycloak-js-altum-style-auth' into 'main'
feat(auth): миграция oidc-client-ts → keycloak-js (altum-pattern, Phase 1a)

See merge request 2-6/2-6-4/terravault/ordinis!270
2026-05-26 08:31:42 +00:00
Zimin A.N. 7e435e07b7 feat(auth): миграция oidc-client-ts → keycloak-js (altum-pattern)
Phase 1a из плана: только swap библиотеки, realm/URL остаются на
auth.nstart.space/realms/nstart. Phase 2 (intgr с altum auth-service)
— отдельный эпик.

Зачем. oidc-client-ts с automaticSilentRenew триггерил бесконечный
POST /token loop когда refresh_token истекал (см. MR !269 revert,
~30+ 400'к в console, страница залипала на 403). Altum давно ушёл
с oidc-client-ts на keycloak-js по той же причине.

Новая модель.
- src/lib/keycloak.ts — singleton + initKeycloak(check-sso, PKCE) +
  ensureFreshToken(N) wrapper над kc.updateToken(). Зеркало
  altum/src/lib/keycloak.ts.
- src/stores/authStore.ts — Zustand store: {user, isAuthenticated,
  isLoading, error}. checkAuth() ждёт initKeycloak, wireKeycloakEvents()
  → onAuthSuccess/Refresh/Logout/TokenExpired re-снэпшотят store.
- src/auth/useAuth.ts — compat shim для react-oidc-context API
  (auth.user.profile, isAuthenticated, signinSilent, signinRedirect,
  signoutRedirect, removeUser, error). Все 11 callsites продолжают
  работать без правок (только сменили путь import'а).
- src/api/client.ts — request interceptor дёргает ensureFreshToken(30)
  ПЕРЕД каждым запросом + attaches Bearer ${getToken()}. 401 interceptor
  делает ensureFreshToken(0) + retry один раз. Никаких таймеров, никаких
  setAccessToken/setUnauthorizedHandler holder'ов — keycloak-js сам source
  of truth.
- src/main.tsx — убран <AuthProvider>, <TokenSync />. Просто
  useAuthStore.getState().checkAuth() на старте, потом обычный render.
- public/silent-check-sso.html — endpoint для silentCheckSsoRedirectUri.

Удалены.
- src/auth/TokenSync.tsx — не нужен, ensureFreshToken на запросах.
- src/auth/oidcConfig.ts — не нужен, конфиг внутри lib/keycloak.ts.
- npm deps: react-oidc-context, oidc-client-ts.

Добавлены deps: keycloak-js@26.2.4, zustand@5.0.13.

Tests: 171/171 passed, TSC чистый.

Migration callsites — все unchanged по семантике благодаря compat shim.
2026-05-26 11:18:30 +03:00
semantic-release-bot 273393cc7e chore(release): v2.42.6
## [2.42.6](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.5...v2.42.6) (2026-05-26)

###  Reverts

* уберу proactive re-auth и 403→reauth — спам Keycloak /token ([0aaae9c](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/0aaae9cb2b53c00d6c7e7cae08f8904c97f118e0))
2026-05-26 08:03:18 +00:00
Александр Зимин b45eaa6ae9 Merge branch 'revert/auth-reauth-loop' into 'main'
revert: убрать proactive re-auth + 403 reauth — спам Keycloak /token

See merge request 2-6/2-6-4/terravault/ordinis!269
2026-05-26 07:57:22 +00:00
Zimin A.N. 0aaae9cb2b revert: уберу proactive re-auth и 403→reauth — спам Keycloak /token
Симптом: пользователь видел ~30+ POST на Keycloak /token endpoint
с 400 Bad Request, страница залипала.

Корень. MR !267 добавил useEffect в TokenSync который на
auth.user?.expired === true делал signinSilent → catch → signinRedirect.
MR !268 добавил interceptor триггер на 403 + !accessToken.

Цикл:
1. token expires, refresh_token тоже (Keycloak SSO Session Idle прошёл)
2. useEffect видит expired=true → signinSilent → POST /token с refresh
3. Keycloak возвращает 400 invalid_grant (refresh expired)
4. catch → signinRedirect, но oidc-client-ts automaticSilentRenew=true
   параллельно тоже пытается → ещё POST /token → ещё 400
5. signinRedirect не успевает редиректнуть до следующего render'а
6. reauthing.current=false → useEffect fire'ит снова → новый signinSilent
7. → ещё 400, и так в loop ~30 раз пока что-то не разрулит

Откатываю обе ветки. Возвращаемся к v2.42.3 поведению: silent renew
автоматически пробует один раз, на failure пользователь видит 403
(или stale data из cache) и делает relogin вручную. Не идеально, но
не спамит Keycloak и не залипает на endlessly.

Backend ScopeContext admin → RESTRICTED (MR !267 backend часть)
остаётся — это безвредная правка, помогает юзерам с admin ролью
без явного INTERNAL.
2026-05-26 10:56:11 +03:00
semantic-release-bot 465a37836c chore(release): v2.42.5
## [2.42.5](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/compare/v2.42.4...v2.42.5) (2026-05-25)

### 🐛 Fixes

* **admin-ui:** 403 + отсутствует accessToken → trigger reauth handler ([a1e83a8](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/commit/a1e83a86bc22988e66443b81f011f6359754053a))
2026-05-25 16:26:05 +00:00
Александр Зимин 788bfadd5c Merge branch 'fix/api-403-noauth-reauth' into 'main'
fix(admin-ui): 403 без токена → reauth (продолжение MR !267)

See merge request 2-6/2-6-4/terravault/ordinis!268
2026-05-25 16:20:45 +00:00
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
Александр Зимин 6ad83f10fd Merge branch 'fix/admin-role-scope-restricted' into 'main'
fix(auth): admin → RESTRICTED + TokenSync proactive re-auth при expired

See merge request 2-6/2-6-4/terravault/ordinis!267
2026-05-25 15:58:09 +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
Zimin A.N. c5adbdf39c fix(auth): роль admin даёт RESTRICTED scope (видит всё)
ScopeContext.normalizeRoleToScope ищет суффикс PUBLIC|INTERNAL|RESTRICTED
после `-`/`_`/`:`. Роль `admin` (и `ordinis-admin`, `ordinis:client:admin`)
не match'илась — нет суффикса с known scope name → DataScope.valueOf("ADMIN")
бросал IllegalArgumentException → ignored → юзер с одной только admin
ролью получал PUBLIC scope → 403 на любом /admin/* endpoint'е с requireInternal().

Симптом на проде/staging: админ открывает /audit, видит 403 на /admin/audit
и /admin/users несмотря на JWT с admin ролью.

Fix: processRole() сначала проверяет isAdminRole (tail == "ADMIN" после
prefix-stripping и separator-split) → даёт RESTRICTED (RESTRICTED visibleTo
PUBLIC + INTERNAL + RESTRICTED). Иначе идёт обычный normalizeRoleToScope.

Поддерживаются: admin, ADMIN, ordinis-admin, ordinis_admin, ordinis:client:admin,
ANYTHING-admin, ANYTHING_ADMIN — короче, любая роль с tail'ом ADMIN.

Tests: +4 новых кейса (admin alone, ordinis-dash-admin, colon-separated admin,
admin + explicit scope unionize). Старый unrecognizedRolesGivePublicFallback
оставил только "viewer" — admin теперь recognized.

17/17 ScopeContextTest passed.
2026-05-25 18:51:43 +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
Александр Зимин b34fab8a7b Merge branch 'fix/user-display-skip-anonymous' into 'main'
fix(admin-ui): UserCell — не резолвить «anonymous» через display endpoint

See merge request 2-6/2-6-4/terravault/ordinis!266
2026-05-25 15:34:58 +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
Александр Зимин 70235b69dc Merge branch 'fix/user-picker-hooks-order' into 'main'
fix(admin-ui): UserPicker — порядок хуков (rules-of-hooks)

See merge request 2-6/2-6-4/terravault/ordinis!265
2026-05-25 15:15:20 +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
Александр Зимин ff076550eb Merge branch 'fix/day-picker-ordinis-styles' into 'main'
fix(admin-ui): /audit — стили DayPicker + UserPicker fallback на TextInput

See merge request 2-6/2-6-4/terravault/ordinis!264
2026-05-25 14:51:12 +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
Александр Зимин 59a6537d7f Merge branch 'feat/audit-period-presets' into 'main'
feat: /audit — кастомные фильтры (период presets + user typeahead)

See merge request 2-6/2-6-4/terravault/ordinis!263
2026-05-25 14:25:15 +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
Александр Зимин f5761a1f64 Merge branch 'fix/ci-retag-fixed-sha' into 'main'
fix(ci): retag — фиксированный 8-char SHA вместо git %h (анти-race)

See merge request 2-6/2-6-4/terravault/ordinis!262
2026-05-25 13:05:32 +00:00
Zimin A.N. 768651528c fix(ci): retag — фиксированный 8-char short-SHA вместо git %h
Docker push в main pipeline тегирует образ как main-${CI_COMMIT_SHORT_SHA}
(GitLab гарантирует 8 chars). Retag в tag pipeline получал PARENT_SHA
через `git log -1 --format=%h HEAD~1` — а %h динамически растягивает
abbrev (8 → 9 chars) когда git детектит SHA-коллизию в локальном clone'е.

При расхождении retag тянет main-3d3a03ab9 → "failed to resolve
reference ... not found" → retag job failed → commit-prod-image-bump
skipped (через needs:) → v2.41.3 (MR !261) попала на эту race.
Часть retag-* выживала только потому что на их runner'е clone оказался
shallower и %h дал 8 chars; параллельные jobs получали 9 → fail.

Заменил на `git rev-parse HEAD~1 | cut -c1-8` — гарантированно 8 chars,
матчится с CI_COMMIT_SHORT_SHA в docker pipeline.
2026-05-25 16:02:55 +03: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
Александр Зимин 712866a178 Merge branch 'fix/sidebar-mydrafts-badge-status-filter' into 'main'
fix(admin-ui): sidebar badges — actionable only

See merge request 2-6/2-6-4/terravault/ordinis!260
2026-05-25 12:31:23 +00: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
Александр Зимин 8e0272a82e Merge branch 'fix/reviews-dict-uuid-to-name' into 'main'
fix(admin-ui): reviews+sidebar polish (6 фиксов одной пачкой)

See merge request 2-6/2-6-4/terravault/ordinis!259
2026-05-25 12:04:12 +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
Александр Зимин 9418784ca4 Merge branch 'feat/mission-spacecraft-codes-fk' into 'main'
feat(cuod): FK-уборка — country, industry_sector, spacecraft, frequency_band

See merge request 2-6/2-6-4/terravault/ordinis!258
2026-05-25 11:22:15 +00:00
Zimin A.N. cd5ee1fcf6 feat(cuod): остальные FK — country, industry_sector, frequency_band
Pass 2 по тому же паттерну. Все target-словари существуют, FK-валидация
на бэке уже работает — просто помечаем поля чтобы admin-UI рендерил
picker вместо free-text.

Изменения:
- mission.country               → x-references country.code
- spacecraft.country            → x-references country.code
- consumer.industry_sector      → x-references industry_sector.code
- consumer.country              → x-references country.code
- antenna.supported_bands.items → x-references frequency_band.code

Schema bumps:
- spacecraft 2.0.0 → 2.1.0
- mission    1.1.0 (без доп. bump'а — country добавляется в этот же релиз)
- consumer   1.0.0 → 1.1.0
- antenna    1.0.0 → 1.1.0
- bundle     1.3.4 → 1.3.5

Skip: spacecraft.spectrum_bands (enum смешивает оптику и радиодиапазоны —
нет одного словаря-target'а; разделение на два поля — отдельная задача).
2026-05-25 14:18:34 +03:00
Zimin A.N. fd7d7d4535 feat(cuod): mission.spacecraft_codes — FK на spacecraft.designator
Зеркало правки в instrument.spacecraft_codes (MR !257). Список КА программы
теперь рендерится в admin-UI как MultiSelect с поиском по designator
(2014-037A — Ресурс-П №2) вместо comma-separated «businessKey'и из spacecraft».

mission schemaVersion 1.0.0 → 1.1.0, bundle 1.3.3 → 1.3.4.
bundleUpdateExistingSchema=true (MR !90) докатит schema_json на рестарте writer'а.
2026-05-25 14:14:44 +03: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
Александр Зимин 028c9bd637 Merge branch 'feat/array-fk-multi-picker' into 'main'
feat(admin-ui,cuod): array-FK picker для instrument

See merge request 2-6/2-6-4/terravault/ordinis!257
2026-05-25 10:39:00 +00:00