Александр Зимин
b428c78b18
fix(auth): permitAll /api/v1/version (UI polling работает до login)
2026-05-10 18:48:55 +00:00
Александр Зимин
deb61a1dea
Merge branch 'feat/catalog-hub-view' into 'main'
...
feat(admin-ui): Hub view (B) per design handoff — dict relations 3-col layout
See merge request 2-6/2-6-4/terravault/ordinis!20
2026-05-10 18:41:37 +00:00
Александр Зимин
1df232679d
feat(admin-ui): Hub view (B) per design handoff — dict relations 3-col layout
2026-05-10 18:41:37 +00:00
Александр Зимин
6b71b651e6
Merge branch 'fix/catalog-toolbar-compact' into 'main'
...
fix(admin-ui): compact catalog toolbar (single row)
See merge request 2-6/2-6-4/terravault/ordinis!19
2026-05-10 18:30:49 +00:00
Александр Зимин
5afc233a4b
fix(admin-ui): compact catalog toolbar (single row)
2026-05-10 18:30:49 +00:00
Александр Зимин
e0292aea45
Merge branch 'feat/catalog-fk-chips' into 'main'
...
feat(admin-ui): catalog FK chips на cards (← N использ.)
See merge request 2-6/2-6-4/terravault/ordinis!18
2026-05-10 18:26:30 +00:00
Александр Зимин
49ab6c9340
feat(admin-ui): catalog FK chips на cards (← N использ.)
2026-05-10 18:26:30 +00:00
Александр Зимин
2ec45acc6f
Merge branch 'feat/catalog-list-with-fk' into 'main'
...
feat(admin-ui): catalog list view per design handoff (D-list-view)
See merge request 2-6/2-6-4/terravault/ordinis!17
2026-05-10 18:05:21 +00:00
Александр Зимин
75e844eb98
feat(admin-ui): catalog list view per design handoff (D-list-view)
2026-05-10 18:05:21 +00:00
Александр Зимин
71b20c6a09
Merge branch 'docs/design-handoff-catalog-v2-richer' into 'main'
...
docs(admin-ui): rich design handoff Dictionary Catalog (3 views + nstart/ui patch)
See merge request 2-6/2-6-4/terravault/ordinis!16
2026-05-10 17:59:01 +00:00
Александр Зимин
ddbe7368a2
docs(admin-ui): rich design handoff Dictionary Catalog (3 views + nstart/ui patch)
2026-05-10 17:59:01 +00:00
Александр Зимин
c60a6b82c5
Merge branch 'feat/catalog-v2' into 'main'
...
feat(admin-ui): Dictionary Catalog v2 (URL filters + sort + density)
See merge request 2-6/2-6-4/terravault/ordinis!15
2026-05-10 17:37:47 +00:00
Александр Зимин
7e460ebff7
feat(admin-ui): Dictionary Catalog v2 (URL filters + sort + density)
2026-05-10 17:37:47 +00:00
Александр Зимин
7ad358a6f1
Merge branch 'fix/version-banner-frontend-commit' into 'main'
...
fix(admin-ui): pass git commit к vite через Docker ARG
See merge request 2-6/2-6-4/terravault/ordinis!13
2026-05-10 17:33:44 +00:00
Александр Зимин
215ba678ac
fix(admin-ui): pass git commit к vite через Docker ARG
2026-05-10 17:33:44 +00:00
Александр Зимин
69a2477bff
Merge branch 'fix/dict-detail-toolbar-wrap' into 'main'
...
fix(admin-ui): dict detail toolbar — wrap buttons вместо ломки текста
See merge request 2-6/2-6-4/terravault/ordinis!14
2026-05-10 17:30:38 +00:00
Александр Зимин
f5fb2b70a6
fix(admin-ui): dict detail toolbar — wrap buttons вместо ломки текста
2026-05-10 17:30:38 +00:00
Александр Зимин
5ecfdb3c08
Merge branch 'fix/build-info-null-git-properties' into 'main'
...
fix(build): pass git info via Docker build args (unblock main pipeline)
See merge request 2-6/2-6-4/terravault/ordinis!12
2026-05-10 17:10:45 +00:00
Александр Зимин
d740092d0c
fix(build): pass git info via Docker build args (unblock main pipeline)
2026-05-10 17:10:45 +00:00
Александр Зимин
6a6441d169
Merge branch 'feat/version-banner' into 'main'
...
feat: version banner UI + GET /api/v1/version backend endpoint
See merge request 2-6/2-6-4/terravault/ordinis!11
2026-05-10 16:50:50 +00:00
Александр Зимин
e8f98230f2
feat: version banner UI + GET /api/v1/version backend endpoint
2026-05-10 16:50:50 +00:00
Александр Зимин
17386ba0b4
Merge branch 'docs/design-handoff-dictionary-catalog' into 'main'
...
docs(admin-ui): design handoff для Dictionary Catalog v2
See merge request 2-6/2-6-4/terravault/ordinis!10
2026-05-10 16:45:26 +00:00
Александр Зимин
94df199529
docs(admin-ui): design handoff для Dictionary Catalog v2
2026-05-10 16:45:26 +00:00
Александр Зимин
11ec6bd43a
Merge branch 'ci/dockerfile-future-proof-modules' into 'main'
...
ci(docker): future-proof backend Dockerfiles (fix root cause from Phase A break)
See merge request 2-6/2-6-4/terravault/ordinis!9
2026-05-10 16:23:04 +00:00
Александр Зимин
51d6709fb3
ci(docker): future-proof backend Dockerfiles (fix root cause from Phase A break)
2026-05-10 16:23:03 +00:00
Александр Зимин
58efa5cfbd
Merge branch 'fix/admin-ui-critical-review-issues' into 'main'
...
fix(admin-ui): top-5 critical review bugs (auth + AJV + TZ + idempotency + storage)
See merge request 2-6/2-6-4/terravault/ordinis!7
2026-05-10 16:22:35 +00:00
Александр Зимин
fc2fb35717
fix(admin-ui): top-5 critical review bugs (auth + AJV + TZ + idempotency + storage)
2026-05-10 16:22:35 +00:00
Александр Зимин
f2614244bf
Merge branch 'fix/dockerfile-include-ordinis-notifications' into 'main'
...
fix(docker): include ordinis-notifications in 3 Dockerfiles (unblock main pipeline)
See merge request 2-6/2-6-4/terravault/ordinis!8
2026-05-10 15:57:52 +00:00
Александр Зимин
3d5304b308
fix(docker): include ordinis-notifications in 3 Dockerfiles (unblock main pipeline)
2026-05-10 15:57:52 +00:00
Александр Зимин
0275e78907
Merge branch 'feat/notifications-phase-a-foundation' into 'main'
...
feat(notifications): Phase A foundation — module + schema 0021
See merge request 2-6/2-6-4/terravault/ordinis!5
2026-05-10 15:41:34 +00:00
Александр Зимин
3b7b1bffe5
feat(notifications): Phase A foundation — module + schema 0021
2026-05-10 15:41:34 +00:00
Александр Зимин
419e9b9b01
Merge branch 'ci/serialize-docker-builds-runner-disk' into 'main'
...
ci(docker): serialize docker builds + prune cache
See merge request 2-6/2-6-4/terravault/ordinis!6
2026-05-10 15:39:26 +00:00
Александр Зимин
38a5c19452
ci(docker): serialize docker builds + prune cache
2026-05-10 15:39:26 +00:00
Александр Зимин
c785d1e186
Merge branch 'chore/branding-strip-csod-from-logo' into 'main'
...
chore(admin-ui): strip "НСИ ЦУОД" from logo
See merge request 2-6/2-6-4/terravault/ordinis!4
2026-05-10 15:20:56 +00:00
Александр Зимин
02167e29f6
chore(admin-ui): strip "НСИ ЦУОД" from logo
2026-05-10 15:20:56 +00:00
Александр Зимин
c675c5e310
Merge branch 'fix/nginx-drafts-routing-staging' into 'main'
...
fix(admin-ui): route /api/v1/drafts/* to writer
See merge request 2-6/2-6-4/terravault/ordinis!3
2026-05-10 14:48:43 +00:00
Александр Зимин
3547b86a3e
fix(admin-ui): route /api/v1/drafts/* to writer
2026-05-10 14:48:43 +00:00
Александр Зимин
7c1a469123
Merge branch 'ci/no-cache-blob-recovery' into 'main'
...
ci(docker): add --no-cache to bypass stale inline cache from missing blob
See merge request 2-6/2-6-4/terravault/ordinis!2
2026-05-10 12:33:57 +00:00
Zimin A.N.
7909588304
ci(docker): add --no-cache to bypass stale inline cache from missing blob
...
After 44395e2 added --pull, pipeline 6032 still fails on docker-app/
projection-writer/read-api with same blob 74e368... missing. --pull pulled
base image fine, but BuildKit reads BUILDKIT_INLINE_CACHE из previously-pushed
:latest (которое references the lost blob). BuildKit thinks "this blob already
in registry, skip push" → manifest references missing blob.
--no-cache форсирует rebuild from scratch без inline cache reading. Все blobs
пересобираются → pushed заново → manifest is consistent.
Stack +2 min per service (3 services = +6 min total pipeline). Acceptable
до восстановления Nexus blob storage health. Когда :latest references stable
blobs снова — оба флага можно убрать одной revert командой.
Sister fix к 44395e2 .
2026-05-10 15:10:46 +03:00
Александр Зимин
62152cdeb6
Merge branch 'ci/fix-nexus-blob-cache' into 'main'
...
ci(docker): force --pull on base images to recover from Nexus blob loss
See merge request 2-6/2-6-4/terravault/ordinis!1
2026-05-10 12:01:31 +00:00
Zimin A.N.
44395e27e4
ci(docker): force --pull on base images to recover from Nexus blob loss
...
Pipeline 6017 (07c5ca0 , main) failed on docker-app/projection-writer/read-api
all referencing missing blob sha256:74e368405305a59e71a1fec5de6a75cc307bd663ebb9b47af045c3a5bdf30cec
после Nexus disk-full cleanup. Blob — это shared layer от base image
(eclipse-temurin:21-jre / maven:3.9.9-eclipse-temurin-21), который Nexus
docker-proxy кэш потерял. BuildKit на runner'е держал stale reference в своём
inline cache, не push'ал blob, manifest fail.
`docker build --pull` форсирует Nexus proxy лениво re-fetch base image с
Docker Hub на каждом build'е → blob возвращается в storage. После одного
успешного pipeline'а можно убрать флаг (cache восстановится), но flag
безвреден в healthy state — стоит ~5-10s на проверку pull.
Не меняет behaviour приложения. CI/runtime fix only.
2026-05-10 14:44:16 +03:00
Zimin A.N.
07c5ca04c9
docs(release): v1.1.0 release notes draft + tag message
...
Готовит ground для тага v1.1.0 после окончания Approval Workflow v2 soak'а
на staging (≥7 дней с 2026-05-08 → target ≥2026-05-15).
- docs-internal/releases/v1.1.0.md — полные release notes:
Track A (Approval Workflow v2 P0–P4, 8 commits) + Track B (5 CEO v1
gap closures), migrations 0017-0020, API surface additions, decisions
D1-D4 rationale, 6 lessons learned, cluster ops checklist, out-of-scope
list (deferred к v3 design doc).
- docs-internal/releases/v1.1.0-tag-message.txt — готовый текст для
`git tag -a v1.1.0 -F docs-internal/releases/v1.1.0-tag-message.txt`.
NOT тагаем сейчас — soak ahead. Documentация заранее = меньше work
после soak conclusion. Если в soak найдутся issues, release notes
проще обновить чем писать с нуля.
2026-05-10 12:31:57 +03: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.
d229c545f9
feat(approval): bundle-driven approval policy + spacecraft/ground_station soak flag
...
Approval Workflow v2 Phase 3 — soak prep:
- BundleManifest.DictionaryEntry: добавлены опциональные approvalRequired
(Boolean) и approvalMinRole (String).
- CuodBundleImporter: применяет policy на fresh + on-update + при стабильной
схеме (flip без bumping schema_version). Apply ТОЛЬКО когда явно изменился
→ не штормим UPDATE'ами.
- CuodBundleProperties.honorApprovalPolicy (default false): env-gated guard.
Bundle ship'ит approvalRequired:true для пилотных dicts, prod игнорирует
пока не явно включит.
- manifest cuod 1.3.0 → 1.3.1: spacecraft + ground_station получили
approvalRequired:true. Без honor flag — no-op.
Этого commit'а достаточно для соответствующего helm change в
ordinis-infra (bundleHonorApprovalPolicy=true в values-staging) — flip'ает
spacecraft/ground_station на staging при следующем deploy. Prod остаётся
default=false до окончания soak.
2026-05-08 14:59:34 +03:00
Zimin A.N.
9ef9b8147e
feat(approval): Approval Workflow v2 Phase 3 — e2e suite + 0020 case fix
...
Phase 3 e2e coverage (8 tests, ApprovalWorkflowE2ETest):
- happyFlow: maker submit → checker approve → live record + outbox event
- rejectFlow: keeps draft REJECTED, no live record, re-approve blocked
- selfApprove: same JWT subject blocked (self_approve_forbidden / self_reject_forbidden)
- twoPendingDrafts: exclusion constraint → 409 draft_already_pending
- directCRUD on approval_required dict: POST/PUT/DELETE → 409 draft_required (D2=A)
- regression: dict без approval_required — direct CRUD по-прежнему работает
- withdrawByMaker: maker может, reviewer не может (403 not_draft_maker)
- rejectWithoutComment: 400 reject_reason_required
E2e exposed real Phase 0 bug:
Hibernate @Enumerated(EnumType.STRING) пишет UPPERCASE ('PENDING'), а
Liquibase 0019 check constraint + exclusion WHERE использовали lowercase
('pending'). Каждый insert проваливался на check, маппился в
DataIntegrityViolation, и DraftService.submit маскировал его в
draft_already_pending — скрывая реальную причину.
Fixes:
1. Migration 0020: пересоздание record_drafts_status_check + exclusion
с UPPERCASE значениями (matches enum serialization).
2. DraftService.submit: только SQLSTATE 23P01 (exclusion violation)
маппится в draft_already_pending. Остальные DataIntegrity (check, FK,
NOT NULL) пробрасываются как-есть — иначе schema bugs продолжат
маскироваться.
3. DraftServiceTest StubDraftRepo: оборачивает SQLException 23P01 в
DataIntegrityViolation cause chain — отражает прод поведение.
Test counts:
- ordinis-rest-api unit: 119/119 PASS
- ordinis-app e2e: 28/28 PASS (был 20, +8 новых ApprovalWorkflowE2ETest)
2026-05-08 13:52:17 +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.
2020af99b7
feat(approval): Approval Workflow v2 Phase 1 — full lifecycle + approval gate
...
Phase 1 на скелете Phase 0 (e438c4c ). Делает workflow рабочим end-to-end:
maker submit → reviewer approve/reject → COPY → live record + outbox event.
Backend:
- DictionaryRecordService:
* requireApprovalGate() — pre-check на create/update/close. Throws 409
draft_required если dict.approval_required=true с pointer на drafts API.
* applyApprovedCreate/Update/Close — public bypass methods для DraftService.
Обходят approval check (drafts уже прошли review). Schema validation
повторяется ON APPROVE — schema может measure'ся между submit и approve
(failure modes #2 из design doc).
* createDirect/updateDirect/closeDirect — internal helpers (private),
extract'ed из public методов.
- DraftService:
* approve(draftId, comment) — SERIALIZABLE tx. Validates pending status,
self-approve guard (D3 maker-checker), dispatches на applyApproved*
based on draft.operation. Marks draft APPROVED + reviewer fields
(D4=A: kept в таблице для compliance).
* reject(draftId, comment) — non-blank reason required (compliance).
Marks REJECTED + reviewer fields. Никаких applyApproved calls.
* withdraw(draftId) — maker-only. 403 not_draft_maker если другой user.
Marks WITHDRAWN.
* Constructor split: production-ctor (с recordService) + test-ctor
(без recordService — для unit tests submit/list path). @Autowired
отмечает canonical для Spring.
- DraftController endpoints:
* POST /api/v1/drafts/{id}/approve?comment=
* POST /api/v1/drafts/{id}/reject?comment=
* DELETE /api/v1/drafts/{id} — withdraw
DTO surface — admin UI токglet'ы для approval policy:
- DictionaryResponse: +approvalRequired, +approvalMinRole.
- CreateDictionaryRequest: optional Boolean approvalRequired, String
approvalMinRole (null = no change в update).
- DictionaryDefinitionService.create + updateSchema: применяют поля.
- DictionaryDefinitionService.findById — added (нужно DraftService.approve
чтобы взять def name по id).
Tests:
- DraftServiceTest 13 unit tests (+7 new для Phase 1):
* approve dispatches CREATE/UPDATE/CLOSE per draft.operation
* approve blocked при self-approve (maker == reviewer)
* approve 409 если draft уже approved/rejected
* reject marks с reason; reject 400 если reason пустой
* withdraw marks WITHDRAWN; 403 если не maker
* + 6 Phase 0 tests от прошлого commit'а сохранены
TrackingRecordService stub — extends DictionaryRecordService с null deps,
overrides applyApproved* для записи calls в list. Verifies dispatch без
real DB.
Verify:
- mvn -pl ordinis-rest-api -am test: 119/119 PASS (+7 new).
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS — approval gate не
ломает existing flow когда approval_required=false (default).
Phase 2 (next): admin UI reviewer queue + diff viewer + status badges.
Phase 3: e2e/integration tests + per-dict opt-in soak.
Source: ~/.gstack/projects/claude/zimin-main-design-approval-workflow-v2-20260507-121632.md
2026-05-08 13:19:24 +03:00
Zimin A.N.
e438c4c8c8
feat(approval): Approval Workflow v2 Phase 0 — schema + skeleton API
...
Approval Workflow v2 epic, Phase 0 foundation. Maker-checker pattern для
критичных справочников через record_drafts отдельную таблицу (Approach A
из design doc).
Decisions made (auto, per design doc recommendations):
- D1=A: per-dict policy через колонки на dictionary_definitions
(approval_required + approval_min_role).
- D2=A: legacy POST на approval-required dict вернёт 409 (Phase 1 wire'ит
в DictionaryRecordService).
- D3=A: pool model для reviewer'ов — все ordinis:approver видят queue.
- D4=A: rejected drafts kept (status='rejected'); compliance audit trail.
Migration 0019:
- record_drafts table:
* id, dictionary_id, business_key, parent_record_id, operation
* data (jsonb), geometry, proposed_valid_from/to
* status, maker_id, maker_comment, submitted_at
* reviewer_id, reviewed_at, review_comment, version
- 4 constraints:
* status_check: pending/approved/rejected/withdrawn
* operation_check: CREATE/UPDATE/CLOSE
* no_self_approve: maker_id != reviewer_id
* one_pending_per_key: EXCLUDE WHERE status=pending
(concurrent edits → последний wins на submit, prevents merge conflicts)
- 3 indices: pending_by_dict, by_maker, pending_global.
- dictionary_definitions: +approval_required (BOOLEAN, default false),
+approval_min_role (VARCHAR 64, optional Keycloak role).
Backend:
- Entity RecordDraft + Repository с JPQL queries для:
* findPendingByKey (single pending lookup)
* findPending (global queue для approver pool, paginated)
* findPendingByDictionary (per-dict list)
* findByMaker (own drafts tracking, paginated)
- DTO SubmitDraftRequest, DraftResponse.
- DraftService skeleton:
* submit() — creates pending draft. 409 на exclusion violation.
* findById, listPendingByDictionary, listPendingGlobal — read-only.
* approve / reject / withdraw / updateDraft — Phase 1 stubs (комментарии).
- DraftController с 4 endpoints:
* POST /api/v1/dictionaries/{name}/records/drafts — submit
* GET /api/v1/dictionaries/{name}/records/drafts — list per dict
* GET /api/v1/drafts/{id} — single draft
* GET /api/v1/admin/reviews/pending — global queue (paginated)
Tests:
- DraftServiceTest 6 unit tests:
* submit creates with PENDING status
* 409 на duplicate pending business_key (exclusion constraint)
* invalid WKT → 400
* dict not found → 404
* findById unknown → 404
* listPendingByDictionary returns multiple pending
- StubDraftRepo simulates DB exclusion constraint в memory.
Verify:
- mvn -pl ordinis-rest-api -am test: 112/112 PASS (+6 new).
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS — migration 0019
applies clean, no regression.
- helm/yaml configs не трогали — Phase 0 чистый schema + code.
Phase 1 (next session) — full lifecycle:
- DraftService.approve(): SERIALIZABLE tx с COPY draft → live + outbox
RecordCreated/Updated/Deleted event + audit. Validate against schema
ON APPROVE (могла schema измениться с момента submit'а).
- reject() — mark status=rejected, kept (D4).
- withdraw() — maker отзывает свой pending.
- DictionaryRecordService.create/update/close: pre-check
approval_required → 409 draft_required. JWT subject → maker_id.
- Reviewer queue + diff viewer admin UI.
Source: ~/.gstack/projects/claude/zimin-main-design-approval-workflow-v2-20260507-121632.md
2026-05-08 13:08:21 +03:00