Phase 1a из плана: только swap библиотеки, realm/URL остаются на
auth.nstart.space/realms/nstart. Phase 2 (intgr с altum auth-service)
— отдельный эпик.
Зачем. oidc-client-ts с automaticSilentRenew триггерил бесконечный
POST /token loop когда refresh_token истекал (см. MR !269 revert,
~30+ 400'к в console, страница залипала на 403). Altum давно ушёл
с oidc-client-ts на keycloak-js по той же причине.
Новая модель.
- src/lib/keycloak.ts — singleton + initKeycloak(check-sso, PKCE) +
ensureFreshToken(N) wrapper над kc.updateToken(). Зеркало
altum/src/lib/keycloak.ts.
- src/stores/authStore.ts — Zustand store: {user, isAuthenticated,
isLoading, error}. checkAuth() ждёт initKeycloak, wireKeycloakEvents()
→ onAuthSuccess/Refresh/Logout/TokenExpired re-снэпшотят store.
- src/auth/useAuth.ts — compat shim для react-oidc-context API
(auth.user.profile, isAuthenticated, signinSilent, signinRedirect,
signoutRedirect, removeUser, error). Все 11 callsites продолжают
работать без правок (только сменили путь import'а).
- src/api/client.ts — request interceptor дёргает ensureFreshToken(30)
ПЕРЕД каждым запросом + attaches Bearer ${getToken()}. 401 interceptor
делает ensureFreshToken(0) + retry один раз. Никаких таймеров, никаких
setAccessToken/setUnauthorizedHandler holder'ов — keycloak-js сам source
of truth.
- src/main.tsx — убран <AuthProvider>, <TokenSync />. Просто
useAuthStore.getState().checkAuth() на старте, потом обычный render.
- public/silent-check-sso.html — endpoint для silentCheckSsoRedirectUri.
Удалены.
- src/auth/TokenSync.tsx — не нужен, ensureFreshToken на запросах.
- src/auth/oidcConfig.ts — не нужен, конфиг внутри lib/keycloak.ts.
- npm deps: react-oidc-context, oidc-client-ts.
Добавлены deps: keycloak-js@26.2.4, zustand@5.0.13.
Tests: 171/171 passed, TSC чистый.
Migration callsites — все unchanged по семантике благодаря compat shim.
Цифра у иконки словарей показывала общее число справочников (~40 на проде).
Не меняется в течение работы юзера — не actionable, только визуальный шум.
Actionable badges остаются: «Мои черновики», «На ревью», «Outbox», «Webhooks» —
там цифра реально что-то означает (надо что-то сделать или разобраться).
Заодно убрал unused dictsQuery / dictsCount + import useDictionaries из обоих
сайдбаров (desktop + mobile). TSC noUnusedLocals чистый.
Badge в сайдбаре (desktop + mobile) показывал totalElements от /drafts/me
и /admin/schema-drafts/me — это сумма ВСЕХ статусов (PENDING + APPROVED +
REJECTED + WITHDRAWN). После approve/reject счётчик не уменьшался, юзер
видел «висящие» цифры на драфтах которые уже terminal.
Tab-бейдж в ReviewsPage давно делает правильно через classifyRecordStatus
/ classifySchemaStatus. Подтягиваем sidebar к той же логике:
- fetch size 50 вместо 1 (нужны .items[] для filter; TanStack дедуплицирует
query — на /reviews этот же ключ уже зовётся, лишних запросов нет)
- record → 'pending', schema → 'pending'|'wip'
- approved schema всё ещё попадает в pending по дизайну (maker должен
publish'нуть; см. classifySchemaStatus комментарий)
В collapsed-режиме (56px полоска) рендерили только иконку без label
и без badge — счётчик pending записей/драфтов был не виден, юзер не
понимал что есть необработанные элементы пока не раскроет сайдбар.
Теперь в collapsed: badge → маленький круглый pill в углу иконки
(top-right, 16×16, ring-1 от bg чтобы хорошо отделялся от иконки).
Cap "99+" если число большое — uniform width. Tooltip на link
включает label + (count) при hover.
Expanded-mode без изменений — badge справа от label как был.
Восемь дефектов из QA-отчёта одним MR.
#1 — Records-table теряла первую колонку после закрытия Create-modal.
Корень: lg:grid-cols-[220px_1fr] без minmax(0,…) — 1fr имел implicit
min-content (=max-content), позволяя table вырасти шире viewport.
После закрытия Radix Dialog снималось body padding-right (~15px), grid
пересчитывался и клиппил колонку под overflow-hidden card.
Fix: grid-cols-[220px_minmax(0,1fr)] + убран overflow-hidden с card
(Table primitive имеет свой overflow-x-auto).
#2 — Esc закрывал modal с задержкой (визуал/DOM рассинхрон).
duration-200 на slow CPU ощущался как несколько кадров между state=closed
и unmount. Снижено до duration-150 — совпадает с tw-animate-css дефолтами.
#3 — «+ Создать» обрезалось до «+ Соз» на ~1075px на detail-странице.
EditorTabBar: 6 tabs + 3 кнопки actions в одном flex row без shrink-0
давили друг друга. Решение: tabs в overflow-x-auto wrapper с min-w-0,
actions в shrink-0 контейнере — tabs скроллятся, actions всегда видны
полностью. scrollbar скрыт визуально (height-консистентность).
#4 — Обрезка названий в records-table при PUBLIC+INTERNAL.
Добавлен div.overflow-x-auto вокруг table + min-w-[760px] на table +
min-w-[180px] max-w-[320px] truncate на name cell. Catalog table также
получает min-w-[220px] на name column.
#5 — Заголовки колонок и кнопка «Граф» не локализованы на EN.
defaultValue:'Название' inline в коде падал на ru-text при EN locale.
Вынесены в i18n: dict.col.title/id/bundle/records/updated/outgoingFk/
incomingFk + dict.list.graph для обеих локалей.
#6 — Modal backdrop почти невидим.
bg-black/40 + 1px blur → bg-black/55 + 2px blur. Заметно темнее при
сохранении читабельности фона, фокус на modal.
#7 — Неестественная подсказка в форме «Создать»:
«Имя оставьте уникальное; версия сбросится на 1.0.0» →
«Укажите уникальное имя — версия сбросится на 1.0.0».
#8 — «Главная» в сайдбаре сбивала active state.
Клик на «Главную» вёл на `/` → redirect на /dictionaries → подсветка
уезжала на «Справочники». Logo сверху уже даёт home affordance, поэтому
duplicate nav item удалён (desktop + mobile).
Phase 1d enhancement: 'Согласования' пункт сайдбара скрыт если у user'а
нет approver/reviewer ролей. Раньше показывался любому authenticated —
оператор без reviewer прав видел queue, кликал и получал пустую
страницу (или 403 если бы пробовал approve).
Visibility rules:
- 'Мои черновики' — auth (maker может быть любой)
- 'Согласования' — auth + (ordinis:schema:reviewer OR ordinis:record:reviewer
OR ordinis:admin super-role) — иначе hidden
- 'Outbox' / 'Webhooks' / 'Аудит' — auth (admin-y но backend не gated
отдельной ролью)
Новый NavItem.reviewerOnly flag — фильтр в visibleSections учитывает
оба flag'а (authRequired + reviewerOnly).
Применено и к desktop Sidebar и к MobileSidebar (DRY refactor — отдельный
task).
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).
Бэйдж 'На ревью' в 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
stroke=5 в viewBox 256 при 28px render = 5*28/256 = 0.55px (subpixel,
почти невидимо). stroke=12 даёт ~1.31px real — чёткая линия. Заодно
переключил с --color-line-2 на --color-line чтобы немного контрастнее
на bg-surface.
Делёные design team 2026-05-11 в Downloads/files:
- ordinis-mark.svg — двойные орбиты + 6 dots + gradient core
(DeepBlue #185FA5 → SkyBlue #378ADD), brand spec
- ordinis-favicon.svg — minimal 32px variant
- ordinis-appicon.svg — 1024px PWA icon с dark rounded tile
- ordinis-lockup-light/dark.svg — horizontal lockup с wordmark
(для landing/splash/marketing — пока не используется но
хранится в public для future use)
Changes:
- public/favicon.svg → minimal 32px variant
- Sidebar logo block: inline ordinis-mark с двойными орбитами +
gradient core. Wordmark 'ORDINIS MDM' остаётся в Tektur.
- index.html: theme-color #185FA5 (был #120a8f), apple-touch-icon
link на appicon, mask-icon color match.
Note: SVG colors hardcoded в Sidebar inline для full design fidelity
(brand assets specifically defined gradient). currentColor не подходит
т.к. brand требует точные DeepBlue/SkyBlue/LightBlue tokens.
Follow-up к MR !90 per user feedback iterations:
1) "в поиске по справочникам два крестика" — fixed в SearchInput:
`type="search"` (native browser X) → `type="text"`. Custom onClear X
остаётся единственным. Catalog input тоже type="text".
2) "блин два крестика было на топбар поиске а тут норм все. верни" —
restore TopBar search input. Context-aware behavior сохранён:
- На /dictionaries → live filter каталога через URL ?q=
- На остальных routes → submit (Enter) → /search records search
Single X confirmed by visual test.
3) "свитч темы можно тоже в профиль перенести" — ThemeSwitch переехал в
Sidebar footer (рядом с RU/EN button). Right row в footer: lang button
слева, theme switch (3 icons sun/moon/monitor) справа через ml-auto.
4) "руководство и версию оставить в топбар чтобы подсвечивать новинки" —
Docs + VersionBadge stays in TopBar. Туда же добавлена
NotificationsBell (placeholder Bell icon) — broadcast slot для
webhook errors / draft reviews / system notices когда backend
notifications stream появится.
Per /Users/zimin/Downloads/Ordinis (4)/redesign/compact.html (бэкграунд
прототипа уже #faf9f5 — match'ит наш neutral default).
Catalog table:
- ИЗМЕНЁН column показывает relative time: Xс / Xмин / Xч / Xд / Xмес / Xг.
Прошлая абсолютная дата (05/06/2026) затирала scale ("5 минут vs 3 месяца —
визуально одинаково далеко"). Прототип использует 2ч, 3д, 1мес.
- Chevron `›` column добавлен в конец каждой строки — affordance что row
кликабелен (открывает editor).
Sidebar:
- Flatten: убраны section labels (workflow / admin). Все nav items в одном
списке per redesign: Главная → Справочники → Мои черновики → На ревью →
Outbox → Webhooks → Аудит → Поиск.
- /graph удалён из sidebar — теперь reachable только через catalog toolbar
"Граф ⇄" button (single entry point, no duplication).
- Применено в обоих Sidebar (desktop) и MobileSidebar (drawer).
Cleanup: убран unused GitBranch icon import.
UI redesign matching prototype (Ordinis (3) dump):
- ExportModal: format (CSV/JSON/Excel/SQL) + scope (all/filtered/selected) +
column toggles + encoding/delimiter + preview filename per redesign line
1059-1092. CSV uses backend mutation, JSON client-side from filtered rows,
Excel/SQL disabled (backend pending).
- TimeTravelModal: full-page compare СЕЙЧАС vs ТОЧКА ВО ВРЕМЕНИ + quick
presets (сейчас/неделя/месяц/год/при создании) + step toggle версия/день
+ slider + tabs (Что изменилось / Записи на момент / Структура и поля) +
"Открыть как readonly" CTA. Replaces inline TimeTravelPicker.
- InfoPanel ← используют merge: rich rows с field path + active records
count + onClose policy chip (BLOCK/WARN/CASCADE). Inline
DictionaryDependentsPanel above records table removed (one source of truth).
- Sidebar logo: match redesign/ui-kit.html .brand SVG (outer ring + inner
faded ring + orbit dot offset right cx=22.5). ORDINIS Tektur tracking 0.22em
+ MDM smaller mute 0.32em.
- Bg neutralized: #fbf8ee Earth cream → #faf9f5 warm neutral default
(per user preference, Earth cream still described in comment как opt-in).
- WorkflowBanner: Case 1 Live (approvalRequired=false) returns null —
permanent "Опубликовано" banner = noise, status already visible в
InfoPanel scope/version/updatedAt. Banner показывается только когда
есть actionable state.
Critical bug fixes:
- fix(CascadeConfirmDialog): infinite render loop "Maximum update depth
exceeded". useEffect had cascadeMut (TanStack Query mutation wrapper) в
deps — wrapper identity changes every render → effect fires → reset() →
store update → render → loop. Extract stable `reset` reference перед
useEffect. Симптомы у user: создание/редактирование записи не работали.
- fix(semantic-release): add missing peer dep
conventional-changelog-conventionalcommits (9.3.1). semantic-release 22+
больше не bundles preset. Без неё release job падал с
"Cannot find module 'conventional-changelog-conventionalcommits'".
1) MODAL OFF-SCREEN (Schema/AOI/Confirm dialogs)
DialogContent had `inset-0 w-full h-full` + `min-[880px]:inset-auto`
конфликтовал — inset-0 winning по CSS specificity, modal приклеивался
к левому краю viewport вместо центра. User screenshot показал Schema edit
выходящим за левую границу экрана.
Fix: remove inset-0 fullscreen pattern. Use simple centered modal —
left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 + w-[95vw] max-h-[95vh]
overflow-y-auto. На mobile <880px max-w из size variant даёт ширину 95vw
(natural fit). На desktop max-w ограничивает до 440/560/720/880/920px.
2) LOGO 'ORDINIS MDM' С CIRCLE ICON (per redesign prototype)
Sidebar logo: 'ORDINIS' → circle icon (accent orbit ring + dot) +
'ORDINIS' bold + 'MDM' mute subtitle (Tektur). SVG inline — нет external
dep.
Collapsed mode: только circle icon, no text (uses Tektur ORDINIS dropped).
3) TIMELINE BUTTON LABEL
InfoPanel button 'Time-travel…' → 'Таймлайн' (per user feedback). Behaviour
не меняется — клик открывает picker. Inline таймлайн visualization
отложен (большой refactor, нужен product decision).
Verified:
- Dialog modal центрируется на десктоп
- Logo рендерится с svg + text
- Tests: 116 pass
Handoff prototype показывает «‹ Свернуть» button внизу sidebar — toggle
desktop rail в icon-only 56px. Persists в localStorage ord-sidebar-collapsed.
Sidebar component:
- State collapsed (localStorage 'ord-sidebar-collapsed' = '1')
- <aside width> transitions между w-[220px] и w-[56px] (200ms ease)
SidebarContent (DRY с MobileSidebar) получил props:
- collapsed?: boolean (default false)
- onToggleCollapsed?: () => void (undefined hides button — mobile drawer)
Когда collapsed:
- Logo: 'ORDINIS' → 'O' (single char, centered)
- Section labels (text-cap headers) — hidden
- Nav links: icon-only, centered, icon size 18px (vs 15px expanded);
title attribute с label для hover tooltip
- Badges (records count, pending) — hidden (нет места)
- User block: avatar only, name/email hidden; title attribute с full name
- Toggle button bottom: '‹ Свернуть' → '›' (icon-only)
Mobile (MobileSidebar Sheet) НЕ получает collapse — там Sheet даёт full
nav text всегда, close через × header.
A11y: aria-label / title для всех collapsed buttons. Click handler
sets localStorage + state — survives F5.
Tests: 116 pass. TS strict clean.
Per design_handoff_ordinis_mdm/README.md responsive table (line 464-465):
- ≤1024px: Sidebar becomes overlay drawer (hamburger в TopBar)
- ≤880px: Drawers и modals fullscreen (100vw × 100dvh, no border-radius)
Implementation:
src/components/layout/Sidebar.tsx
- Extracted SidebarContent (logo + nav + user block) для DRY
- Desktop <Sidebar /> wraps SidebarContent в hidden lg:flex aside
- New <MobileSidebar open onClose /> — Sheet с тем же содержимым,
260px wide, slides from left
- SidebarLink принимает onClick → drawer auto-close при navigate
src/components/layout/TopBar.tsx
- Optional onMenuClick prop — рендерит hamburger button (Menu icon
из lucide-react) с lg:hidden
- VersionBadge скрыт <md (768px) — diagnostic info, не critical UX
src/routes/__root.tsx
- useState mobileNavOpen, передаём в MobileSidebar + TopBar
src/ui/components/dialog.tsx
- SIZE_CLASSES получили prefix (arbitrary Tailwind v4
breakpoint per handoff exact 880px). На mobile <880px modal становится
fullscreen (inset-0 w-full h-full no rounded), ≥880px — centered с
translate + max-w из SIZE_CLASSES.
src/ui/components/sheet.tsx
- size variants: sm:max-w-* → min-[880px]:max-w-*. На mobile <880px
sheet берёт w-full (fullscreen drawer per handoff).
src/components/record/RecordDrawer.tsx
- max-width переключатель 520/880 теперь применяется только ≥880px.
На mobile drawer всегда fullscreen.
src/ui/components/language-switch.tsx
- Display fallback: opt.short → opt.label → uppercase id.
Previously показывал id ('ru-RU', 'en-US') когда short не был задан —
теперь использует label ('RU', 'EN') как fallback.
Browser-verified в preview viewport 375×812:
- Hamburger ☰ visible, click открывает Sheet с nav items
- Backdrop fade + slide-from-left animation
- TopBar fits в 375px (RU/EN compact, Sign in visible)
- LoadingBlock + Alert text-wrap correctly