feat(notifications): TODO 7 — draft decision toast + email + bell badge

This commit is contained in:
Александр Зимин
2026-05-14 14:40:35 +00:00
parent 50d263745a
commit 9050e2427e
21 changed files with 1992 additions and 82 deletions
+93 -69
View File
@@ -28,90 +28,114 @@ Closed in [MR !189](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/merg
---
## 5. DESIGN.md for the admin UI
## 5. DESIGN.md for the admin UI ✅ SHIPPED v0 baseline
**What.** Run `/design-consultation` skill or stub DESIGN.md manually to lock
the implicit design system: drawer-vs-modal usage rules, term glossary
(maker / reviewer / publisher and their Russian counterparts), Badge variant
mapping (CREATE → success, UPDATE → info, CLOSE → warning — already used
consistently but undocumented), code panes pattern (live = `bg-line/30`,
proposed = `bg-accent/4`).
Authored as `DESIGN.md` at repo root (2026-05-14). 12 sections covering:
voice & tone, color tokens (3 themes), typography, spacing/layout, component
primitives, badge variant mapping (operations / record status / schema status /
notifications), code/diff visual language, interaction states, term glossary,
RBAC, AI slop anti-patterns, open items для future /design-consultation sprint.
**Why.** Pass 5 rated design system alignment 4/10. The system *exists* — design
tokens are used consistently — but it's tribal knowledge. Next developer building
a feature like "Phase 4 webhook approvals" will guess and either match by luck
or drift.
**Pros.** Half-day work. Pays off on every new feature. Source of truth for
`/plan-design-review` calibration on future plans.
**Cons.** Documentation has to be maintained or it lies. Mitigated by keeping
DESIGN.md to invariants (tokens, primitives, semantic rules), not specific
components.
**Context.** User chose "defer to /design-consultation sprint" instead of
stubbing inline. The decision is to do it properly when the sprint happens,
not piecemeal during this review.
**Depends on / blocked by.** Nothing.
Full /design-consultation skill run deferred — v0 captures observed patterns
as fact-based source of truth; later sprint can refine policy gaps listed in
Section 12 (responsive breakpoints, motion durations, loading state taxonomy,
etc.).
---
## 6. A11y audit post-deploy via `/design-review`
## 6. A11y audit post-deploy via `/design-review` ⏳ Punch list captured 2026-05-14
**What.** After the in-flight approval workflow MRs deploy to staging, run
`/design-review` skill against the live `/reviews` page + DictionaryEditorDialog
flow. The skill does Lighthouse, axe, screen reader walk-through, contrast
audit, keyboard navigation testing.
Ran focused live audit on https://ordinis.k8s.265.nstart.cloud/reviews (staging
v2.25.x, anonymous + read-only state). Findings captured below; fix loop deferred
to standalone polish MR.
**Why.** Pass 6 rated responsive/a11y 5/10. Specific suspects:
- Button heights not verified at 44px touch target.
- `text-mute` contrast against `bg-surface` — might fail WCAG AA 4.5:1.
- Reject reason TextArea has no specified `minHeight`.
- Bulk action toolbar wrap behavior at 375px.
- Diff `<pre>` panes have no aria-label — screen reader reads JSON literally.
- Keyboard tab order through long diff panes — reviewer may have to tab 50 times
to reach approve buttons.
### P0 — Severe
**Pros.** Necessary for compliance contexts (gov contracts, accessibility laws).
Catches things static review misses (real screen reader, real touch device).
- **A11y-1: Bell badge "9+" lacks contextual aria-label.** Screen reader reads
literal "9+" without "unread notifications" context. The `<span>` carrying
the digit needs its own `aria-label={t('notifications.unreadCount', {count})}`.
TopBar.tsx NotificationsBell. (Already addressed in MR !190 — verify in deploy.)
- **A11y-2: i18n leak — "СВЕРНУТЬ" hardcoded RU.** When EN locale active
(sidebar reads Home/Dictionaries/Search), sidebar collapse button at bottom
still shows " СВЕРНУТЬ". Either missing translation key or hardcoded label.
Find in `Sidebar.tsx` near collapse handler.
**Cons.** Requires the in-flight MRs to be deployed first. Cannot do in
plan mode.
### P1 — High
**Context.** User chose "defer to /design-review post-deploy". The audit will
produce its own punch list with severity ratings.
- **A11y-3: Touch targets below WCAG 44×44 minimum.** Measured on staging:
- Bell button: 28×28
- Theme switches (Light/Dark/System): 28×26
- Lang switch button "EN": 37×28
- Sidebar nav items: ~32px tall
- Tabs: 78×40 (Records) and 112×40 (Schemas) — height below 44
- "Open →" action link in schema queue row: ~50×18 (text link only)
Mobile/touch use is uncomfortable. Add `min-h-[44px]` to icon buttons in
TopBar + Sidebar footer. Tab height needs `h-11` instead of `h-10`.
- **A11y-4: UserCell raw UUID fallback ugly when fetch fails.** `77ec2cc8`
shown literally in AUTHOR column when the user-display lookup 401s (anon)
or returns empty. Fall back to `t('user.unknown')` ("Неизвестный пользователь")
instead of UUID slice.
- **A11y-5: No `aria-live` on tab panel.** Switching Records → Schemas → Mine
doesn't announce content change to screen readers. Add
`aria-live="polite"` on the panel container or rely on Radix `<Tabs.Content>`
which already manages focus.
**Depends on / blocked by.** MRs !179, !180, !182, !183, !184, !185 deployed
to staging.
### P2 — Medium
- **A11y-6: Radix Tabs inline `style="outline:none"`.** Tablist has
`style="outline: none"` inline that defeats default focus ring. Verify
`:focus-visible` styling kicks in for keyboard nav — if not, remove the
inline override or add a Tailwind focus ring.
- **A11y-7: Anonymous Mine tab handling.** Need to verify what happens for
unauthenticated user clicking Mine — should redirect to /sign-in or render
"Sign in to see your drafts" empty state (current behavior likely error
toast or blank query).
- **A11y-8: Description text has no max-width.** "Pending drafts from makers
awaiting…" body copy stretches to viewport edge on wide screens (>1440px).
Apply `max-w-prose` for readable measure (45-75 chars).
### P3 — Polish
- **A11y-9: Column header tracking-wide ALL CAPS** ("DICTIONARY", "FROM
VERSION", "AUTHOR") creates uneven scan rhythm against lowercase data
rows. Verify against design tokens — likely intentional cap-style but
worth a visual review at narrow widths.
- **A11y-10: "Open →" link should be Button.** It's a text Link in Action
column; better as a `Button variant="ghost"` to align with the
existing Records tab "Review" CTA pattern.
### Status
Punch list compiled. Fix loop deferred — group into separate "a11y polish"
MR after current notifications work (!190) lands. Most fixes are 2-5 LOC
each except touch-target sweep which touches 5+ components.
---
## 7. Notifications service: draft decision toast + email
## 7. Notifications service: draft decision toast + email ⏳ MR !190 pending
**What.** Notifications service subscribes to outbox `RecordDraftApproved /
RecordDraftRejected / RecordDraftWithdrawn` events (now fired thanks to
MR !184) and:
Closed in [MR !190](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/merge_requests/190).
1. Fires email to maker with reviewer comment.
2. Sets a flag (DB or Redis) so on next login the admin UI shows a toast:
"Your draft for `test1/BK-42` was approved by reviewer X. The record is
now live."
3. Bell icon in the header gains a badge counter for unread decisions.
**Backend** (ordinis-notifications + ordinis-rest-api + ordinis-app):
- `NotificationsDispatcher` — mirrors WebhookDispatcher pattern, reads outbox
events of type RecordDraftApproved/Rejected/Withdrawn/Submitted, fan-outs
via IdempotencyGuard + EmailChannel
- `UserDisplayPort` SPI in notifications module (anti-corruption layer)
- `NotificationsUserDisplayBridge` wires port → UserDisplayService in
ordinis-app (avoids circular dep)
- `MeNotificationsController` GET /api/v1/me/notifications + POST /{id}/read +
/read-all
- Migration 0024 `notification_read_state(user_id, notification_id, read_at)`
- 5 unit tests (mock-maker-subclass for Java 25 compat)
**Why.** Maker has no synchronous feedback. They submit, then either:
- Tab "Мои" → Decided (covered by TODO 1) — but only when they come back
- This TODO — push notification so they know without re-visiting
**Frontend**:
- `useMyNotifications` query (30s refetch + background poll for badge)
- `useMarkNotificationRead` + `useMarkAllNotificationsRead` mutations
- `NotificationsDrawer` right-side drawer with per-row mark-read + footer
mark-all
- `NotificationsBell` replaces placeholder — unread badge (capped "9+") +
delta-toast on poll-detected new arrivals when drawer closed
- 14 ru-RU + 14 en-US i18n keys
**Pros.** Closes the maker feedback loop. Reuses the outbox infra. Mirrors
existing notification flows in `ordinis-notifications`.
**Cons.** New email template + new in-app toast surface + bell badge. Half-day
to a day of work split between notifications service and admin-ui.
**Context.** Pass 7 unresolved decision. User chose "in-app toast + email via
notifications service" over the lighter "tab badge polling" option. Backend
events already fire (MR !184), so the gate is consumer side.
**Depends on / blocked by.** MR !184 deployed. TODO 1 ("Мои" tab) helps but
is not a hard blocker — the toast and email work without that surface.
**Email templates** already existed (draft_submitted, draft_decision,
draft_withdrawn) in notifications_{ru,en}.properties — reused as-is.