Commit Graph

13 Commits

Author SHA1 Message Date
Александр Зимин 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
Zimin A.N. b94912789f fix(fonts): semantic typography utilities per handoff (7 roles)
Заменяет blanket override из MR !45 на типизированную type scale per
design_handoff_ordinis_mdm/README.md "Scale" section.

Семь semantic @utility в styles.css:
  text-title-xl  22/600   — modal title, section header
  text-title-lg  17/600   — page title в editor
  text-title-md  15/600   — dictionary card title
  text-body      13/400   — workhorse: body, buttons, tabs, inputs
  text-cell      12.5/500 — table cell text
  text-mono      11/500   — Mono: IDs, dates, FK refs (font-mono baked in)
  text-cap       10.5/600 — Tektur UPPERCASE caption (font/uppercase baked in)

Audit phases:
  P1: добавил 7 утилит, font/uppercase/tracking baked где надо
  P2: 43 deterministic codemods (font-mono+text-2xs/[11px] → text-mono,
      [15/17/22]px+font-semibold → text-title-*, [12.5]px → text-cell,
      [10.5]px+uppercase+tracking → text-cap)
  P3: 64 text-sm → text-body (handoff workhorse), 84 text-2xs context-aware
      (TableCell → text-cell, bare → text-cell, плюс cleanup caps в backticks
      template literals который Phase 2 пропустил), 25 text-xs (6 → text-mono
      когда с font-mono, 19 → text-cell), 8 titles text-base/lg → text-title-*
  P4: убрал --text-2xs override (no users), оставил --text-sm: 13px scoped
      к @nstart/ui passthrough (см. comment в styles.css — убирается в Stage 3.9)

Stats:
  text-body: 69 | text-cell: 99 | text-mono: 50 | text-cap: 42
  text-title-xl: 5 | text-title-lg: 5 | text-title-md: 7
  text-sm/text-2xs/text-xs в src/: 0 (только в styles.css комментариях)

Поведение всех 277 typography usages теперь явно соответствует handoff —
каждое место осознанно выбрано под роль, не плажирующий override.
2026-05-11 14:31:35 +03:00
Zimin A.N. 71432e9ae7 fix(fonts): align all text sizes to handoff spec via Tailwind theme overrides
Глобальные правки через @theme в styles.css вместо 200+ точечных правок:

- text-sm: 14px -> 13px (handoff workhorse — body, buttons, tabs)
- text-2xs: undefined -> 11px (handoff IDs, mono meta — было невидимо)
- @utility text-cap: 10.5px Tektur uppercase 0.10em (handoff caps)

Codemod (30 sites): text-2xs + uppercase + tracking-[0.10em] -> text-cap.
Sidebar section labels consolidated с explicit text-[10.5px] на text-cap.

Покрывает 64 text-sm + 148 text-2xs автоматически. Table cells (12.5px)
и page/modal titles (17px/22px) остаются explicit text-[Npx] arbitrary
values по месту — нет slots в Tailwind scale.
2026-05-11 14:12:57 +03:00
Александр Зимин 6db2a0c345 feat(admin-ui): catalog refresh + token migration (Stage 3.2) 2026-05-11 09:08:38 +00:00
Александр Зимин b7d2fbd7ce feat(admin-ui): codemod @nstart/ui → @/ui (Stage 2.2) 2026-05-10 22:17:06 +00:00
Zimin A.N. 84a4de4ceb feat(approval): Approval Workflow v2 Phase 2 — admin UI reviewer queue + toggle
Phase 2 admin UI на скелете Phase 0/1 (e438c4c, 2020af9). Делает workflow
end-to-end usable: reviewer'ы могут approve/reject через UI; admins
включают approval_required per-dict через checkbox.

API surface (admin-ui):
- types: DraftStatus, DraftOperation, DraftResponse, SubmitDraftRequest,
  ReviewQueuePage. + DictionaryDefinition: approvalRequired, approvalMinRole.
- queries (react-query):
  * useReviewQueue (page, size) — global queue, refetch 30s.
  * useDraft(id) — single draft for diff drawer.
  * useLiveRecord(dict, bk) — current live record for side-by-side comparison.
- mutations:
  * useSubmitDraft(dict) — POST /dictionaries/{n}/records/drafts
  * useApproveDraft() — POST /drafts/{id}/approve
  * useRejectDraft() — POST /drafts/{id}/reject (reason required)
  * useWithdrawDraft() — DELETE /drafts/{id}

New /reviews route:
- Table queue: dict, business_key, operation badge, maker, submitted_at.
- Click "Просмотр" → side-by-side drawer:
  * left: current live (если есть; для CREATE — placeholder).
  * right: proposed (или CLOSE notice).
  * maker comment chip если есть.
  * Reviewer comment input + Approve / Reject buttons.
  * Reject disabled пока comment пустой (mirror's backend 400).
- Auto-invalidates на success — queue refresh, list updates.
- Nav link "Согласования" в header.

DictionaryEditorDialog Metadata tab:
- New checkbox "Требовать approval (maker-checker)" — orbit/8 styled
  (отделимо от Redis projection card visually).
- При checked → shown approval_min_role TextInput (e.g. "ordinis:internal").
- payload sends approvalRequired + approvalMinRole.

i18n RU/EN:
- nav.reviews, reviews.{title,description,empty,queueTotal_*,col.*,
  action.*, drawer.*}
- schema.approvalRequired.{label,hint}, schema.approvalMinRole.{label,hint}
- Plurals для RU queueTotal (one/few/many/other).

Verify:
- pnpm tsc --noEmit: clean.
- pnpm test (vitest): 89/89 PASS.
- pnpm build: clean.
- routeTree.gen.ts auto-regenerated с /reviews.

Phase 3 (next session):
- e2e tests: full flow maker submit → checker approve → record live + outbox event.
- e2e: reject keeps draft, doesn't publish.
- e2e: self-approve blocked (no_self_approve constraint).
- e2e: two pending drafts на one business_key blocked (one_pending_per_key).
- Per-dict opt-in soak: 1-2 dicts (например ground_station) с
  approval_required=true в staging.
2026-05-08 13:28:56 +03:00
Zimin A.N. 0a8275ef4c feat(redis-projection): per-dict feature flag (CEO plan E2 tiered perf)
Per-dictionary opt-in для Redis projection materialization. Default false
— projection-writer пропускает event'ы того dict'а, никаких dead writes
в Redis. Включается админ'ом через UI checkbox в Metadata tab.

Migration 0017:
- ALTER TABLE dictionary_definitions ADD COLUMN redis_projection_enabled
  BOOLEAN NOT NULL DEFAULT false.
- Index idx_dict_def_redis_proj для быстрого filtering в list queries.

Backend:
- DictionaryDefinition entity: new field + getter/setter.
- DictionaryResponse DTO: returns flag в response.
- CreateDictionaryRequest DTO: optional Boolean (null/missing = no change).
- DictionaryDefinitionService.create + updateSchema: применяет flag из request.
- ProjectionWriter (projection-writer service):
  * upsert() — early-return + skip counter если flag=false на dict.
  * delete() — symmetric: skip если flag=false (защита от stale keys
    после flag-flip; полный cleanup TODO Phase 2).
  * New metric: ordinis_projection_records_skipped_total.

Admin UI:
- DictionaryEditorDialog Metadata tab: checkbox "Включить Redis-проекцию"
  с описанием. Apply через CreateDictionaryRequest payload.
- DictionaryDefinition + CreateDictionaryRequest types updated.
- i18n RU/EN: schema.redisProjection.{label,hint}.

Что НЕ делается (deferred Phase 2):
- read-api Redis routing — read-api сейчас всегда идёт в PG read replica.
  Когда dict опт-инится в флаг, projection материализуется, но read-api
  ещё не использует. Read routing — отдельная feature с benchmark proof
  (через JMH + k6 SLO test).
- One-shot cleanup job для stale keys после flag-flip from true→false.

Behavior change в production:
- До patch: projection-writer пишет в Redis для ВСЕХ dict events (40
  dictionaries). Default flag=false после migration → projection-writer
  пропускает всё → Redis keys для existing dicts становятся stale.
  Это OK потому что read-api НИКОГДА не читал из Redis (writes были
  dead). Stale keys eventually evicted Redis maxmemory policy.
- После patch: только dicts с redis_projection_enabled=true получают
  материализацию. По default — нет. Admin opt-ins per-dict через UI.

Verify:
- mvn test (full reactor): SUCCESS, all green.
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS (migration 0017 applied).
- pnpm tsc --noEmit: clean.
- pnpm test (vitest): 89/89 PASS.
- pnpm build: clean.
2026-05-08 12:34:58 +03:00
Zimin A.N. 5bc82d2951 feat(admin-ui): webhook event preview tab в DictionaryEditorDialog
CEO plan v1 polish — закрывает gap "Webhook preview при определении схемы".

Что добавлено:
- New tab "События" (4-й после Метаданные / Поля / JSON) в schema editor.
- Per-event sample JSON envelope для всех 5 event types которые fire'ят
  на dict + records: RecordCreated, RecordUpdated, RecordDeleted,
  DefinitionCreated, DefinitionUpdated.
- Sample data автогенерируется из schema property types:
  string → "sample", integer → 42, number → 3.14, boolean → true,
  array → [<sample>], object → recursive, x-localized → {ru-RU, en-US},
  enum → first value, x-references → "REF-001", date/datetime → ISO.
- Topic name pattern shown: ordinis.{dict}.{scope.lower} для record events,
  ordinis.definitions для definition events.
- Disclaimer что это client-side preview — реальный shape определяется
  ordinis-events-api DTOs.

Helps integrators (Альтум, Геопортал, BI consumers) понять shape события
до того как dict published — без ожидания live event'а.

Files:
- src/components/schema/eventPreview.ts (new, ~155 LOC) — pure helper
- src/components/schema/EventsPreviewTab.tsx (new, ~70 LOC) — UI
- src/components/schema/DictionaryEditorDialog.tsx — +16 LOC, mount tab
- src/i18n.ts — schema.tabs.events + schema.events.{intro,topic,disclaimer}
  RU + EN

Verify:
- pnpm tsc --noEmit: clean
- pnpm test (vitest): 89/89 PASS (4.34s)
- pnpm build: clean
2026-05-08 12:14:35 +03:00
Zimin A.N. d8686c0352 feat(schema): создание словаря по шаблону существующего
Для семейств похожих словарей (satellite_type / consumer_type /
processing_level — все 'code+name+description' с таким же scope)
admin копировал JSON руками или дублировал поля в Поля-табе. Это
~3 минуты на каждый.

Теперь в create-mode dictionary editor сверху появился блок 'Создать
по шаблону': SingleSelect списка существующих словарей. На выбор
fetch'ится detail и заполняются:
- description, scope, bundle
- supportedLocales / defaultLocale
- properties (через parseSchemaJson — round-trip preserved для FK,
  enum, localized, opaque kinds)
- idSource

Не трогаем:
- name (admin вводит уникальный имя — это валидация на 422)
- displayName (специфичен для нового словаря)
- schemaVersion (resets на 1.0.0)

Чисто client-side: queryClient.fetchQuery(dictionaryDetailQuery(name))
— TanStack Query сам кеширует результат и для users 'Изменить схему'
позже. Не требует backend изменений.

Edit-mode не показывает шаблон — там initial уже имеет schema
существующего словаря.
2026-05-06 21:41:21 +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. 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