Commit Graph

19 Commits

Author SHA1 Message Date
Александр Зимин fece673017 fix(dict): default-hide для existing users (v=2 storage) + sticky header/pagination 2026-06-02 12:22:19 +00:00
Александр Зимин b6190713e4 feat(dict): таблица справочника — hide tail columns по умолчанию (без H-scroll) 2026-06-02 12:14:00 +00:00
Александр Зимин c2e7dd0c87 feat(ui): dict table column sort + visibility toggle (localStorage) 2026-06-02 10:58:59 +00:00
Александр Зимин c1a006765f chore(ui): a11y touch-targets sweep + useUserDisplay JSDoc refresh 2026-05-27 15:49:17 +00:00
Zimin A.N. da0f894342 feat(auth): Phase 1b — swap default Keycloak realm на altum
Frontend default: https://auth.nstart.space/realms/nstarthttps://altum.nstart.cloud/auth/realms/altum. Shared SSO с altum portal.

Keycloak setup (Keycloak admin actions требуются ДО merge):
- В realm `altum` создать client `ordinis` (тип: public, standard flow)
- Redirect URIs:
    https://ordinis.nstart.cloud/*
    https://ordinis.k8s.265.nstart.cloud/*
    http://localhost:5173/*
- Web origins: те же
- Client-roles: ordinis-public, ordinis-internal, ordinis-restricted,
  admin (admin даёт RESTRICTED через ScopeContext.isAdminRole)

Backend issuerUri меняется отдельным MR в ordinis-infra
(charts/ordinis/values.yaml).

После настройки Keycloak + деплоя — старый realm auth.nstart.space/nstart
больше не используется. Все юзеры должны иметь учётки в realm altum
(вероятно brokered к nstart-id если SSO с New Start ID настроен).

171/171 tests, TSC чистый.
2026-05-26 13:11:49 +03:00
Zimin A.N. 7e435e07b7 feat(auth): миграция oidc-client-ts → keycloak-js (altum-pattern)
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.
2026-05-26 11:18:30 +03:00
Zimin A.N. 8c0b74cbf8 fix(admin-ui): не резолвить «anonymous» через /admin/users/{sub}/display
Backend currentUserId() возвращает литерал "anonymous" когда JWT отсутствует
(ORDINIS_AUTH_REQUIRED=false fallback). Audit / draft entries сохраняют
этот sub в БД. Frontend UserCell дёргал /admin/users/anonymous/display
→ 404 → catch → display "—". Логически ок, но в консоли DevTools мусорный
404 на каждом рендере таблицы.

Skip endpoint когда uuid === "anonymous":
- useResolveUserDisplay: enabled = uuid && uuid !== 'anonymous'
- useUserDisplay: short-circuit ветка возвращает display: 'anonymous'

UX без изменений (раньше показывал «—» с full UUID в tooltip; теперь
показывает «anonymous» без 404 в консоли).
2026-05-25 18:34:20 +03:00
Александр Зимин 5c4b9c9f21 fix(admin-ui): em-dash fallback в UserCell + sidebar.collapse i18n 2026-05-15 09:02:39 +00:00
Александр Зимин fb64ca4350 refactor(reviews): interaction state polish — 5 UX fixes 2026-05-14 14:13:57 +00:00
Александр Зимин 4c29f4e7cb refactor(approval-ux): eng-review fixes — extract helpers, add tests, register i18n 2026-05-14 13:52:12 +00:00
Александр Зимин be90077520 feat(users): /admin/users/{sub}/display endpoint + UserCell uses it 2026-05-13 16:42:46 +00:00
Zimin A.N. e6294ce79c fix(ui): shared UserCell для display UUID actor'ов везде
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.
2026-05-13 11:43:04 +03:00
Zimin A.N. 9e2d985485 fix(ui): tailwind-merge стрипал text-body — buttons ушли в 16px inherited
CRITICAL bug — на staging buttons рендерились в 16px вместо handoff
workhorse 13px. UpdateBanner secondary buttons показывали invisible
текст (text-ink overrode by inherited text-on-accent от parent banner).

Root cause:
  buttonVariants base = 'rounded-md text-body font-medium ...'
  primary variant     = 'bg-navy text-on-accent ...'

tailwind-merge default config pattern-matches `text-*` как ОДНУ группу
(color). Видя `text-body text-on-accent` он считал это конфликтом двух
colors → keep last → text-on-accent остаётся, text-body стрипается.
Result: button теряет font-size → inherit body 16px.

Fix: extendTailwindMerge регистрирует наши custom semantic typography
utilities (text-body/cell/mono/cap/title-*) в группе font-size (отдельной
от colors). twMerge теперь знает `text-body text-on-accent` это size +
color, обе сохраняются.

Also: text-mono/text-cap в font-family группе (они baked Mono/Tektur)
— fix конфликт с font-mono/font-display tokens если кто-то их случайно
combined.

Verified в preview: 'Sign in' button class теперь содержит text-body,
computed fontSize: 13px (вместо 16px).

Plus CI: release job image node:22-alpine → repo.nstart.cloud/library/
node:22-alpine (Docker Hub rate-limited, mirror используется во всех
других jobs).
2026-05-11 15:45:27 +03:00
Александр Зимин 6db2a0c345 feat(admin-ui): catalog refresh + token migration (Stage 3.2) 2026-05-11 09:08:38 +00:00
Александр Зимин 3bfa7cae17 feat(admin-ui): shadcn/ui + Tailwind v4 foundation + theme switch 2026-05-10 21:46:47 +00:00
Александр Зимин fc2fb35717 fix(admin-ui): top-5 critical review bugs (auth + AJV + TZ + idempotency + storage) 2026-05-10 16:22:35 +00:00
Zimin A.N. 5e1b39db03 fix(dict): name column в records list — pick localized value, не [object Object]
Раньше для записей с x-localized name (КА, антенна, ground_station,
operator) колонка 'name / code' показывала literal '[object Object]'
потому что String({'ru-RU': 'Канопус', 'en-US': 'Kanopus'}) =
toString() стандартный → '[object Object]'.

Решение:
- Новый shared util lib/locales.ts:
  - pickLocalized(value, defaultLocale) — извлекает строку из i18n
    объекта или plain string. Fallback на first non-empty key
    если defaultLocale не найден.
  - recordDisplayName(data, defaultLocale) — name → code → '—' chain.
- Records list использует recordDisplayName с dict.defaultLocale
  (берётся из DictionaryDetail).
- SchemaDrivenForm дедуплицирован — был свой локальный pickLocalized,
  теперь импортирует из util'ы.

Tests: +13 (76 → 89), unit tests на pickLocalized + recordDisplayName
покрывают все edge cases (plain string, localized object, fallback
chain, null/undefined input).
2026-05-06 21:05:50 +03:00
Zimin A.N. 3f2a6b5d53 feat(admin-ui): scope accent on dictionary detail page
Перенесли scope-визуальные константы (SCOPE_ORDER, SCOPE_DOT,
SCOPE_ACCENT) в `lib/scope-style.ts` для переиспользования. На detail
странице (`dictionaries/$name`) добавили top accent strip 4px цветом
scope + цветной чип в breadcrumb рядом с back-link. Теперь при переходе
из списка в карточку scope сразу виден без чтения badge в таблице.
2026-05-06 12:50:44 +03:00
Zimin A.N. f7efb23e09 test+ci: 51 frontend unit-тестов (Vitest) + CI rollback на single resolve-tag
Tests (Vitest):
- src/lib/dates.ts (вынесено из SchemaDrivenForm + dictionaries.$name): parseFormDate,
  formatIsoDate, formatIsoDateTime, extractTime, combineDateTime, localTzOffset, nowIsoLocal
- src/lib/dates.test.ts (20 tests): TZ-safety, pure-date local midnight, far-future filter,
  round-trip parse/format, time extraction в local TZ, clear-button (null Date → '')
- src/components/schema/types.test.ts (31 tests): buildSchemaJson всех 9 kinds + x-unique +
  x-id-source, parseSchemaJson roundtrip, diffSchemas (8 breaking detection cases),
  suggestVersionBump (major/minor/patch + breaking-wins-mix)

CI:
- Откатил dual resolve-tag (BACKEND/FRONTEND) на single resolve-tag.
  GitLab variable interpolation в trigger jobs не подхватывает dotenv от needs-job
  → admin-ui-only push не доезжал до infra. Now backwards compat path работает
  как раньше с UPSTREAM_IMAGE_TAG.
- Backend docker jobs (maven-package + 4 docker-*) теперь без rules:changes —
  catch ImagePullBackOff если frontend push шёл без backend rebuild. BuildKit cache
  делает повторный билд идентичных layers ~30 sec.
- Path-filter оставлен на maven-test (тесты не запускаются на frontend-only PR
  — они бэкендовые) и на docker-admin-ui (frontend-only).

Run: pnpm test → 51 passed.
2026-05-04 04:37:24 +03:00