Project uses Altum-style colon-separated realm roles:
- ordinis:client:public / internal / restricted (scope ACL, уже active)
Schema workflow roles (Phase 1d TODO) приводим к той же convention:
- ordinis:schema:reviewer (был ordinis-schema-reviewer)
- ordinis:schema:publisher
- ordinis:schema:maker
Изменения в usePermissions.ts:
- JSDoc обновлён с новой convention
- Constants ROLE_SCHEMA_* для discoverable rename'а в Phase 1d
- TODO comments указывают на constants, не bare strings
Phase 1d (когда подключится backend role check):
1. Uncomment role check в hooks
2. Backend SchemaDraftService.decide() — require role
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).
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.
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).
Два бага в 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
автора и кнопкой 'Отозвать' справа внизу.
Бэйдж 'На ревью' в 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
[skip ci] в commit message skip'ает GitLab pipeline для ВСЕХ ref'ов на
этом SHA — включая tag pipeline. Эффект: semantic-release создаёт v*.*.*
tag, но tag pipeline скипается → images с tag'ом v*-*-*-<sha> не
собираются → propose-prod-image-bump (этот MR) не имеет images для bump.
Confirmed на v2.16.0 (commit bf07d6b): tag pipeline 6814 = skipped,
registry без 'v2-16-0-*' tags → невозможно deploy.
Loop prevention для chore(release) commit'ов работает через releaseRules
('chore': release: false) — semantic-release на этом commit'е detect'ит
'no releasable commits since previous tag' → exit 0 → no new tag → no loop.
Cost: docker-admin-ui ребилдит на каждом chore(release) commit (package.json
+ CHANGELOG.md match frontend-changes path-filter) — ~2 мин wasted CI time,
приемлемо vs broken release flow.
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)