Commit Graph

161 Commits

Author SHA1 Message Date
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
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
Александр Зимин 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. 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. 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
Александр Зимин 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
Александр Зимин 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
Александр Зимин 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
Александр Зимин 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
Александр Зимин 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
Александр Зимин f096b5746d feat(my-drafts): schema drafts тоже в "Мои черновики" 2026-05-13 12:45:45 +00:00
Zimin A.N. 5fb1658880 feat(sidebar): role-based visibility для /reviews
Phase 1d enhancement: 'Согласования' пункт сайдбара скрыт если у user'а
нет approver/reviewer ролей. Раньше показывался любому authenticated —
оператор без reviewer прав видел queue, кликал и получал пустую
страницу (или 403 если бы пробовал approve).

Visibility rules:
- 'Мои черновики' — auth (maker может быть любой)
- 'Согласования' — auth + (ordinis:schema:reviewer OR ordinis:record:reviewer
  OR ordinis:admin super-role) — иначе hidden
- 'Outbox' / 'Webhooks' / 'Аудит' — auth (admin-y но backend не gated
  отдельной ролью)

Новый NavItem.reviewerOnly flag — фильтр в visibleSections учитывает
оба flag'а (authRequired + reviewerOnly).

Применено и к desktop Sidebar и к MobileSidebar (DRY refactor — отдельный
task).
2026-05-13 13:52:40 +03:00
Zimin A.N. 6ee88a86bd fix(ui): кнопки в schema draft drawer — RU label + flow row
User feedback (screenshot): 'Approve' остался английским в RU локали +
'Отозвать' улетал на отдельную строку справа из-за ml-auto.

Fix:
- i18n.ts: 'workflow.schemaDraft.actions.approve' default 'Approve' →
  'Одобрить'. EN остался 'Approve'.
- SchemaDraftDrawer.tsx: убрал ml-auto на withdraw button. Раньше это
  force pushed его в конец строки → flex-wrap отправлял на новую строку
  правым краем. Сейчас withdraw сидит сразу после reject в той же
  flow-row, с danger color для визуальной distinction. На узком экране
  всё ещё wrap'нется естественно (без 'плавающего' alignment).
2026-05-13 11:45:29 +03:00
Zimin A.N. e6294ce79c fix(ui): shared UserCell для display UUID actor'ов везде
Browser QA на staging показал что UUID actor'а render'ится raw во многих
местах кроме drawer (MR !158 — там был inline fix):
- /reviews Records tab — column AUTHOR
- /reviews Schemas tab — column AUTHOR
- /audit log — column USER

77ec2cc8-98e3-4d70-8037-94038fcbde67 — нечитаемо для оператора. Тот же
паттерн что я делал в MR !158, теперь shared helper:

src/lib/useUserDisplay.tsx:
- useUserDisplay(uuid) hook — returns { display, isMe, fullId }
- UserCell component — readonly cell с title=fullId tooltip
- Match current user (JWT sub claim) → 'Я • <preferred_username>'
- Other user → short UUID (8 chars) + full в title
- null/undefined → 'anonymous'

Применено:
- reviews.tsx:334,625 — records + schemas table AUTHOR cells
- audit.tsx:476 — USER cell
- SchemaDraftDrawer.tsx — refactor (был inline в MR !158 → теперь shared)

TODO Phase 1d: backend endpoint /admin/users/{sub}/display чтобы и для
не-current-user отображать username.
2026-05-13 11:43:04 +03:00
Zimin A.N. aec2cdd644 fix(sidebar,audit): badges для Outbox + Webhooks, clarify audit scope label
Sidebar:
- Outbox: бэйдж = outbox.pending (события queued, не отосланы). DLQ
  отдельно — alarming но не actionable из бэйджа, видно на /outbox.
- Webhooks: бэйдж = count активных subscriptions (w.active === true).
  User feedback: 'вебхуки активные есть а цифры с бэйджем нет'.

Audit log column header:
- 'Scope' → 'Доступ актора' (ru) / 'Actor scope' (en)
- Backend audit_log хранит userScope (claim из JWT актора), НЕ scope
  изменённого ресурса. User confusion: 'правки в public были а тут
  RESTRICTED' — потому что у пользователя scope-привилегия RESTRICTED.
  Уточнённая label убирает путаницу.
- TODO: backend feature — добавить dictionaryScope в AuditEntry чтобы
  показать обе ('Актор: RESTRICTED → Ресурс: PUBLIC' — двусторонний
  audit trail для compliance).
2026-05-13 11:03:47 +03:00
Александр Зимин 441bbedadf Merge branch 'fix/schema-draft-author-display-and-withdraw-gate' into 'main'
fix(schema-draft): author display + gate Отозвать только maker'у

See merge request 2-6/2-6-4/terravault/ordinis!158
2026-05-13 07:39:59 +00:00
Zimin A.N. fbf978aa39 fix(schema-draft): автор display + gate Отозвать только maker'у
Два бага в SchemaDraftDrawer (видны на review pending):

1. АВТОР показывал raw UUID (77ec2cc8-98e3-4d70-8037-...). Резолвим:
   - Если maker = current user (JWT sub claim) → 'Я • <preferred_username>'
   - Иначе → short UUID (первые 8 символов) с full в title attribute
   - Стилизация: 'Я' — text-ink (читаемая), чужой — font-mono text-mute
   - TODO Phase 1d: backend endpoint для resolve чужих sub → display name

2. Кнопка 'Отозвать' показывалась всем юзерам, не только автору.
   - Backend всё равно 403'нет non-maker withdraw — но UI confusion:
     reviewer видел кнопку которая всегда fail'ит.
   - Gate: показываем только когда auth.user.profile.sub === draft.makerId.
   - Reviewer теперь видит чистый action row (Approve / Запросить правки /
     Отклонить), без danger-button мимо контекста.

Реальный случай: см. user screenshot на v2.16.x — Reviewer modal с UUID
автора и кнопкой 'Отозвать' справа внизу.
2026-05-13 10:33:02 +03:00
Zimin A.N. 252939f63a fix(sidebar): подключить reviews badge к real count
Бэйдж 'На ревью' в sidebar (desktop + mobile) был захардкожен в 0 —
TODO comment ждал cheap /drafts/pending/count endpoint. На самом деле
existing /admin/reviews/pending и /admin/schema-reviews/pending уже
возвращают paged response с totalElements/total в metadata — request
size=1 даёт нужный counter с минимальным payload (~1 строка + 4
metadata field'а).

Новый hook useReviewsBadgeCount():
- Параллельно дёргает обе queue (record + schema)
- Суммирует totalElements (record) + total (schema)
- Полл каждые 60s (lighter чем 30s queue refresh — badge не critical)
- Gated на auth.isAuthenticated (anonymous не должен дёргать admin endpoint'ы)

Sidebar.tsx (оба варианта — desktop + mobile MobileSidebar):
- reviewsCount: 0 → reviews.total
2026-05-13 10:08:57 +03:00
Zimin A.N. e46598c47d feat(updateBanner): «Что нового» button открывает modal с changelog
Per user feedback: «по кнопке что нового должно открываться модальное
окно с изменениями».

UpdateBanner кнопка «Что нового» (т9n key `updateBanner.docs`) ранее
открывала `/docs/` в новой вкладке — generic documentation page, не
contextual для «что именно в этом обновлении».

Теперь — открывает `WhatsNewModal` (тот же modal что показывает 🎁
button в TopBar) — public changelog с release notes сразу.

## Refactor

- **`WhatsNewModal.tsx` (NEW)** — extracted controlled-mode modal с
  release notes. Props: `open`, `onClose`. Internal logic читает
  changelog, tracks lastSeen в localStorage, на open mark'ает latest
  как seen.
- **`WhatsNewButton.tsx`** — refactored: trigger button + use
  `WhatsNewModal`. Поведение unchanged (🎁 icon с unread badge → click → modal).
- **`UpdateBanner.tsx`** — replaced `window.open(DOCS_URL)` на
  `setWhatsNewOpen(true)` + render `<WhatsNewModal>`. DOCS_URL constant
  removed (больше не используется).

## UX intent

Когда update detected:
1. User видит orange banner «Доступна новая версия v2.16.0»
2. Hover «Что нового» button → opens modal
3. User читает release notes (что добавлено, исправлено) ДО reload
4. Click «Обновить» → reload

Раньше «Что нового» вёл к generic docs page — user не знал ЧТО
изменилось, только что update есть. Modal даёт structured answer.

## Test plan

- [x] `pnpm tsc --noEmit` clean
- [x] `pnpm build` clean
- [x] `pnpm vitest run useChangelog.test` — 11/11 pass (parser logic
      unchanged)
- [ ] Manual: симулировать update banner (mock useAppVersion
      `updateAvailable=true`), click «Что нового» → modal opens
- [ ] Manual: 🎁 button в TopBar также opens same modal
- [ ] Manual: close modal → red dot disappears на 🎁 button
      (localStorage updated)
2026-05-12 23:28:12 +03:00
Александр Зимин 5103ed6d9c fix(admin-ui): changelog build context — copy script + CI before_script 2026-05-12 18:48:04 +00:00
Александр Зимин a34c6d4c82 feat(topbar): «Что нового» button + 2026-05-12 state.md update 2026-05-12 17:05:24 +00:00
Александр Зимин 5479142093 feat(webhook): time-series histogram stats endpoint + frontend chart 2026-05-12 14:30:22 +00:00
Александр Зимин da04b862ab feat(drawer): section chip strip scroll-spy via IntersectionObserver 2026-05-12 13:29:52 +00:00
Александр Зимин 775b8345e3 feat(timetravel): Records + Schema tabs wired with real data 2026-05-12 13:00:03 +00:00
Александр Зимин 3dbad1e16c feat(timetravel): «Что изменилось» tab — закрывает «backend pending» placeholder 2026-05-12 12:45:55 +00:00
Александр Зимин 3984d99004 fix(ux): friendly error UI вместо raw AxiosError 2026-05-12 12:41:48 +00:00
Александр Зимин d864a8561a feat(webhooks): stats cards dashboard на subscription detail 2026-05-12 12:25:15 +00:00