GlobalExceptionHandler не имел @ExceptionHandler для
MethodArgumentNotValidException — провал bean-валидации @Valid
@RequestBody (@NotBlank businessKey, @NotNull data и т.п.) падал в
generic Exception catch → HTTP 500 internal_error с пустым traceId.
admin-ui на создании/редактировании записи получал бесполезный
«Internal server error» без полей вместо 400 с пофайловой разбивкой.
Добавлен handler → 400 validation_failed с FieldError-списком.
Покрыто GlobalExceptionHandlerTest.
Файлы лежат в docs/integration/ с прошлых MR (!243, !247, !248), но в
toc.yaml не были включены — Diplodoc не строил их в /docs nav, и
пользователи могли попасть на них только по прямой ссылке.
Добавил три entries в секцию «Интеграция по API» сверху списка:
- 🚀 Quick Start (15 минут) → api-quickstart.md
- 📋 Cheat Sheet → api-cheatsheet.md
- ✅ Регламент — что заводить в НСИ → dictionary-criteria.md
После merge ordinis-docs container пересоберётся, и в admin UI ссылка
"Docs" (top-right) откроет страницу с этими entries в left nav.
Cheatsheet был отдельным файлом — теперь дополнительно встроен в
api-quickstart.md как Приложение A. Reasoning:
* Single source — интегратор открывает quickstart один раз, дальше
живёт в приложении (search в одном файле, не прыгает между двумя)
* Standalone api-cheatsheet.md сохранён для прямых ссылок и Postman README
* Section 13 обновлён: ссылка на Приложение + Postman collection
* Из appended body убраны self-references и дубликаты
Размер: 587 → 874 строк (+287 — cheatsheet body).
После Postman demo audit — в cheatsheet не хватало живых примеров,
которые можно скопировать и сразу проверить. Добавил:
* FK-зависимость:
GET /dictionaries/satellite_type/dependents →
[{sourceDict: spacecraft, sourceField: satellite_type_code,
onClose: BLOCK, activeRecordsInSourceDict: 14}]
+ список других живых FK на staging (ground_station, country etc).
* Локализация записи через Accept-Language:
ru: НАСА / Глобальный мониторинг суши и атмосферы
en: NASA / Global land and atmosphere monitoring
+ warning что ?locale= игнорируется и schema endpoint вообще
не локализуется.
* Time-travel: 14.05.2026 12:00 → 200, 14.05.2026 00:00 → 404
record_not_active (ожидаемое поведение, не bug).
* History и changelog/diff живые примеры с test1.
Также убрал password-grant из Auth (используется client_credentials —
это правильный flow для server-to-server интеграторов). И добавил
dependents проверку в 60-second smoke.
Tested: GET /dictionaries/spacecraft с Accept-Language: en-US возвращает
schemaJson с descriptions на ru (defaultLocale). Header игнорируется на
этом endpoint.
Локализация (Accept-Language header) работает только на records-side
(read-api DictionaryReadController). DictionaryDefinitionController
(writer/ordinis-app) возвращает schema без locale negotiation.
Cheatsheet обновлён, добавлен warning в query-params секцию.
После testing на staging выявлено: backend парсит локаль ТОЛЬКО из
Accept-Language header. ?locale=en-US query param сейчас игнорируется
(сервер возвращает default ru-RU вне зависимости от значения).
Реальный demo (spacecraft/1999-068A):
default (без header): operator: "НАСА"
Accept-Language: en-US: operator: "NASA"
?locale=en-US (без header): operator: "НАСА" ← не работает
Cheatsheet + quickstart обновлены. Postman collection тоже (используется
header). ?locale= query — отдельный backend feature request в roadmap.
ClassCastException on every GET /api/v1/dictionaries/{name}/snapshots:
java.lang.ClassCastException: class java.time.Instant cannot be cast
to class java.sql.Timestamp
Root cause: pgJDBC 42.7+ возвращает timestamptz из native query как
java.time.Instant, а старый код жёстко кастил в java.sql.Timestamp.
42.6 возвращал Timestamp — отсюда и тесты с Timestamp проходили,
а runtime ломался.
Fix: helper `toBucketOffsetDateTime(Object raw, OffsetDateTime now)`
ловит все 3 возможных типа от драйвера:
- java.sql.Timestamp (pgJDBC 42.6 и ранее)
- java.time.Instant (pgJDBC 42.7+ — current behavior)
- java.time.OffsetDateTime (если jdbc.timestamp.return-type=offset-date-time)
При любом другом типе бросаем IllegalStateException — fail-loud вместо
silent CCE на каждом запросе.
Tests:
+ snapshots_acceptsInstantFromPg42_7Driver — regression на конкретный
bug (Instant from current driver)
+ snapshots_acceptsOffsetDateTime — defensive coverage
+ snapshots_rejectsUnexpectedTimestampType — fail-loud branch
Existing tests (с Timestamp) сохранены для backward-compat.
Top-left dark-glass input на странице /graph. Substring match
case-insensitive по name + displayName (admin помнит либо
technical id `antenna`, либо русское название `Антенны`).
UX:
- Matched nodes остаются в full opacity, остальные dim'ятся (0.25) —
как при hover-focus. Force-layout не трогаем (positions стабильны).
- При единственном match → matched node получает focused border
(orange accent + glow) — легко найти результат.
- Edges светятся только когда оба конца — match. Иначе dim.
- Counter chip `N из M` под input'ом + `ничего не найдено` для empty.
- Esc clears query и снимает focus. ✕-кнопка тоже clears (focus
остаётся на input).
- Search dominates над hover dim — non-matched nodes остаются dim
даже если hover их затрагивает.
i18n: graph.search.placeholder / .empty / .count для ru-RU и en-US.
Force-simulation не пересчитываем при изменении query — это намеренно.
Иначе nodes прыгали бы по экрану на каждый символ, ломая spatial
recognition (юзер уже привык где какой node лежит).
Восемь дефектов из 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).
После !234 ship'нул AiSchemaController на writer, frontend hit /api/v1/ai/info
получает 500 — generic /api/v1/ location route к read-api который не имеет
этих handlers. Тот же pattern fix что для /drafts/, /me/, /admin/ — explicit
location к writer перед generic /api/v1/ fallback.
Без этого AI feature unusable в staging (даже если backend healthy).
Last item из QA report backlog'а. Banner + empty-state hint показывают
aggregate count, но не помогают find конкретные записи которые скоро
изменятся. Badge per row даёт O(1) visual scan.
Backend (read-api):
- ScheduledRecordsSummary.scheduledBusinessKeys: List<String> (capped 500)
- Repo method findScheduledBusinessKeys (DISTINCT subquery)
- Single fetch вместо N+1 per-row queries
Frontend:
- scheduledByKey Set<string> построен из summary
- Badge variant=info «Запланировано» рядом с businessKey badge'ом
- Title attribute с full RU/EN tooltip
- Не блокирует layout — gap-1.5 с pendingReview badge'ем (могут быть оба)
i18n keys для ru/en — short label «Запланировано» / «Scheduled».