Commit Graph

239 Commits

Author SHA1 Message Date
Александр Зимин 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
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
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
Zimin A.N. c8dd5aad4e feat(schema): Phase 1 schema templates + Nexus DevOps runbook
## AI Schema Assist Phase 1 (steps 1-2)

Approach A core (templates only, no LLM) — admin начинает создание нового
справочника с curated skeleton вместо пустого Monaco. Approach C (per-field
LLM suggest) — Phase 2 на этом фундаменте.

### Backend
- `templates/*.template.json` в ordinis-cuod-bundle resources — 6 шаблонов
  (blank/spacecraft/ground-station/antenna/frequency-band/operator)
- `SchemaTemplateRegistry` (rest-api) — classpath scan at startup, strip
  x-template-* metadata, expose canonical JSON Schema
- `SchemaTemplatesController` — GET /api/v1/dictionaries/templates (list)
  + /{id} (detail с schemaJson)
- Cross-bundle support: scanner matches classpath*:templates/*.template.json
  — другие bundles могут shippит свои templates через те же conventions

### Frontend
- types: SchemaTemplateSummary + SchemaTemplateDetail
- queries: schemaTemplatesQuery + lazy detail query
- TemplatePicker.tsx — grid of cards (icon + name + description + tags),
  click → fetch detail → fire onPick(detail)
- Integrated в DictionaryEditorDialog (create mode, properties.length===0
  visible — после первой property picker исчезает чтобы не перезатереть)

## Nexus DevOps handoff

`docs-internal/ops/nexus-alpine-mirror.md` — runbook про APKINDEX vs file
inventory drift на v3.23 mirror. Includes:
- Verification commands
- 3 fix options (invalidate cache / scheduled refresh / self-host)
- Prometheus alert proposal
- Current workaround (node:22-alpine3.20 pin) reference

Active issue paired с CI saga !227/!229/!230 — нужен DevOps action.
2026-05-16 13:20:43 +03:00
Zimin A.N. e5a07fc391 feat(records): «Запланировано» badge per row для scheduled future versions
Last item из QA report backlog'а. Banner + empty-state hint показывают
aggregate count, но не помогают find конкретные записи которые скоро
изменятся. Badge per row даёт O(1) visual scan.

Backend (read-api):
- ScheduledRecordsSummary.scheduledBusinessKeys: List<String> (capped 500)
- Repo method findScheduledBusinessKeys (DISTINCT subquery)
- Single fetch вместо N+1 per-row queries

Frontend:
- scheduledByKey Set<string> построен из summary
- Badge variant=info «Запланировано» рядом с businessKey badge'ом
- Title attribute с full RU/EN tooltip
- Не блокирует layout — gap-1.5 с pendingReview badge'ем (могут быть оба)

i18n keys для ru/en — short label «Запланировано» / «Scheduled».
2026-05-16 12:49:42 +03:00
Zimin A.N. cb8a229adf fix(ui): QA report P3/P4 batch — 7 cosmetic/UX bugs
QA report cleanup (после P1/P2 ship'нутых в v2.31.4):

- **BUG-009**: Excel/SQL export форматы получили явный «скоро» badge поверх
  opacity-only disabled state. Title attribute оставлен.
- **BUG-010**: «Доступ актора» (audit scope column) переименован в «Scope» —
  компактнее, не переносится на 2 строки.
- **BUG-011**: Все audit table headers получили title attribute → hover
  tooltip когда text truncate'ится («ИЗМЕНЕН…» → full «Изменения»).
- **UX-001**: Scope chips в /dictionaries теперь показывают ✓ glyph когда
  selected → визуально clear что multi-select. Aria-label расширен:
  «Фильтр по области (можно выбрать несколько)».
- **UX-002**: Breadcrumb segments получили title attribute → hover показывает
  full label когда truncate'ится в narrow viewport.
- **UX-003**: При смене вкладки в редакторе справочника (records → relations
  и т.п.) URL params q/scopes сбрасываются — раньше search фильтр оставался
  применённым после возврата на records.
- **UX-004**: TimeTravel slider boundary метки получили 2-digit year:
  «05.05.26» вместо «05.05» — disambiguates конец декабря / начало января.
  Title attribute exposes full локальную дату.
- **UX-009**: «Удалить» button в RecordDrawer переименован в «Закрыть запись»
  — same action как row context menu «Закрыть» (bitemporal close, не truly
  delete). Consistent labeling across UI.
2026-05-16 00:09:41 +03:00
Zimin A.N. ff7e72e0fd fix(qa-report): version tag, /drafts redirect, audit filter complete, i18n cleanup
QA report v2.31.x batch:

- **VersionController.tag**: было 'none' (maven build на main без git tag) → UI
  показывал «dev · 6077d4df». Helm передаёт image.tag через env
  ORDINIS_BUILD_IMAGE_TAG, controller парсит «v2-31-2-e3c82775» → «v2.31.2».
  Helm chart change в ordinis-infra (separate MR).

- **/drafts redirect** (BUG-003): пустой 404 «Not Found» plain text → теперь
  static redirect на /my-drafts.

- **Audit action filter** (BUG-004): дропдаун содержал только 3 значения
  (CREATE/UPDATE/CLOSE), но в таблице 11 типов (DRAFT_*, DEFINITION_*).
  Frontend AuditAction enum + ACTIONS list синхронизированы с backend
  AuditLogger/SchemaDraftService/DraftService.

- **i18n cleanup** (BUG-006/007/UX-005):
  - myDrafts.description: «Все ваши submissions с pending/approved...» →
    «Все ваши черновики с историей: ожидает рассмотрения, одобрен...»
  - reviews.description: «Pending drafts от makers, ждут approve/reject» →
    «Черновики от авторов, ждут одобрения или отклонения. Общая очередь...»
  - reviews.queueTotal: «N pending drafts» → «N черновиков в очереди»
  - myDrafts.col.reviewer: «Reviewer» → «Рецензент» (RU only)
  - notifications chip labels: 'submit/approved/rejected' → 'отправлен/
    одобрен/отклонён'
  - audit.action.* expanded к 13 entries для каждого backend action
    type, с понятными лейблами «Черновик одобрен» вместо «DRAFT_APPROVE»

- **WhatsNewModal markdown** (UX-006): тексты вроде «**График доставки
  webhook'ов**» рендерились с буквальными звёздочками. Mini inline renderer
  для **bold**, *italic*, `code` — без full markdown library.
2026-05-15 22:02:29 +03:00
Zimin A.N. a740ab32bf fix(record-form): prefill validFrom=now в create mode тоже
Form review после !224 показал что create mode имеет тот же баг что и edit:
SchemaDrivenForm без переданного defaultValues fallback'ит на '' для
validFrom, и пользователь видит пустое поле «Действует с».

Добавлен defaultValues={{ validFrom: nowIsoLocal() }} для create — consistent
UX с edit mode (!224 fix). Other formы (drafts, reviews, schema workflow) —
либо read-only либо без validFrom, clean.
2026-05-15 21:32:41 +03:00
Александр Зимин a996b1d4e8 Merge branch 'fix/scheduled-marks-banner-prefill' into 'main'
fix(records): scheduled marks + non-empty banner + editor validFrom prefill

See merge request 2-6/2-6-4/terravault/ordinis!224
2026-05-15 18:30:30 +00:00
Zimin A.N. d0ef822b9e fix(timetravel): drop arbitrary +30d max — use real marks only
User feedback: «крайняя дата должна быть исходя из реальной в этом
словаре». Раньше slider всегда extended до now + 30d даже без причины —
если нет scheduled записей на 18.05, slider всё равно тянулся туда,
пользователь блуждал по пустоте.

Теперь max = max(marks) — last real change point (record validFrom включая
upcoming scheduled, которые backend возвращает в /scheduled-summary).
Если все marks в прошлом — max=now (always reachable). Если есть future
scheduled на 18.05 — slider тянется ровно туда, не дальше.

Touches: TimeTravelPicker.tsx (inline editor) + TimeTravelModal.tsx (modal).
2026-05-15 21:30:08 +03:00
Zimin A.N. 9199f380e1 fix(records): scheduled marks on time-travel + non-empty banner + editor validFrom prefill
Три связанных UX бага после v2.31.0 release:

1. **TimeTravel marks**: после !220 slider extended до now+30d, но отметок в
   future window не было → blind drag. Backend теперь возвращает
   upcomingValidFroms (top 50) в /scheduled-summary; frontend merges как marks.

2. **Non-empty banner**: empty-state hint работает только когда список пуст.
   Если currently active записи есть AND ещё запланированы — пользователь
   не видит. Добавлен info banner над таблицей с тем же текстом + CTA.

3. **Editor "Действует с"**: edit mode оставлял поле пустым (bitemporal
   policy «новая версия с now»). User feedback: «показывает ничего, а даты
   заданы» — выглядит как баг. Prefill nowIsoLocal() — теперь юзер видит
   конкретную дату как точку отсчёта.
2026-05-15 21:28:29 +03:00
Александр Зимин 24ac9fd33a Merge branch 'fix/timetravel-modal-future-window' into 'main'
fix(timetravel): extend modal slider max до now + 30 days (paired с !220)

See merge request 2-6/2-6-4/terravault/ordinis!223
2026-05-15 18:08:45 +00:00
Zimin A.N. 1713951f76 fix(timetravel): extend modal slider max до now + 30 days (paired с !220)
MR !220 расширил range у TimeTravelPicker.tsx, но юзер видит другой компонент —
TimeTravelModal.tsx (fullscreen modal с компарисоном now vs picked moment).
У него своя range logic с max = now + pad (1 day), и фикс !220 туда не дотянулся.

User feedback: создал запись с validFrom=18.05, открыл time-travel modal — слайдер
14.05 → 16.05, не дотянуть до 18-го.

Apply same 30-day future window logic к TimeTravelModal так чтобы оба компонента
вели себя одинаково.
2026-05-15 21:08:02 +03:00
Александр Зимин fd98ce9d8e Merge branch 'feat/scheduled-records-hint' into 'main'
feat(records): scheduled records empty-state hint

See merge request 2-6/2-6-4/terravault/ordinis!222
2026-05-15 17:20:53 +00:00
Zimin A.N. 234184645f feat(records): scheduled records empty-state hint
Fixes UX bug: пользователь создаёт запись с validFrom на будущую дату
(e.g. 18.05), открывает catalog 15.05 — пустая таблица, никаких хинтов
о том что запись существует.

Backend (read-api):
- New endpoint GET /api/v1/{dict}/records/scheduled-summary
- Returns {count, nearestValidFrom} для записей с validFrom > now AND
  validTo > now в текущем scope view
- Single-aggregate JPA query (COUNT + MIN), respect scope filter same way
  as listActive — INTERNAL scheduled не учитывается для PUBLIC consumer'а

Frontend:
- New useScheduledSummary hook + ScheduledRecordsSummary type
- Empty-state в dictionaries.$name now shows: "Запланировано N, ближайшая
  на DD.MM [Перейти к этой дате]" с CTA который set'ает time-travel на
  nearestValidFrom
- i18n keys для ru/en plural forms

Связан с MR !219 (TimeTravelPicker 30-day future window) — теперь slider
позволяет дойти до даты записи, и empty-state hint позволяет узнать что
запись существует.
2026-05-15 20:17:07 +03:00
Александр Зимин 1d359beb8e Merge branch 'fix/sidebar-admin-nav-internal-only' into 'main'
fix(sidebar): hide outbox/audit/webhooks для PUBLIC users

See merge request 2-6/2-6-4/terravault/ordinis!221
2026-05-15 17:05:51 +00:00
Александр Зимин df704c28a6 fix(sidebar): hide outbox/audit/webhooks для PUBLIC users 2026-05-15 17:05:51 +00:00
Александр Зимин 4dd67af503 fix(timetravel): extend slider max до now + 30 days (или future marks) 2026-05-15 17:03:00 +00:00
Александр Зимин 4f1a02de2a Merge branch 'feat/phase-b2-per-event-prefs' into 'main'
feat(notifications): Phase B-2 — per-event-type granular preferences

See merge request 2-6/2-6-4/terravault/ordinis!219
2026-05-15 16:53:32 +00:00
Александр Зимин f04b1be1f6 feat(notifications): Phase B-2 — per-event-type granular preferences 2026-05-15 16:53:32 +00:00
Александр Зимин 4bc947b88b Merge branch 'fix/review-drawer-state-leak-between-drafts' into 'main'
fix(reviews): reset drawer state когда reviewer переключает draft

See merge request 2-6/2-6-4/terravault/ordinis!218
2026-05-15 16:40:34 +00:00
Александр Зимин 641a9a64f8 fix(reviews): reset drawer state когда reviewer переключает draft 2026-05-15 16:40:33 +00:00
Александр Зимин 597e05db4c fix(topbar): notification bell badge — fix glyph clip 2026-05-15 16:28:49 +00:00
Александр Зимин e2fc3bcbe0 feat(admin-ui): Phase B frontend — notifications preferences route + UI 2026-05-15 14:56:48 +00:00
Александр Зимин e80d31a340 fix(reviews): anonymous Mine tab — dedicated empty state с sign-in CTA 2026-05-15 13:29:11 +00:00
Александр Зимин 5c4b9c9f21 fix(admin-ui): em-dash fallback в UserCell + sidebar.collapse i18n 2026-05-15 09:02:39 +00:00
Александр Зимин 4078c60bee Merge branch 'fix/mobile-sidebar-a11y-title' into 'main'
fix(a11y): add hidden SheetTitle к MobileSidebar (Radix warning)

See merge request 2-6/2-6-4/terravault/ordinis!200
2026-05-14 22:42:00 +00:00
Александр Зимин dfae205c95 fix(a11y): add hidden SheetTitle к MobileSidebar (Radix warning) 2026-05-14 22:42:00 +00:00
Александр Зимин ac74a8fbd9 fix(sidebar): gate outbox+webhook badge polls on INTERNAL scope (no more 403 spam) 2026-05-14 22:41:28 +00:00
Александр Зимин 9e6ddd1bce fix(ui): replace raw AxiosError dumps with QueryErrorState across routes 2026-05-14 20:42:45 +00:00
Александр Зимин 35c69fd010 refactor(a11y): 44×44 touch targets + normalize accent-bg alpha 2026-05-14 17:32:10 +00:00
Александр Зимин 9050e2427e feat(notifications): TODO 7 — draft decision toast + email + bell badge 2026-05-14 14:40:35 +00:00
Александр Зимин fb64ca4350 refactor(reviews): interaction state polish — 5 UX fixes 2026-05-14 14:13:57 +00:00
Александр Зимин be58b61913 feat(reviews): "Мои" tab — maker self-tracking with status filter 2026-05-14 14:06:30 +00:00
Александр Зимин 4c29f4e7cb refactor(approval-ux): eng-review fixes — extract helpers, add tests, register i18n 2026-05-14 13:52:12 +00:00
Andrei Zimin fe7f9cd01f feat(approval-ux): inline draft CTA + reviewer diff summary + tightened drawer
Acts on the two "build now" items from the design review of the approval
workflow UX (other five went to TODOS.md). Plus the TODOS.md file itself,
authored during the same review.

1. **Inline "Создать черновик с моими изменениями" CTA**
   (DictionaryEditorDialog + CreateSchemaDraftModal)

   Was: when a maker edited an approval-required dict, Save disabled and a
   warning told them to "close the editor and click 'Создать черновик схемы'
   on the dict page." Three friction steps and a hunt for a button.

   Now: the warning Alert carries an action button — "Создать черновик с
   моими изменениями" — that opens CreateSchemaDraftModal in-place with the
   maker's in-progress schemaJson pre-loaded. They add a reason and submit.
   No retyping, no hunt.

   Implementation:
   - CreateSchemaDraftModal accepts optional `initialProposedSchema` prop.
     Seed order: edit-mode draft → caller pre-fill → live HEAD.
   - DictionaryEditorDialog mounts the modal inline and triggers it from the
     Alert action. Closes the editor on successful create.

2. **Diff summary chips in ReviewDrawer**

   Was: two raw JSON dumps side by side. Reviewer had to eyeball-diff long
   records to find the actual edit.

   Now: a row of summary chips above the panes — "+N added, −M removed,
   ~K changed" with the field names inline. Computed shallow client-side
   (shallowDiffSummary helper) — ordinis records are flat-ish at the top
   level, no need for a backend diff endpoint. Skipped for CLOSE op (no
   proposed) and when both sides are null.

3. **Tightened drawer header + trimmed empty-state copy**

   Was: single flex-wrap line with four spans (badge + bk + maker + ts) —
   visually busy. Plus a verbose empty-state description: "Когда maker
   отправит schema draft на ревью, он появится здесь."

   Now: two-row layout (identity row on top, provenance row below) so the
   eye lands on Badge + BK first. Empty-state copy shortened to "Очередь
   пуста" / "Схемы на ревью появятся здесь."

Other five gaps (Мои tab, full state-coverage pass, DESIGN.md, a11y audit,
maker notifications) recorded in TODOS.md.
2026-05-14 15:56:50 +03:00
Александр Зимин 898cb345ec fix(ui): DictionaryEditorDialog disables Save when draft workflow needed 2026-05-14 12:33:43 +00:00
Andrei Zimin 455ca2ff26 fix(ui): UserCell в changelog/events/history — ещё 6 мест
После !178 (record drawer + RecordHistoryDrawer + webhook detail) ещё
осталось 6 точек где раньше рендерился UUID. Все они в "истории
словаря" и tab "События":

1. ChangelogDiffModal:57 — header "автор: <name>" в diff modal.
   Было: `{data.publishedBy.name}` (тип ChangelogPublisher.name —
   docstring буквально говорит "Backend пока эхает sub").
   Стало: <UserCell uuid={data.publishedBy.sub} />.

2. TimeTravelModal:686 — список изменений, имя автора рядом с датой.
3. TimeTravelModal:898 — RIGHT panel "История этой версии", dt/dd
   pair с автором.
4. TimeTravelModal ComparePanel:414 — "· {author}" suffix,
   author там DictionaryDetail.updatedBy (тоже UUID).

5. dictionaries.$name.tsx:2263 (events tab, EventsTabContent row) —
   `{r.userId ?? 'anonymous'}` raw. Сменил на UserCell для не-null
   userId, anonymous-fallback оставил без UserCell (нечего lookup'ать).

6. dictionaries.$name.tsx:2399 (history tab, HistoryTabContent entry)
   — `{entry.publishedBy.name}` raw.

Всё через тот же useUserDisplay hook что и в !177 — backend
endpoint /admin/users/{sub}/display + JwtUserCaptureFilter cache.
Если sub нет в cache, UserCell fallback'ит на shortened UUID
(текущее поведение из !177).

ChangelogPublisher type comment в src/api/client.ts уже намекал что
.name это placeholder — useUserDisplay теперь реально резолвит.
2026-05-14 14:33:20 +03:00
Andrei Zimin 93b2676994 fix(ui): wrap remaining UUIDs into UserCell — 3 missed spots
MR !177 added /admin/users/{sub}/display + UserCell + useUserDisplay,
but only some renderers were migrated. Three places still rendered
raw UUID strings:

1. reviews.tsx:466 — record draft drawer ("Согласование draft" header)
   showed `{draft.makerId}` raw. Tables on the same page already use
   <UserCell uuid={d.makerId} />. Verified via prod staging:
   soloviev.da approved a record draft today and the drawer header
   showed the maker as "77ec2cc8-98e3-4d70-8037-94038fcbde67" instead
   of the username.
2. RecordHistoryDrawer.tsx:110 — version history `updatedBy` raw.
3. webhooks.$id.tsx:235 — webhook subscription `createdBy` raw.

All three switched to <UserCell uuid={...} />, matching the existing
pattern. Added the import where missing. Wrapped each in
inline-flex so the UserCell sits next to the label cleanly.

No backend / API changes — same /admin/users/{sub}/display endpoint
populated by JwtUserCaptureFilter (MR !177).
2026-05-14 14:07:02 +03:00
Александр Зимин 95c2b46dd3 Merge branch 'feat/user-display-endpoint' into 'main'
feat(users): /admin/users/{sub}/display endpoint + UserCell uses it

See merge request 2-6/2-6-4/terravault/ordinis!177
2026-05-13 16:42:46 +00:00
Александр Зимин be90077520 feat(users): /admin/users/{sub}/display endpoint + UserCell uses it 2026-05-13 16:42:46 +00:00
Александр Зимин c8d90930e3 Merge branch 'feat/phase2-approval-min-role-enforcement' into 'main'
feat(approval): enforce per-dictionary approvalMinRole (Phase 2)

See merge request 2-6/2-6-4/terravault/ordinis!172
2026-05-13 16:42:09 +00:00
Александр Зимин 5187845047 feat(approval): enforce per-dictionary approvalMinRole (Phase 2) 2026-05-13 16:42:09 +00:00
Александр Зимин 6188062c34 Merge branch 'feat/close-via-draft-when-approval-required' into 'main'
feat(records): close / bulk-close через draft когда approvalRequired

See merge request 2-6/2-6-4/terravault/ordinis!175
2026-05-13 16:41:09 +00:00
Александр Зимин d66db60b84 feat(records): close / bulk-close через draft когда approvalRequired 2026-05-13 16:41:09 +00:00
Александр Зимин 94360681b6 fix(reviews): friendly error в ReviewDrawer вместо AxiosError 2026-05-13 16:41:05 +00:00
Александр Зимин 6eb56cffe4 feat(records): auto-route create/edit через draft когда approvalRequired 2026-05-13 15:29:11 +00:00
Александр Зимин f096b5746d feat(my-drafts): schema drafts тоже в "Мои черновики" 2026-05-13 12:45:45 +00:00