Commit Graph

82 Commits

Author SHA1 Message Date
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. 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
Александр Зимин f04b1be1f6 feat(notifications): Phase B-2 — per-event-type granular preferences 2026-05-15 16:53:32 +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
Александр Зимин 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
Александр Зимин 5187845047 feat(approval): enforce per-dictionary approvalMinRole (Phase 2) 2026-05-13 16:42:09 +00: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. 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
Александр Зимин 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
Александр Зимин 21d95613fc feat(ui): searchable MultiSelect (cmdk) + DateRangePicker (react-day-picker) 2026-05-12 13:55:35 +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
Александр Зимин 27ffd36dca feat(webhooks): event-type multi-select + schema draft events (E) 2026-05-12 11:48:34 +00:00
Александр Зимин a49e01b6c9 feat(reviews): tabs UX — Записи / Схемы (D) 2026-05-12 11:44:46 +00:00
Александр Зимин b3a9104d54 feat(schema-workflow): PATCH /drafts/{id} — edit без recreate (A) 2026-05-12 11:33:01 +00:00
Александр Зимин ac3395f4de feat(schema-workflow): Phase 1c — CreateModal + /reviews extension + RBAC stub 2026-05-12 11:20:29 +00:00
Александр Зимин dd3a2d97e5 feat(schema-workflow): Phase 1b — SchemaDraftDrawer with status-aware actions 2026-05-12 11:08:31 +00:00
Александр Зимин 57be0d88d3 feat(changelog): wire up History tab to /changelog endpoint + diff modal 2026-05-12 10:49:53 +00:00
Александр Зимин 7192aed71c fix(history): подсказка и Button-стайл когда у записи 1 версия 2026-05-12 08:57:44 +00:00
Александр Зимин 1a72418cb8 feat(form): x-section schema metadata + dynamic section bucketing 2026-05-11 21:52:24 +00:00
Zimin A.N. b312ba37ad feat(ui): 3-dot row menu + history compare/revert + DateTime fitting + tablet touch
Per user feedback screenshots:

SchemaDrivenForm DateTimeField:
- grid → flex flex-wrap: на узких cells (drawer 520px ÷ 2 cols = ~210px)
  time wraps под date вертикально. На wide — inline.
- Date min-w 8rem (128px), time w-24 (96px) — tablet touch-safe.

TableHeaderCell без visible label (sr-only для screen readers).
TableCell содержит DropdownMenu trigger 36×36px (touch target):
  - History (always)
  - Edit, Separator, Close (только canMutate, Close в danger color)
Заменяет 3 inline IconButton'a — экономит ширину + tablet friendly.

RecordHistoryDrawer accepts onCompare / onRevert callbacks. Каждая
non-current version row показывает:
  - Сравнить с текущим — opens ConflictDiffModal (reused diff UI)
  - Откатить к v{N} — submit updateMut с этой version данными как
    payload (+ confirm dialog). Backend создаёт новую active version,
    bitemporal history preserves старые.
JSON viewer переключился с <details> на state-driven (hide/show
без CSS rabbit hole).

history.hideData/compare/revert/revertConfirm (RU + EN).

- 3-dot trigger 36px = comfortable touch target (Apple HIG 44, ours
  acceptable для density)
- Drawer на <880px viewport = fullscreen sheet (already)
- DateTime wraps на cell <8rem+9rem+gap = 290px
- Sidebar collapse на <1024 = hamburger overlay
2026-05-12 00:03:44 +03:00
Zimin A.N. 0c978bec0c fix(ui): bigger logo + shorter 'Создать' label
Logo: при 26px+тонких stroke (1.5) почти невидим. Bumped:
- width/height 26 → 28
- strokes 1.5 → 5 (визуально ~1.5px при 28px render)
- outer dots r=8 → 16/14 (main + secondaries)
- inner dots r=6.4 → 13/11
- core r=15 → 24
- outer orbit stroke использует --color-line-2 (мягче чем --color-line)

Favicon тоже укрупнён (stroke 2.2, dot r=3, core r=4) — читается в 16px tab.

Label: 'Создать запись' → 'Создать' для dict.action.create (RU + EN).
User feedback: 'Создать запись надо сократь на Создать'.
2026-05-11 23:36:00 +03:00
Zimin A.N. 6627fd918d fix(topbar): revert ThemeSwitch to 3-icon original + shorten create button
User explicitly: "ThemeSwitch не трогай". Реверт my 2-button Earth|Dark
переделку обратно на 3-icon Sun/Moon/Monitor segmented control (по
original 3bfa7ca).

Также:
- schema.action.create RU: "Создать справочник" → "Создать"
- schema.action.create EN: "Create dictionary" → "Create"
  (per prototype catalog button — короче чтобы не растягивать toolbar)
2026-05-11 19:50:41 +03:00
Zimin A.N. e580a04007 feat(editor): scope-colored InfoPanel badge + Time-travel label
Per redesign (4) prototype.

- InfoPanel scope badge: было `<Badge variant="info">` (generic light bg).
  Стало scope-color filled chip: PUBLIC→accent-bg, INTERNAL→warn-bg,
  RESTRICTED→pink-bg. Matches table SCOPE col + filter pills.
- i18n: timeTravel.button RU "Просмотр на дату" → "Time-travel…"; EN
  "View as of date" → "Time-travel…". User confirmed: "Time-travel label
  оставь" — единый label везде, без локализации (бренд-слово как редизайн).
2026-05-11 19:46:33 +03:00
Zimin A.N. 750395da09 feat(record): RecordDrawer slide-over per Stage 3.5 handoff (Phase A)
Заменяет Modal-based record create/edit на right slide-over drawer per
design_handoff_ordinis_mdm/README.md Screen 3.

What's в Phase A (drawer shell + sticky chrome):

- src/components/record/RecordDrawer.tsx (NEW)
  - Two widths: 520px default / 880px wide, toggle через ‹/› button.
    Persists в localStorage 'ord-drawer-wide'.
  - Sticky header: caption (cap style) + recordId mono + width-toggle +
    history button (edit mode) + ×.
  - Sticky footer: [Удалить] (danger, edit mode) · 'не сохранено · N полей'
    caption · [Отмена] · [Сохранить] (drives form submit через form attr).
  - Built на shadcn Sheet primitive + Radix Dialog. Override sheetVariants
    inline для transition-able max-width (variant size не toggle'ится
    smoothly без animate prop).
  - aria-labelledby wires DialogTitle к содержимому header.
  - Mobile <880px: w-full fullscreen (per handoff responsive table).

- src/components/form/SchemaDrivenForm.tsx
  - New optional props: formId, onDirtyChange.
  - Когда formId передан — внутренний FormActions footer прячется (drawer
    footer driving submit через <button form={formId} type=submit>).
  - Dirty-count считается из RHF formState.dirtyFields (top-level + data.*).
  - useEffect surfaces count к parent — drawer footer caption updates live.

- src/routes/dictionaries.$name.tsx
  - Modal create/edit -> RecordDrawer.
  - State: + recordDirtyCount (reset на close).
  - onDelete wires к existing close-confirm flow (kind: 'close-confirm').
  - onHistory wires к existing setHistoryKey RecordHistoryDrawer.

- src/i18n.ts
  - + form.delete, form.unsavedNFields_one/few/many/other (i18next plural rules).
  - + drawer.{collapse,expand,close,captionEdit,captionCreate}.

Phase B (TODO Stage 3.5b — separate MR):
- Section anchor chips (sticky strip)
- Wide-mode left ToC rail (180px)
- Sticky toolbar: search + 'только заполненные' toggle + counter
- Requires x-section schema metadata в backend properties (CEO open question #5 в handoff)

Tests: 8 files, 116 tests pass. TS strict clean.
2026-05-11 14:39:38 +03:00
Александр Зимин dc0002f379 fix(admin-ui): UpdateBanner generic message 2026-05-11 10:39:35 +00:00
Александр Зимин b2228c2a29 feat(admin-ui): Sidebar 220px + TopBar (Stage 3.1) 2026-05-11 08:51:37 +00:00
Александр Зимин 3bfa7cae17 feat(admin-ui): shadcn/ui + Tailwind v4 foundation + theme switch 2026-05-10 21:46:47 +00:00
Александр Зимин 94da10f487 fix(admin-ui): TimeTravelPicker → visual SVG timeline 2026-05-10 19:50:17 +00:00
Александр Зимин 69f1a05a1f fix(admin-ui): v1.1.1 patch — version routing + guest mode + timeline UX 2026-05-10 19:34:47 +00:00
Александр Зимин 48dd8a487a fix(admin-ui): backlog sweep — 7 review items 2026-05-10 19:21:33 +00:00
Александр Зимин 5afc233a4b fix(admin-ui): compact catalog toolbar (single row) 2026-05-10 18:30:49 +00:00
Александр Зимин 75e844eb98 feat(admin-ui): catalog list view per design handoff (D-list-view) 2026-05-10 18:05:21 +00:00
Александр Зимин 7e460ebff7 feat(admin-ui): Dictionary Catalog v2 (URL filters + sort + density) 2026-05-10 17:37:47 +00:00
Александр Зимин 02167e29f6 chore(admin-ui): strip "НСИ ЦУОД" from logo 2026-05-10 15:20:56 +00:00
Zimin A.N. 361f3b431c feat(approval): bulk approve/reject в /reviews queue (Phase 4 polish)
Phase 4 last optional UX из design doc — bulk operations:

- Multi-select column в reviewer queue (Checkbox per row + select-all
  header). Set<draftId> selection state, persisted между refetch'ами.
- "Approve выбранные" — confirm dialog → parallel approveDraft mutations
  через Promise.all. Каждая mutation independent: если одна fail
  (409 self_approve_forbidden / draft_not_pending race), остальные
  продолжают.
- "Reject выбранные" — Modal с TextArea для shared reason (required,
  matches backend reject_reason_required validation). Reason одинаковый
  для всех — bulk обычно "duplicate registration" / "wrong scope" / etc.
- Bulk result Alert: approved count / rejected count / failed list с
  per-draft reason'ами (err code из 409). User видит точно что прошло
  и что нет.
- Auto-clear selection после bulk operation. Queue refetch'ит через
  refetchInterval=30s, плюс mutation onSuccess invalidate'ит ['drafts'].

i18n RU/EN: 22 keys (selectAll/selectRow/selectedCount/approveSelected/
rejectSelected/clear/confirmApprove/rejectModal*/confirmReject/cancel/
resultTitle/approvedCount/rejectedCount/failedCount). Pluralization
через i18next plurals.

Why ship without soak data:
- Implementation не зависит от queue depth — это просто loop'ит над
  выбранными drafts.
- Reviewer на любом dict с queue >5 уже выгадывает время.
- Если queue staying small после soak — feature не bothering нас (UI
  toolbar показывается только когда anySelected).

Tests: vitest 89/89, tsc clean, prod build clean.
2026-05-10 12:12:57 +03:00
Zimin A.N. aedeafc84e feat(approval): /my-drafts maker self-tracking view + /api/v1/drafts/me
Approval Workflow v2 Phase 4 — UX:

Backend:
- DraftController.myDrafts: GET /api/v1/drafts/me?page&size — все статусы
  (PENDING/APPROVED/REJECTED/WITHDRAWN) текущего authenticated user, sorted
  submitted_at DESC. Resolve maker_id из JWT subject (fallback "anonymous"
  если auth off).
- Reuses existing DraftService.listByMaker (Phase 1 method, был не exposed).

Frontend:
- routes/my-drafts.tsx: новый маршрут с табличным view.
  Колонки: businessKey / operation / status / submittedAt / reviewedAt /
  reviewerId / comment / actions. Status badge tinted (warning/success/
  error/neutral). PENDING строки имеют Withdraw кнопку (calls existing
  useWithdrawDraft с confirm).
- api/queries.ts: useMyDrafts (refetch 30s — maker увидит когда reviewer
  decide).
- api/mutations.ts: useWithdrawDraft теперь invalidate'ит ['drafts'] —
  /me + by-dict обе видят изменение.
- routes/__root.tsx: nav link "Мои черновики" / "My drafts" (после Reviews).
- i18n RU/EN: 18 keys (nav.myDrafts, myDrafts.* col/action/status).
  Pluralization для total через i18next plurals.

Why это нужно:
- До этого maker submit'нул draft и пропадал — единственный feedback был
  badge "На review" в records list (только для текущего dict). Здесь — все
  его submissions с history + withdraw для PENDING.
- Reviewer pool (D3=A) маленький, /reviews queue только для них. Maker
  без role'а не видит queue, но должен tracking своих submissions.

Tests: ordinis-rest-api unit 119/119, ordinis-app e2e 28/28, vitest 89/89.
2026-05-10 12:07:35 +03:00
Zimin A.N. b2e7fd84c9 feat(approval): Approval Workflow v2 Phase 4 monitoring + pending-review badges
Approval Workflow v2 — Phase 4: monitoring + UX visibility.

Backend metrics (Micrometer → Prometheus):
- Gauge nsi_draft_pending_total — global queue depth (polled на scrape).
- Counter nsi_draft_submit_total{dictionary,operation,outcome=ok|already_pending|error}
  — submit attempts с разбивкой по outcome. Phase 3 lessons: error outcome
  выявит schema bugs которые ранее маскировались.
- Counter nsi_draft_decision_total{dictionary,operation,outcome=approved|rejected|withdrawn}
  — counts decisions, разбит по типу.
- Timer nsi_draft_review_duration_seconds — time-to-decide histogram (от
  submit до approve/reject/withdraw). publishPercentileHistogram() — для
  Prometheus histogram_quantile p95/p99.

DraftService:
- Optional<MeterRegistry> — null-safe в test ctor'ах.
- @PostConstruct registerGauges() — Gauge.builder polled от
  RecordDraftRepository.countPending().
- recordDecision() helper в approve/reject/withdraw — записывает Timer +
  Counter одной точкой.
- 4-arg ctor compat для Phase 1 unit tests.

UI (admin):
- useDictPendingDrafts — per-dict pending list, refetch 30s, gated на
  detailQuery.data.approvalRequired (no-op для не-approval dicts).
- "На review" badge в businessKey cell records table — моментальная
  видимость что есть pending draft. Open /reviews для act'а.
- i18n RU/EN: dict.pendingReview.label + tooltip.

Domain:
- RecordDraftRepository.countPending() — для Gauge polled count.

Tests: ordinis-rest-api unit 119/119, ordinis-app e2e 28/28, vitest 89/89.
2026-05-08 15:11:25 +03: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. 48df5ceeb8 feat(search): smart JSONB search across all dictionaries
CEO plan v1 stretch — закрывает gap "Smart JSONB search across all dictionaries".
Last gap из v1 list shipped.

Migration 0018:
- CREATE EXTENSION pg_trgm (CNPG initdb обычно не enable'ит, добавили
  явно с MARK_RAN preCondition если уже есть).
- CREATE INDEX idx_dict_records_data_trgm
    ON dictionary_records USING GIN ((data::text) gin_trgm_ops)
- Trigram-based ILIKE с минимум 3 символа в query — index используется.
  ILIKE '%pat%' на data::text возвращает любые matches в JSONB serialized.

Backend:
- New RecordSearchQuery (ordinis-domain): native SQL c ROW_NUMBER()
  per-dict cap. SQL injection защищён PreparedStatement param + extension
  whitelist (allowed_scopes — text[]).
- New SearchController (ordinis-rest-api): GET /api/v1/search?q=&size&perDict.
  Default size=100, perDict=10. Min q length=3 (silently empty if shorter).
  Scope filtering through ScopeContext — каждый caller видит только свои
  scope levels. Results grouped per dict с display name + count + items.

Admin UI:
- New /search route с SearchInput + grouped Panel results.
- URL state ?q=… — share-friendly link "/search?q=SAR-X".
- Per-result link → /dictionaries/{dict}?q={businessKey} для drill-down.
- Min-3 hint, empty state, loading + error.
- New "Поиск" / "Search" tab в navigation header.
- i18n RU (с правильными plurals search.totalHits) + EN.

Behavior:
- Active records only (valid_from <= now() < valid_to).
- Per-dict cap 10 — защита от dominant-dict skew (один dict с 1000 matches
  не забьёт результат).
- Total cap 100 — admin "find this code" use case достаточно. Browser-style
  search-as-you-type (10k+ matches, instant) — отложен на v2 с dedicated
  index server (Elastic / Meili).

Verify:
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS (migration applied).
- pnpm tsc --noEmit: clean.
- pnpm test (vitest): 89/89 PASS.
- pnpm build: clean.

После migration apply'я index size на dictionary_records будет ~30-40%
data column. На текущих 5k records ~3-5 MB, acceptable.
2026-05-08 12:53:44 +03:00