Commit Graph

182 Commits

Author SHA1 Message Date
Zimin A.N. 353130c529 feat(admin-ui): group dictionaries by scope with color accent
40 справочников в плоском grid тяжело сканировать. Разделили на секции
PUBLIC / INTERNAL / RESTRICTED с цветными accent-полосами слева у карточек
(emerald / amber / rose). Search фильтрует across all sections, пустые
секции скрыты. Цвет даёт instant scan, секции — explicit разделение.
2026-05-06 12:43:01 +03:00
Zimin A.N. 171d049fc5 feat(admin-ui): поиск справочников + счётчик активных записей
Backend:
- DictionaryRecordRepository.countActiveGroupedByDictionary — один GROUP BY
  по всем справочникам с фильтром по scope, без N+1.
- DictionaryResponse.recordCount (nullable) + factory from(d, count).
- DictionaryDefinitionController передаёт счётчики в list/get,
  фильтрованные по currentScopes() пользователя.

Frontend:
- SearchInput над сеткой карточек, client-side filter по
  name/displayName/description (case-insensitive, useDeferredValue).
- Badge "N записей" (i18n plural ru/en) в углу карточки.
- "Показано N из M" рядом с поиском.
- EmptyState когда поиск пустой.
2026-05-06 08:46:45 +03:00
Zimin A.N. ab6f6cb860 feat(admin-ui): RequireAuth gate + RTL component тесты
RequireAuth: гейтит весь UI по аутентификации. Anonymous юзер вместо
красного "AxiosError 401" получает immediate signinRedirect на Keycloak;
authenticated юзер с истёкшим токеном получает silent renew → fallback на
full redirect. Guards против infinite loop:
- auth.isLoading / activeNavigator — bootstrap или signin/signout уже идёт
- ?error= в URL — вернулись с провалом из Keycloak, показываем сообщение
- auth.error без isAuthenticated — Keycloak недоступен, показываем сообщение

main.tsx: RequireAuth обёрнут вокруг Router/Query, чтобы tan-stack-router и
React Query не делали запросов до auth (иначе 401-каскад на старте).

TokenSync: 401 interceptor теперь редиректит и anonymous (раньше срабатывал
только для authenticated с истёкшим токеном), с теми же guards.

RTL component тесты — 22 теста на 3 ключевых компонента:
- SchemaBuilder (7): empty state, add/remove/reorder properties
- PropertyEditor (10): kind change, required/unique toggle, kind-specific
  поля (string→pattern, integer→min/max, enum→values, array_string→uniqueItems),
  move up/down disabled на boundaries
- SchemaDrivenForm (5): identity tab с required, переключение на extra,
  isPending → save disabled, cancel callback, serverError → Alert

Setup:
- vite.config.ts: test block (jsdom env, setup file, css off)
- src/test/setup.ts: jest-dom matchers, cleanup, HTMLCanvasElement.getContext
  stub (lottie-web из @nstart/ui крашит в jsdom без canvas)
- src/test/render.tsx: renderWithI18n helper, форсит ru-RU
  (LanguageDetector в node по умолчанию даёт en-US)

Deps: +@testing-library/react +@testing-library/jest-dom
+@testing-library/user-event +jsdom
2026-05-06 00:11:07 +03:00
Zimin A.N. 11faf086a4 perf(admin-ui): manualChunks splits vendor по domain → лучше кэширование
До: 1 chunk index-XXX.js 2 МБ / 703 КБ gzip — любой commit инвалидирует весь.

После: 10 chunks. Vendor стабилен и кэшируется, при правке кода только
main (18 КБ gz) перезагружается:

  vendor-react       194 / 60 gz   (rare changes)
  vendor-tanstack    127 / 40 gz
  vendor-forms       145 / 46 gz   (react-hook-form + ajv)
  vendor-nstart-ui  1229 /465 gz   (corp UI — brand assets + stickers)
  vendor-icons        62 / 13 gz   (phosphor)
  vendor-auth         72 / 19 gz   (oidc-client-ts + react-oidc-context)
  vendor-i18n         56 / 18 gz
  vendor-net          42 / 17 gz   (axios + crypto)
  vendor-misc         15 / 6 gz
  index (main)        67 / 19 gz   ← наш код

vendor-nstart-ui всё ещё 465 КБ gz — доминирует. Это brand assets
(stickers, lottie animations) которые eager-imported в библиотеке.
Дальнейший win требует tree-shaking в @nstart/ui — отдельная задача.
2026-05-05 17:32:33 +03:00
Zimin A.N. cef78f9971 feat(admin-ui): SSO через Keycloak (PKCE, public client)
react-oidc-context + oidc-client-ts. Confidential client тоже работает —
SPA просто не использует client_secret.

- src/auth/oidcConfig.ts — UserManager settings (authority, redirect_uri,
  PKCE, automaticSilentRenew). VITE_KEYCLOAK_{URL,REALM,CLIENT_ID} с дефолтами
  на nstart realm + ordinis client.
- src/auth/TokenSync.tsx — синкает access_token в localStorage и axios default
  header после login/refresh. Existing API код не трогали — interceptor уже
  читает localStorage.
  Bonus: 401 interceptor триггерит signinSilent → signinRedirect fallback.
- src/auth/AuthBadge.tsx — header'ный бейдж: "Войти" если anonymous, имя +
  logout если залогинен.
- main.tsx обёрнут в <AuthProvider>.
- __root.tsx показывает AuthBadge рядом с language switch.
- .env.example с конфигом Keycloak.
- i18n auth.login / auth.logout (RU + EN).

Keycloak client `ordinis` — настроить:
  Valid redirect URIs: https://ordinis.k8s.265.nstart.cloud/*,
                       https://ordinis.k8s.264.nstart.cloud/*,
                       http://localhost:5173/*
  Web Origins: те же + (или "+"), PKCE Code Challenge: S256
2026-05-04 18:28:31 +03:00
Zimin A.N. 5ffb51e119 fix(admin-ui/nginx): proxy /admin/* и /swagger-ui|v3/api-docs на writer
После добавления audit/outbox admin endpoints + Swagger UI они оказались за
SPA-fallback'ом — nginx отдавал index.html вместо проксирования на writer.

- /api/v1/admin/* (AuditAdminController + OutboxAdminController) → writer.
- /swagger-ui*, /v3/api-docs*, /swagger-resources, /webjars/swagger-ui →
  writer (там springdoc сидит).
2026-05-04 15:54:43 +03:00
Zimin A.N. 7b2fe6f2d5 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).
2026-05-04 15:44:14 +03:00
Zimin A.N. f7efb23e09 test+ci: 51 frontend unit-тестов (Vitest) + CI rollback на single resolve-tag
Tests (Vitest):
- src/lib/dates.ts (вынесено из SchemaDrivenForm + dictionaries.$name): parseFormDate,
  formatIsoDate, formatIsoDateTime, extractTime, combineDateTime, localTzOffset, nowIsoLocal
- src/lib/dates.test.ts (20 tests): TZ-safety, pure-date local midnight, far-future filter,
  round-trip parse/format, time extraction в local TZ, clear-button (null Date → '')
- src/components/schema/types.test.ts (31 tests): buildSchemaJson всех 9 kinds + x-unique +
  x-id-source, parseSchemaJson roundtrip, diffSchemas (8 breaking detection cases),
  suggestVersionBump (major/minor/patch + breaking-wins-mix)

CI:
- Откатил dual resolve-tag (BACKEND/FRONTEND) на single resolve-tag.
  GitLab variable interpolation в trigger jobs не подхватывает dotenv от needs-job
  → admin-ui-only push не доезжал до infra. Now backwards compat path работает
  как раньше с UPSTREAM_IMAGE_TAG.
- Backend docker jobs (maven-package + 4 docker-*) теперь без rules:changes —
  catch ImagePullBackOff если frontend push шёл без backend rebuild. BuildKit cache
  делает повторный билд идентичных layers ~30 sec.
- Path-filter оставлен на maven-test (тесты не запускаются на frontend-only PR
  — они бэкендовые) и на docker-admin-ui (frontend-only).

Run: pnpm test → 51 passed.
2026-05-04 04:37:24 +03:00
Zimin A.N. 14d32d93c3 fix(admin-ui): корректные бейджи в истории — future/active/expired по реальному времени
Раньше любую запись с validTo<9999 помечал 'закрыта'. Но запись с validTo в
будущем — не закрытая, а либо ЗАПЛАНИРОВАННАЯ (validFrom > now), либо АКТИВНАЯ
(validFrom <= now < validTo). User видел 'закрыта' на v3 которая ещё не настала.

Теперь:
- future:  validFrom > now → бейдж 'запланирована' (info), точка orbit/horizon
- active:  validFrom <= now < validTo → бейдж 'активна' (success) если не latest
- expired: validTo <= now → 'закрыта' (neutral), точка пустая
- latest active: 'текущая' (primary), точка ultramarain

Бессрочная запись (validTo year>=9999) больше не показывает 'действует до:' —
у бессрочной нет конечной даты, отображение его смущало. i18n: новые ключи
history.active / history.scheduled (RU/EN).
2026-05-04 04:27:44 +03:00
Zimin A.N. 303a5f0402 feat(ci+ux): path-filtered CI build + Edit semantics в bitemporal model
CI: path-filtering (наконец-то)
- Backend jobs (maven-test, maven-package, docker-app/read-api/projection-writer/migrations,
  resolve-backend-tag) запускаются только при backend-changes (Java модули, pom.xml, CI yml).
- Frontend (docker-admin-ui, resolve-frontend-tag) — только при ordinis-admin-ui/**.
- Trigger downstream передаёт BACKEND_IMAGE_TAG и/или FRONTEND_IMAGE_TAG. Backwards
  compat для старого UPSTREAM_IMAGE_TAG сохранён в infra pipeline.
- Frontend-only PR теперь не катит backend rolling update (raньше каждый push
  тегал ВСЕ сервисы новым sha и роллил).

Edit dialog UX (bitemporal):
- При Edit validFrom default = now() (а не historical validFrom существующей записи).
  Раньше юзер сдвигал validFrom назад → backend не находил активной версии в этот
  момент → 'record_not_active'. Теперь по умолчанию 'новая версия с этого момента'.
- nowIsoLocal() в timezone браузера (с offset).
- validTo default пустой = бессрочно.

Backend: понятное сообщение для record_not_active с инструкцией.
2026-05-04 04:24:17 +03:00
Zimin A.N. c7b651011a feat(ux): улучшение работы с датами validFrom/validTo
Backend (defensive):
- GlobalExceptionHandler ловит HttpMessageNotReadableException → 400 c понятным
  сообщением вместо 500 'Internal server error'.
- DateTimeParseException → code='invalid_date_format', сообщение с подсказкой
  ISO формата (например 2026-05-08T14:30:00+03:00).
- InvalidFormatException → code='invalid_field_format' с именем сломанного поля.
- Раньше падало 500 если фронт отправлял 'YYYY-MM-DD' вместо OffsetDateTime
  (из-за browser cache на старом bundle).

Frontend:
- Submit-валидация validFrom < validTo с inline error на validTo поле,
  переключение на таб 'Идентификация' с фокусом на ошибке.
- DateTimeField теперь принимает hint и error props и показывает их под
  парой DatePicker+TextInput[time].
- Подсказки i18n под полями: 'Когда запись становится активной. Пусто = с момента
  создания.' / 'Когда перестаёт действовать. Пусто = бессрочно.'
- Новая i18n ключ form.error.validToBeforeFrom (RU/EN).
2026-05-04 04:15:58 +03:00
Zimin A.N. b5c2378ab7 fix(admin-ui): нумерация версий в истории по позиции, не по rec.version
rec.version — JPA @Version (optimistic lock counter). Растёт на UPDATE, поэтому
у нескольких закрытых исторических версий может оказаться одинаковое значение
(была 0, стала 1 при close, новая запись 0, потом 1, итого две закрытые с v=1).
В UI это выглядит как дубли v1, v1.

Используем индекс позиции в timeline: новая = data.length, старая = 1.
Отображение: v3 (текущая) → v2 → v1.
2026-05-04 04:11:04 +03:00
Zimin A.N. 07c8a19cf3 fix(admin-ui): pure-date парсинг как local midnight (защита TZ западнее UTC)
format=date поля приходят как 'YYYY-MM-DD' без времени. new Date(s) интерпретирует
их как UTC-midnight. В TZ западнее UTC (UTC-5 etc.) local Date уезжает на день
назад → getDate() возвращает (день-1).

Сейчас pure-date строки регексим и собираем через Date(y, m-1, d) — всегда
local midnight. Datetime строки парсятся как раньше.
2026-05-04 04:01:20 +03:00
Zimin A.N. 80703f9caa fix(admin-ui): timezone bug в validTo/validFrom + кнопка сброса DatePicker
Bug 1: ставил 8 мая, показывало 9.
Раньше formatIsoDateTime отдавал 'YYYY-MM-DDTHH:MM:00Z' (UTC). На load
new Date(s) парсил UTC и показывал в МСК (UTC+3) → +3 часа → день вперёд.
Сейчас:
  - localTzOffset(d) вычисляет offset из браузера, например '+03:00'
  - formatIsoDateTime(d, time) собирает Date в local TZ и отдаёт ISO с
    явным offset: '2026-05-08T23:59:00+03:00'
  - extractTime(s, fallback) парсит через new Date + getHours/getMinutes,
    показывая time в local TZ юзера

Bug 2: кнопка сброса (X) в DatePicker не очищала validFrom/validTo.
combineDateTime(null) возвращал current value вместо ''. Фикс — на null
возвращаем пустую строку, value уходит в form state как ''.
2026-05-04 03:59:53 +03:00
Zimin A.N. 0e12a254c2 fix(admin-ui): datetime поля внутри schema теряли время — теперь DateTimeField
Поля с format=date-time в schema (например orbit.epoch у spacecraft) рендерились
через DatePicker и форс-сетили T00:00:00Z. Теперь та же DateTimeField (DatePicker
+ time input) что и для validFrom/validTo. format=date по-прежнему чистый
DatePicker без времени.
2026-05-04 03:42:11 +03:00
Zimin A.N. baf76ac6e0 feat(admin-ui): D-pack — history, breaking-change detection, x-unique, x-id-source, datetime
History:
- Drawer на детальной странице записи (timeline всех версий через GET /records/{key}/history)
- Иконка ClockCounterClockwise в actions колонке таблицы записей
- Каждая версия показывает v(version), validFrom/To, updatedBy + expand JSON

Schema diff (breaking detection):
- Util diffSchemas() сравнивает старую/новую схему и классифицирует изменения:
  breaking (removed/typeChanged/becameRequired/enumRemoved/maxLengthTightened/
  patternChanged/newRequiredField), compatible (added optional/enumAdded/
  becameOptional), metadata (description)
- В DictionaryEditorDialog при breaking → Alert с детальным списком + Save заблокирован
- При compatible → Alert info со списком
- suggestVersionBump: major для breaking, minor для compatible field changes,
  patch для metadata-only

x-unique:
- Чекбокс «Уникальное» на PropertyEditor → emit x-unique: true в schema
- Backend enforcement позже — отдельный PR с partial unique index

x-id-source:
- SingleSelect в Метаданных «Использовать как ID» → emit x-id-source на schema root
- В SchemaDrivenForm если установлен — businessKey input скрывается, на submit
  derived из data[idSource]
- Подсказка с именем поля

DateTime для validFrom/validTo:
- Заменил DatePicker (date-only) на DateTimeField компонент: DatePicker + TextInput
  type=time рядом
- Default time: validFrom=00:00, validTo=23:59 (запись действует до конца дня
  иначе зануляется в полночь)
- Поддерживает 2-часовые интервалы (запуск КА в 14:30 → сход в 02:15 etc.)

Bug fix:
- parseSchemaJson изменил сигнатуру с PropertyDef[] на {properties, idSource},
  старый код в DictionaryEditorDialog падал "{} is not iterable". Поправлено
  через destructuring const parsed = parseSchemaJson(...).

i18n: 25+ новых ключей (RU/EN) — schema.unique, schema.idSource, schema.diff.*,
history.*, form.idSourceNote.
2026-05-04 03:38:21 +03:00
Zimin A.N. 0289cf5ff2 fix(admin-ui): отправляем валидный ISO datetime в validFrom/validTo + format=date-time
500 при сохранении записи — backend ждёт OffsetDateTime в DTO CreateRecordRequest,
а DatePicker отдавал просто 'yyyy-MM-dd'. Добавил helpers:
  - formatIsoDateTime(d) → 'yyyy-MM-ddT00:00:00Z'
  - ensureIsoDateTime(s) → нормализует submit-значение validFrom/validTo

Применил:
  - validFrom/validTo на submit нормализуются (если YYYY-MM-DD → +T00:00:00Z)
  - format=date-time data поля отдают datetime ISO, format=date — по-прежнему YYYY-MM-DD

Также добавил скаффолд для D-pack (history drawer + schema diff util) — не
интегрировано в роуты, доделаем в следующем коммите.
2026-05-04 03:29:17 +03:00
Zimin A.N. 987559a6ab feat(admin-ui): visual schema builder для создания/редактирования справочников
Новый функционал — заводить свои справочники через UI без code deploy:

API:
- CreateDictionaryRequest type в client.ts
- useCreateDictionary / useUpdateDictionary mutations с invalidate
  ['dictionaries'] и ['dictionary', name]

Компоненты (src/components/schema/):
- types.ts: PropertyDef + buildSchemaJson() (Draft-07) + parseSchemaJson()
  для двусторонней конвертации между UI state и schema_json
- PropertyEditor: name + kind select + required + description + type-specific
  опции (minLength/maxLength/pattern для string; minimum/maximum для number;
  enum values; uniqueItems для array). Up/Down/Trash icon buttons
- SchemaBuilder: список property cards + кнопка «добавить» + EmptyState
- DictionaryEditorDialog: модалка с 3 табами:
  * Метаданные (name/displayName/description/scope/bundle/version/locales)
  * Поля (SchemaBuilder)
  * JSON (readonly preview сгенерированной schema_json)

Поддерживаемые типы полей: string, integer, number, boolean, enum,
localized (i18n object с patternProperties), date (format=date),
datetime (format=date-time), array_string (с опц. enum + uniqueItems).

Wiring:
- /dictionaries: PageHeader actions = «+ Создать справочник» → Dialog в режиме create
  → onSuccess navigate на /dictionaries/{name}
- /dictionaries/$name: PageHeader actions расширен «Схема» (secondary) +
  «Создать запись» (primary). Dialog в edit-режиме prefilled из
  detailQuery.data + parseSchemaJson()

i18n: 30+ новых ключей в schema.* / scope.* (RU/EN)
2026-05-04 03:12:56 +03:00
Zimin A.N. ef31499bd9 fix(admin-ui): DatePicker + nested object + raw record fetch для Edit
Несколько связанных багов в форме записей:

DatePicker:
- launch_date / decay_date (format=date) и validFrom/validTo рендерились как
  raw text input → теперь @nstart/ui DatePicker с RU локалью.
- DatePickerProvider добавлен в main.tsx с DATE_PICKER_LOCALE_RU.
- Far-future infinity dates (year>=9999) скрываются (пустое поле).
- ISO date format на submit: yyyy-MM-dd.

Nested object → JsonField fallback:
- orbit (type=object с raan_deg/eccentricity/...) и antennas (array of object)
  показывали [object Object] в text input. Добавлен JsonField — TextArea
  с JSON.stringify/parse, локальный state для невалидного промежуточного
  ввода + inline parse error.

Edit raw record fetch:
- Read-api flatten'ит i18n в строку выбранной локали → form для edit получал
  data.name = 'Terra' вместо {ru-RU, en-US}. На Edit теперь fetch raw record
  через writer GET /api/v1/dictionaries/{name}/records/{businessKey}.
- nginx regex location ~ ^/api/v1/dictionaries/ → writer (вместо узкого regex
  только для /dictionaries/{name}). GET schema, GET raw record, mutations —
  всё на writer. Read-api держит /api/v1/{name}/records/* (другой URL pattern).
- LoadingBlock пока тащим raw запись + Alert на ошибке.
2026-05-04 03:01:08 +03:00
Zimin A.N. ff9e6b26b4 feat(admin-ui): wizard tabs in record form (no internal scroll)
Поля авто-распределяются по 3 табам:
- Идентификация: businessKey + validFrom/validTo + required scalar поля
- Описание: все x-localized поля (i18n)
- Дополнительно: optional scalar поля

UX:
- Все табы mounted (через hidden) — RHF state сохраняется при переключении.
- Бейдж с числом ошибок на каждом табе (variant=error).
- На submit: ajv валидация → если есть ошибки, форма автопереключает на таб
  первой ошибки + setError по всем затронутым полям.
- Таб «Описание» / «Дополнительно» скрывается если нет соответствующих полей.

Дроп FormSection — табы заменяют секционирование. Layout стал плоский
(Tabs + grid 1/2 cols + FormActions).
2026-05-04 02:50:12 +03:00
Zimin A.N. 43a06d1a7b fix(admin-ui): translate field labels + responsive header + modal max-h
Issues:
- Field labels (ORBIT/STATUS/COUNTRY/MASS_KG/...) показывались raw'ом потому
  что backend schema не присылает title. Добавил humanize() (snake_case → 'Snake Case')
  + i18n fallback t('field.$key') для известных полей spacecraft/satellite_type/
  ground_station: name, designator, norad_id, status, country, operator, mass_kg,
  mission, orbit, launch_date, decay_date, spectrum_bands, satellite_type_code, etc.
- Modal panel рос в высоту бесконечно (nstart-ui Modal не клампит max-h),
  на iPad портрете форма уходила за viewport. panelClassName=max-h-[calc(100vh-2rem)]
  flex flex-col + bodyClassName=overflow-y-auto flex-1.
- Header в __root: padding/gap responsive (px-4 sm:px-6, py-3 sm:py-4, gap-3 sm:gap-6),
  flex-wrap чтобы LanguageSwitch на узких экранах переносился под навигацию.
- Main container: px-4 sm:px-6 + py-4 sm:py-8.
2026-05-04 02:47:06 +03:00
Zimin A.N. 7aed67fce5 fix(admin-ui): IconButton icon prop + compact 2-col form layout
Bugs:
- IconButton @nstart/ui принимает icon={} prop, не children — иконки рендерились
  пустыми квадратами (видно «Изменить» tooltip без значка карандашика).
  Перевёл оба IconButton на icon={<PencilSimpleIcon/>} / icon={<XCircleIcon/>}.

UX:
- Modal max-w-2xl → max-w-4xl (форма дышит, ru-RU/en-US labels не оборачиваются).
- SchemaDrivenForm: scalar поля теперь в 2-колоночной grid, full-width
  (i18n / array / textarea / nested object) занимают sm:col-span-2.
- Локальные ярлыки i18n: tracking-normal + whitespace-nowrap, w-14 — больше не
  ломаются «ru-» «RU» из-за inherited tracking-label.
2026-05-04 02:37:13 +03:00
Zimin A.N. 4f04872de8 fix(admin-ui): COPY .npmrc + pnpm-lock.yaml в Dockerfile build stage
pnpm install в Docker падал т.к. .npmrc отсутствовал — @nstart/* резолвились
в public npm registry (404). Добавил .npmrc + pnpm-lock.yaml в первый COPY до
install, чтобы scope mapping на repo.nstart.cloud сработал.

Заодно --frozen-lockfile вместо --no-frozen-lockfile (lockfile в репо есть,
CI воспроизводимый install).
2026-05-04 02:29:41 +03:00
Zimin A.N. ebf1b03863 feat(admin-ui): migrate from custom components to @nstart/ui design system
Свои Modal/Button/inputs выкинуты — берём nstart-ui v0.1.3 как в других corp
React приложениях. Установка через .npmrc → repo.nstart.cloud/repository/npm-hosted/.

Что сделано:
- .npmrc: scope @nstart на corp Nexus + min-release-age=7
- deps: @nstart/ui ^0.1.3, @phosphor-icons/react ^2.1.10
- bumped peer deps: i18next ^26 + react-i18next ^17 (требование nstart-ui)
- styles.css: импорт @nstart/ui/styles/{fonts,theme}.css + critical @source
  директива (без неё Tailwind v4 tree-shaking дропнул бы все классы из dist либы)
- main.tsx: <NStartUiProvider> на корне
- DROP src/components/ui/Modal.tsx (свой Modal больше не нужен)

Routes:
- __root: LanguageSwitch вместо двух кнопок RU/EN, токены ultramarain/regolith/carbon
- dictionaries.index: PageHeader + Badge для scope chip + Alert/EmptyState/LoadingBlock
- dictionaries.$name: PageHeader с actions, Modal/Panel/Table/TableRow/Cell,
  IconButton с Phosphor (PencilSimple, XCircle), Button variant=primary/danger/ghost

Form (SchemaDrivenForm):
- TextInput/TextArea/SingleSelect/Checkbox + FieldLabel/Hint/Error из @nstart/ui
- FormSection + FormGrid columns=1|2 для группировки
- FormActions align=end с Button loading state
- Alert variant=error для server-side ошибок (422 IdempotencyConflict / 409 InProgress)
- ajv валидация по schema_json остаётся такой же (Draft-07, как у backend)

Bundle: 192KB → 659KB gzip — тянет echarts/lottie/brand assets из nstart-ui,
для internal admin tool приемлемо (cached after first load).
2026-05-04 02:24:44 +03:00
Zimin A.N. 505dbda0d8 fix(admin-ui): добавить brand-100/200/300/400/600/800/900 в @theme
Кнопка «+ Создать запись» использует bg-brand-600 / hover:bg-brand-700, но в
@theme были определены только brand-50/500/700 — bg-brand-600 резолвился в
ничто, кнопка отрисовывалась без фона + text-white = невидимая на белом.

Добавил полную шкалу 50-900 в OKLCH для консистентного hover/active.
2026-05-04 02:09:23 +03:00
Zimin A.N. e09864bbed fix(admin-ui): split /dictionaries into layout + index for nested route
После regen routeTree.gen.ts /dictionaries/$name стал child route /dictionaries.
dictionaries.tsx рендерил карточки без <Outlet/> — клик по карточке менял URL,
но child компонент негде было рендерить, поэтому видна та же страница со списком.

Разнёс по конвенции:
- dictionaries.tsx — layout с <Outlet/>
- dictionaries.index.tsx — карточки списка (matches /dictionaries)
- dictionaries.$name.tsx — без изменений (matches /dictionaries/{name})
2026-05-04 01:57:52 +03:00
Zimin A.N. b1b1f05e3c feat(admin-ui): record CRUD with json-schema-driven form
POST/PUT/DELETE формы для записей справочников. Schema-driven рендер полей
из definition.schemaJson — i18n-aware (x-localized → input на каждый supportedLocale),
enum → select, type → text/number/checkbox/array. Валидация через ajv (Draft-07,
match backend networknt validator).

- new: api/mutations.ts с Idempotency-Key (crypto.randomUUID) и query invalidation
- new: SchemaDrivenForm — react-hook-form + ajv compile per-schema, errors через
  setError по ajv instancePath
- new: Modal (Esc + backdrop click) и confirm dialog для close-операции
- nginx: regex location ^/api/v1/dictionaries/[^/]+$ → writer (admin schema fetch)
- queries: dictionaryDetailQuery возвращает full def с schemaJson
- i18n: RU/EN ключи для CRUD (create/edit/close, validFrom/validTo, confirm)
2026-05-04 01:49:08 +03:00
Александр Зимин d5a747616a fix(admin-ui): envsubst FQDN с per-cluster DNS suffix
nginx resolver не использует search list, поэтому в proxy_pass нужен
FQDN. Cluster domain различается:
  - cluster.265 (staging): 265.local
  - srvaltum01 (prod):     altum.local
  - default kubespray:     cluster.local

Решение: nginx.conf — template с ${ORDINIS_NAMESPACE} и
${CLUSTER_DOMAIN}, обрабатывается официальным nginx entrypoint
(/docker-entrypoint.d/20-envsubst-on-templates.sh). Helm подставляет
ENV из fieldRef + values per-env.
2026-05-04 01:21:34 +03:00
Александр Зимин 8e97c50e6c fix(admin-ui): resolver 169.254.25.10 для dynamic proxy_pass
Nginx с переменной в proxy_pass требует resolver. Ставим nodelocal-dns
(169.254.25.10 на kubespray-кластерах) с публичным fallback (1.1.1.1).
Это снимает 502 Bad Gateway на /api/v1/* — раньше nginx не мог
зарезолвить ordinis-read-api/ordinis-app в момент запроса.
2026-05-04 01:16:09 +03:00
Александр Зимин 1b1d8c956a fix(admin-ui): vite-only build (skip strict tsc) + ts-nocheck stub
- TS errors на cold-build: routeTree типы unknown пока plugin не сгенерил
  реальный файл, import.meta.env без vite/client typing.
- Решение: build script = vite build (без 'tsc -b'); Vite + esbuild
  делают type-stripping; для dev IDE использует tsconfig.
- Добавлен src/vite-env.d.ts с reference vite/client.
- routeTree.gen.ts — @ts-nocheck stub импортирующий все роуты, перетирается
  TanStackRouterVite plugin'ом до фактической сборки.
2026-05-04 01:11:50 +03:00
Александр Зимин daad2b6349 ci: docker-admin-ui job (Node build → nginx serving)
Build только при изменениях ordinis-admin-ui/** или каждом push в main.
Использует тот же DinD pattern, push в repo.nstart.cloud/terravault/ordinis-admin-ui.
2026-05-04 01:05:20 +03:00
Александр Зимин 1eba2ee59b feat(admin-ui): minimal React+Vite+TanStack scaffolding
- Vite 7 + React 19 + TanStack Router (file-based) + TanStack Query
- Tailwind v4 (CSS-first config, OKLCH brand colors)
- i18next + react-i18next с RU/EN switcher в header
- Axios client с Accept-Language interceptor + JWT placeholder
- 2 pages: /dictionaries (list cards) + /dictionaries/$name (records table)
- Dockerfile (node build + nginx serving + API proxy)
- README + .gitignore

Backend:
- Новый GET /api/v1/dictionaries в read-api (DictionaryListController),
  фильтрует по доступному scope через ScopeContext
- DictionaryListItem DTO без heavy schema_json

routeTree.gen.ts — stub; реальное содержимое генерится Vite plugin'ом при
первом 'pnpm dev'.
2026-05-04 00:55:04 +03:00